Compare commits

...

4 Commits

235 changed files with 12512 additions and 6690 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -80,9 +80,11 @@
"react-transition-group": "^4.4.5", "react-transition-group": "^4.4.5",
"readdirp": "^4.1.1", "readdirp": "^4.1.1",
"recharts": "^2.15.3", "recharts": "^2.15.3",
"sharp": "^0.34.3",
"swr": "^2.3.2", "swr": "^2.3.2",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"valtio": "^2.1.3", "valtio": "^2.1.3",
"zlib": "^1.0.5",
"zod": "^3.24.3" "zod": "^3.24.3"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,51 +1,99 @@
[ [
{ {
"month": "Jan", "month": "Jan",
"year": 2025, "year": 2025,
"totalUnemployment": 160, "totalUnemployment": 160,
"educatedUnemployment": 95, "educatedUnemployment": 95,
"uneducatedUnemployment": 65, "uneducatedUnemployment": 65,
"percentageChange": null "percentageChange": 0.0
}, },
{ {
"month": "Feb", "month": "Feb",
"year": 2025, "year": 2025,
"totalUnemployment": 155, "totalUnemployment": 158,
"educatedUnemployment": 90, "educatedUnemployment": 93,
"uneducatedUnemployment": 65, "uneducatedUnemployment": 65,
"percentageChange": -3.1 "percentageChange": -1.25
}, },
{ {
"month": "Mar", "month": "Mar",
"year": 2025, "year": 2025,
"totalUnemployment": 150, "totalUnemployment": 155,
"educatedUnemployment": 88, "educatedUnemployment": 91,
"uneducatedUnemployment": 62, "uneducatedUnemployment": 64,
"percentageChange": -3.2 "percentageChange": -1.90
}, },
{ {
"month": "Apr", "month": "Apr",
"year": 2025, "year": 2025,
"totalUnemployment": 148, "totalUnemployment": 152,
"educatedUnemployment": 85, "educatedUnemployment": 89,
"uneducatedUnemployment": 63, "uneducatedUnemployment": 63,
"percentageChange": -1.3 "percentageChange": -1.94
}, },
{ {
"month": "Mei", "month": "Mei",
"year": 2025, "year": 2025,
"totalUnemployment": 145, "totalUnemployment": 150,
"educatedUnemployment": 82, "educatedUnemployment": 88,
"uneducatedUnemployment": 63, "uneducatedUnemployment": 62,
"percentageChange": -2.0 "percentageChange": -1.32
}, },
{ {
"month": "Jun", "month": "Jun",
"year": 2025, "year": 2025,
"totalUnemployment": 140, "totalUnemployment": 148,
"educatedUnemployment": 80, "educatedUnemployment": 87,
"uneducatedUnemployment": 60, "uneducatedUnemployment": 61,
"percentageChange": -3.4 "percentageChange": -1.33
} },
] {
"month": "Jul",
"year": 2025,
"totalUnemployment": 145,
"educatedUnemployment": 85,
"uneducatedUnemployment": 60,
"percentageChange": -2.03
},
{
"month": "Agu",
"year": 2025,
"totalUnemployment": 142,
"educatedUnemployment": 84,
"uneducatedUnemployment": 58,
"percentageChange": -2.07
},
{
"month": "Sep",
"year": 2025,
"totalUnemployment": 140,
"educatedUnemployment": 83,
"uneducatedUnemployment": 57,
"percentageChange": -1.41
},
{
"month": "Okt",
"year": 2025,
"totalUnemployment": 138,
"educatedUnemployment": 82,
"uneducatedUnemployment": 56,
"percentageChange": -1.43
},
{
"month": "Nov",
"year": 2025,
"totalUnemployment": 135,
"educatedUnemployment": 80,
"uneducatedUnemployment": 55,
"percentageChange": -2.17
},
{
"month": "Des",
"year": 2025,
"totalUnemployment": 132,
"educatedUnemployment": 78,
"uneducatedUnemployment": 54,
"percentageChange": -2.22
}
]

View File

@@ -0,0 +1,137 @@
[
{
"id": "cmff0rr4z0002vn0twp333m2",
"name": "S6RIjFaPvdQm3oq4rM4X9-desktop.webp",
"realName": "bares.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/S6RIjFaPvdQm3oq4rM4X9-desktop.webp",
"category": "image"
},
{
"id": "cmff0tnf00003vn0t3kgzi0u0",
"name": "_pVNEmThU5ICGa8gv3gh_-desktop.webp",
"realName": "bicara-darma.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/_pVNEmThU5ICGa8gv3gh_-desktop.webp",
"category": "image"
},
{
"id": "cmff0uykf0004vn0trmmxpgfh",
"name": "bv6rdKvjxkkjUSGLQ0lvB-desktop.webp",
"realName": "daves.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/bv6rdKvjxkkjUSGLQ0lvB-desktop.webp",
"category": "image"
},
{
"id": "cmff0z34f0005vn0tjtvq519p",
"name": "Z4hWaV04CvoE20MjccQsV-desktop.webp",
"realName": "mangan.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/Z4hWaV04CvoE20MjccQsV-desktop.webp",
"category": "image"
},
{
"id": "cmff38cyq000bvn0t9f01cz3f",
"name": "LvLAtOqWojx4sn6NjJWB9-desktop.webp",
"realName": "gelah-melah.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/LvLAtOqWojx4sn6NjJWB9-desktop.webp",
"category": "image"
},
{
"id": "cmff0zqvd0007vn0tv6o5hjcq",
"name": "gR2mcvAQVgJ2-rM5coYJj-desktop.webp",
"realName": "inovasi-desa-darmasaba.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/gR2mcvAQVgJ2-rM5coYJj-desktop.webp",
"category": "image"
},
{
"id": "cmff1013m0008vn0th7t0d64d",
"name": "JpL-9F8-IGztMn8E2ce02-desktop.webp",
"realName": "pdkt.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/JpL-9F8-IGztMn8E2ce02-desktop.webp",
"category": "image"
},
{
"id": "cmff10cwq0009vn0tse8dzu3j",
"name": "bxAk4AsGbJTC705_IVdes-desktop.webp",
"realName": "sajjiana-dharma-raksaka.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/bxAk4AsGbJTC705_IVdes-desktop.webp",
"category": "image"
},
{
"id": "cmff2w5ly000avn0telhct71k",
"name": "Vbj_osnMJUkGEQGDTLwV--desktop.webp",
"realName": "perbekel.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/Vbj_osnMJUkGEQGDTLwV--desktop.webp",
"category": "image"
},
{
"id": "cmff3joae0000vn6h8sgs0ilg",
"name": "7hox9spUxj56hY_EBYLnj-desktop.webp",
"realName": "youtube.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/7hox9spUxj56hY_EBYLnj-desktop.webp",
"category": "image"
},
{
"id": "cmff3ll130001vn6hkhls3f5y",
"name": "ChihV7_1eS-AGtSg9UwMv-desktop.webp",
"realName": "gmail.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/ChihV7_1eS-AGtSg9UwMv-desktop.webp",
"category": "image"
},
{
"id": "cmff3mtat0002vn6hs8vyyhdd",
"name": "z8v9ZREwOJHKGIRYauROt-desktop.webp",
"realName": "facebook.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/z8v9ZREwOJHKGIRYauROt-desktop.webp",
"category": "image"
},
{
"id": "cmff3nv180003vn6h5jvedidq",
"name": "BLjMxTKoCNE31uOURR3IU-desktop.webp",
"realName": "telephone-call.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/BLjMxTKoCNE31uOURR3IU-desktop.webp",
"category": "image"
},
{
"id": "cmff3oouh0004vn6hd94brzv9",
"name": "hkJYAeTNWK_vYaYS20w3I-desktop.webp",
"realName": "instagram.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/hkJYAeTNWK_vYaYS20w3I-desktop.webp",
"category": "image"
},
{
"id": "cmff3q12g0005vn6h5ojov2qa",
"name": "6XEoZ9SFu59COpil03Gya-desktop.webp",
"realName": "tiktok.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/6XEoZ9SFu59COpil03Gya-desktop.webp",
"category": "image"
}
]

View File

@@ -2,31 +2,37 @@
{ {
"id": "cmds8w2q60002vnbe6i8qhkuo", "id": "cmds8w2q60002vnbe6i8qhkuo",
"name": "Telephone Desa Darmasaba", "name": "Telephone Desa Darmasaba",
"iconUrl": "081239580000" "iconUrl": "081239580000",
"imageId": "cmff3nv180003vn6h5jvedidq"
}, },
{ {
"id": "cmds8z7u20005vnbegyyvnbk0", "id": "cmds8z7u20005vnbegyyvnbk0",
"name": "Email Desa Darmasaba", "name": "Email Desa Darmasaba",
"iconUrl": "desadarmasaba@badungkab.go.id" "iconUrl": "desadarmasaba@badungkab.go.id",
"imageId": "cmff3ll130001vn6hkhls3f5y"
}, },
{ {
"id": "cmds9023u0008vnbe3oxmhwyf", "id": "cmds9023u0008vnbe3oxmhwyf",
"name": "Desa Darmasaba", "name": "Desa Darmasaba",
"iconUrl": "https://www.youtube.com/channel/UCtPw9WOQO7d2HIKzKgel4Xg" "iconUrl": "https://www.youtube.com/channel/UCtPw9WOQO7d2HIKzKgel4Xg",
"imageId": "cmff3joae0000vn6h8sgs0ilg"
}, },
{ {
"id": "cmds90oul000bvnbe2bqkptoi", "id": "cmds90oul000bvnbe2bqkptoi",
"name": "Pemerintah Desa Darmasaba", "name": "Pemerintah Desa Darmasaba",
"iconUrl": "https://www.facebook.com/DarmasabaDesaku" "iconUrl": "https://www.facebook.com/DarmasabaDesaku",
"imageId": "cmff3mtat0002vn6hs8vyyhdd"
}, },
{ {
"id": "cmds91i4e000evnbe8gtf1gub", "id": "cmds91i4e000evnbe8gtf1gub",
"name": "ddarmasaba", "name": "ddarmasaba",
"iconUrl": "https://www.instagram.com/ddarmasaba/" "iconUrl": "https://www.instagram.com/ddarmasaba/",
"imageId": "cmff3oouh0004vn6hd94brzv9"
}, },
{ {
"id": "cmds92de5000hvnbemlu6sq5x", "id": "cmds92de5000hvnbemlu6sq5x",
"name": "desa.darmasaba", "name": "desa.darmasaba",
"iconUrl": "https://www.tiktok.com/@desa.darmasaba?is_from_webapp=1&sender_device=pc" "iconUrl": "https://www.tiktok.com/@desa.darmasaba?is_from_webapp=1&sender_device=pc",
"imageId": "cmff3q12g0005vn6h5ojov2qa"
} }
] ]

View File

@@ -2,6 +2,7 @@
{ {
"id": "edit", "id": "edit",
"name": "I.B Surya Prabhawa Manuaba, S.H., M.H.", "name": "I.B Surya Prabhawa Manuaba, S.H., M.H.",
"position": "Perbekel Darmasaba periode 2021-2027" "position": "Perbekel Darmasaba periode 2021-2027",
"imageId": "cmff2w5ly000avn0telhct71k"
} }
] ]

View File

@@ -1,50 +1,51 @@
[ [
{
"id": "cmdr7039z0002vn5rttctt9hn",
"name": "Davest",
"description": "Darmasaba Village Festval",
"link": "https://darmasaba.desa.id/berita/55862-rakor-davest-2024"
},
{ {
"id": "cmdr755pf0005vn5rp8tyuubw", "id": "cmdr755pf0005vn5rp8tyuubw",
"name": "Dmangan", "name": "Dmangan",
"description": "Darmasaba Aman Pangan", "description": "Darmasaba Aman Pangan",
"link": "https://darmasaba.desa.id/berita/61452-kader-d-mangan-berhasil-meraih-prestasi-dalam-ajang-lomba-banjar-bali-quis-bbq-tahun-2024" "link": "https://darmasaba.desa.id/berita/61452-kader-d-mangan-berhasil-meraih-prestasi-dalam-ajang-lomba-banjar-bali-quis-bbq-tahun-2024",
"imageId" : "cmff0z34f0005vn0tjtvq519p"
}, },
{ {
"id": "cmdr76nqk0008vn5rdddvcxnr", "id": "cmdr76nqk0008vn5rdddvcxnr",
"name": "Bicara Darmasaba", "name": "Bicara Darmasaba",
"description": "Bicara Darmasaba", "description": "Bicara Darmasaba",
"link": "https://darmasaba.desa.id/berita/42506-bicara-darmasaba" "link": "https://darmasaba.desa.id/berita/42506-bicara-darmasaba",
"imageId" : "cmff0tnf00003vn0t3kgzi0u0"
}, },
{ {
"id": "cmdr77vbw000bvn5rvpmoq31s", "id": "cmdr77vbw000bvn5rvpmoq31s",
"name": "Bares", "name": "Bares",
"description": "Darmasaba Recycling Stock/Exchange", "description": "Darmasaba Recycling Stock/Exchange",
"link": "http://darmasaba.desa.id/berita/56722-bares" "link": "http://darmasaba.desa.id/berita/56722-bares",
"imageId" : "cmff0rr4z0002vn0twp333m2"
}, },
{ {
"id": "cmdr7bxtp000evn5rmy85wihx", "id": "cmdr7bxtp000evn5rmy85wihx",
"name": "Sajjana Dharma Raksaka", "name": "Sajjana Dharma Raksaka",
"description": "Sajjana Dharma Raksaka", "description": "Sajjana Dharma Raksaka",
"link": "https://ppid.badungkab.go.id/storage/dokumen/5RS9dldGkrgzMQq6bKdZsqsVRHI8gffWv4PGfb3r.pdf" "link": "https://ppid.badungkab.go.id/storage/dokumen/5RS9dldGkrgzMQq6bKdZsqsVRHI8gffWv4PGfb3r.pdf",
"imageId" : "cmff10cwq0009vn0tse8dzu3j"
}, },
{ {
"id": "cmdr7dlnk000hvn5r9lur3z35", "id": "cmdr7dlnk000hvn5r9lur3z35",
"name": "PDKT", "name": "PDKT",
"description": "Perangkat Desa Kuat Teknologi", "description": "Perangkat Desa Kuat Teknologi",
"link": "https://darmasaba.desa.id/berita/53752-p-d-k-t" "link": "https://darmasaba.desa.id/berita/53752-p-d-k-t",
"imageId" : "cmff1013m0008vn0th7t0d64d"
}, },
{ {
"id": "cmdr7ftob000mvn5rfhgdtg8v", "id": "cmdr7ftob000mvn5rfhgdtg8v",
"name": "GM", "name": "GM",
"description": "Galah Melah", "description": "Galah Melah",
"link": "https://darmasaba.desa.id/berita/52880-galah-melah" "link": "https://darmasaba.desa.id/berita/52880-galah-melah",
"imageId" : "cmff38cyq000bvn0t9f01cz3f"
}, },
{ {
"id": "cmdr7glue000pvn5r6onzslju", "id": "cmdr7glue000pvn5r6onzslju",
"name": "Inovasi Desa Darmasaba", "name": "Inovasi Desa Darmasaba",
"description": "Inovasi Desa Darmasaba", "description": "Inovasi Desa Darmasaba",
"link": "https://darmasaba.desa.id/produk-lokal-desa" "link": "https://darmasaba.desa.id/produk-lokal-desa",
"imageId" : "cmff0zqvd0007vn0tv6o5hjcq"
} }
] ]

View File

@@ -92,7 +92,7 @@ model FileStorage {
PejabatDesa PejabatDesa[] PejabatDesa PejabatDesa[]
MediaSosial MediaSosial[] MediaSosial MediaSosial[]
DesaAntiKorupsi DesaAntiKorupsi[] DesaAntiKorupsi DesaAntiKorupsi[]
SDGSDesa SDGSDesa[] SDGSDesa SdgsDesa[]
APBDesImage APBDes[] @relation("APBDesImage") APBDesImage APBDes[] @relation("APBDesImage")
APBDesFile APBDes[] @relation("APBDesFile") APBDesFile APBDes[] @relation("APBDesFile")
PrestasiDesa PrestasiDesa[] PrestasiDesa PrestasiDesa[]
@@ -168,9 +168,9 @@ model KategoriDesaAntiKorupsi {
} }
//========================================= SDGS Desa ========================================= // //========================================= SDGS Desa ========================================= //
model SDGSDesa { model SdgsDesa {
id String @id @default(cuid()) id String @id @default(cuid())
name String @unique name String
jumlah String jumlah String
image FileStorage? @relation(fields: [imageId], references: [id]) image FileStorage? @relation(fields: [imageId], references: [id])
imageId String? imageId String?
@@ -183,7 +183,7 @@ model SDGSDesa {
//========================================= APBDes ========================================= // //========================================= APBDes ========================================= //
model APBDes { model APBDes {
id String @id @default(cuid()) id String @id @default(cuid())
name String @unique name String
jumlah String jumlah String
image FileStorage? @relation("APBDesImage", fields: [imageId], references: [id]) image FileStorage? @relation("APBDesImage", fields: [imageId], references: [id])
imageId String? imageId String?
@@ -198,7 +198,7 @@ model APBDes {
//========================================= PRESTASI DESA ========================================= // //========================================= PRESTASI DESA ========================================= //
model PrestasiDesa { model PrestasiDesa {
id String @id @default(cuid()) id String @id @default(cuid())
name String @unique name String
deskripsi String @db.Text deskripsi String @db.Text
kategori KategoriPrestasiDesa @relation(fields: [kategoriId], references: [id]) kategori KategoriPrestasiDesa @relation(fields: [kategoriId], references: [id])
kategoriId String kategoriId String
@@ -1216,25 +1216,40 @@ model LayananPolsek {
// ========================================= KONTAK DARURAT ========================================= // // ========================================= KONTAK DARURAT ========================================= //
model KontakDaruratKeamanan { model KontakDaruratKeamanan {
id String @id @default(uuid()) id String @id @default(uuid())
nama String // contoh: "Layanan Darurat", "Fasilitas Kesehatan" nama String
image FileStorage? @relation(fields: [imageId], references: [id]) image FileStorage? @relation(fields: [imageId], references: [id])
imageId String? imageId String?
kontakItems KontakItem[] kategori KontakItem @relation(fields: [kategoriId], references: [id])
createdAt DateTime @default(now()) kategoriId String
updatedAt DateTime @updatedAt kontakItems KontakDaruratToItem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
} }
model KontakItem { model KontakItem {
id String @id @default(uuid()) id String @id @default(uuid())
nama String // contoh: "Polisi", "Ambulans", "Puskesmas Darmasaba" nama String
nomorTelepon String nomorTelepon String
image FileStorage? @relation(fields: [imageId], references: [id]) image FileStorage? @relation(fields: [imageId], references: [id])
imageId String? imageId String?
kategori KontakDaruratKeamanan @relation(fields: [kategoriId], references: [id]) createdAt DateTime @default(now())
kategoriId String updatedAt DateTime @updatedAt
createdAt DateTime @default(now()) deletedAt DateTime @default(now())
updatedAt DateTime @updatedAt isActive Boolean @default(true)
KontakDaruratToItem KontakDaruratToItem[]
KontakDaruratKeamanan KontakDaruratKeamanan[]
}
model KontakDaruratToItem {
id String @id @default(uuid())
kontakDaruratId String
kontakItemId String
kontakDarurat KontakDaruratKeamanan @relation(fields: [kontakDaruratId], references: [id])
kontakItem KontakItem @relation(fields: [kontakItemId], references: [id])
createdAt DateTime @default(now())
} }
// ========================================= PENCEGAHAN KRIMINALITAS ========================================= // // ========================================= PENCEGAHAN KRIMINALITAS ========================================= //
@@ -1401,6 +1416,9 @@ model PosisiOrganisasi {
pegawai Pegawai[] pegawai Pegawai[]
strukturOrganisasi StrukturOrganisasi[] // Relasi balik strukturOrganisasi StrukturOrganisasi[] // Relasi balik
StrukturOrganisasiPPID StrukturOrganisasiPPID[] StrukturOrganisasiPPID StrukturOrganisasiPPID[]
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("posisi_organisasi") @@map("posisi_organisasi")
} }
@@ -1469,7 +1487,7 @@ model ProgramKemiskinan {
id String @id @default(uuid()) id String @id @default(uuid())
nama String nama String
deskripsi String deskripsi String
ikonUrl String? icon String
isActive Boolean @default(true) isActive Boolean @default(true)
statistikId String? @unique statistikId String? @unique
statistik StatistikKemiskinan? @relation(fields: [statistikId], references: [id]) statistik StatistikKemiskinan? @relation(fields: [statistikId], references: [id])
@@ -1551,7 +1569,7 @@ model DataDemografiPekerjaan {
model DetailDataPengangguran { model DetailDataPengangguran {
id String @id @default(uuid()) @db.Uuid id String @id @default(uuid()) @db.Uuid
month String @db.VarChar(20) month String @db.VarChar(20)
year DateTime year Int
totalUnemployment Int totalUnemployment Int
educatedUnemployment Int educatedUnemployment Int
uneducatedUnemployment Int uneducatedUnemployment Int
@@ -2106,18 +2124,18 @@ model KategoriBuku {
// ========================================= USER ========================================= // // ========================================= USER ========================================= //
model User { model User {
id String @id @default(cuid()) id String @id @default(cuid())
username String username String
nomor String @unique nomor String @unique
role Role @relation(fields: [roleId], references: [id]) role Role @relation(fields: [roleId], references: [id])
roleId String @default("1") roleId String @default("1")
instansi String? instansi String?
UserSession UserSession? // Nama instansi (Puskesmas, Sekolah, dll) UserSession UserSession? // Nama instansi (Puskesmas, Sekolah, dll)
isActive Boolean @default(true) isActive Boolean @default(true)
lastLogin DateTime? lastLogin DateTime?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime? deletedAt DateTime?
} }
model Role { model Role {

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import profilePejabatDesa from "./data/landing-page/profile/profile.json"; import profilePejabatDesa from "./data/landing-page/profile/profile.json";
import programInovasi from "./data/landing-page/profile/programInovasi.json"; import programInovasi from "./data/landing-page/profile/programInovasi.json";
@@ -15,7 +16,7 @@ import posisiOrganisasiPPID from "./data/ppid/struktur-ppid/posisi-organisasi-PP
import visiMisiPPID from "./data/ppid/visi-misi-ppid/visimisiPPID.json"; import visiMisiPPID from "./data/ppid/visi-misi-ppid/visimisiPPID.json";
import dasarHukumPPID from "./data/ppid/dasar-hukum-ppid/dasarhukumPPID.json"; import dasarHukumPPID from "./data/ppid/dasar-hukum-ppid/dasarhukumPPID.json";
import jenisKelamin from "./data/ppid/ikm/jenis-kelamin/jenis-kelamin.json"; import jenisKelamin from "./data/ppid/ikm/jenis-kelamin/jenis-kelamin.json";
import daftarInformasiPublik from "./data/ppid/daftar-informasi-publik-desa-darmasaba/daftarInformasi.json" import daftarInformasiPublik from "./data/ppid/daftar-informasi-publik-desa-darmasaba/daftarInformasi.json";
import pilihanRatingResponden from "./data/ppid/ikm/pilihan-rating-responden/rating-responden.json"; import pilihanRatingResponden from "./data/ppid/ikm/pilihan-rating-responden/rating-responden.json";
import umurResponden from "./data/ppid/ikm/umur-responden/umur-responden.json"; import umurResponden from "./data/ppid/ikm/umur-responden/umur-responden.json";
import categoryPengumuman from "./data/category-pengumuman.json"; import categoryPengumuman from "./data/category-pengumuman.json";
@@ -54,63 +55,90 @@ import programUnggulan from "./data/pendidikan/program-pendidikan-anak/program-u
import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-program.json"; import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-program.json";
import roles from "./data/user/roles.json"; import roles from "./data/user/roles.json";
import users from "./data/user/users.json"; import users from "./data/user/users.json";
import fileStorage from "./data/file-storage.json";
(async () => { (async () => {
// =========== USER & ROLE =========== // =========== USER & ROLE ===========
// In your seed.ts // In your seed.ts
// =========== ROLES =========== // =========== ROLES ===========
console.log("🔄 Seeding roles..."); console.log("🔄 Seeding roles...");
for (const r of roles) { for (const r of roles) {
await prisma.role.upsert({ await prisma.role.upsert({
where: { id: r.id }, where: { id: r.id },
update: { update: {
name: r.name, name: r.name,
description: r.description, description: r.description,
permissions: r.permissions, permissions: r.permissions,
isActive: r.isActive, isActive: r.isActive,
}, },
create: { create: {
id: r.id, id: r.id,
name: r.name, name: r.name,
description: r.description, description: r.description,
permissions: r.permissions, permissions: r.permissions,
isActive: r.isActive, isActive: r.isActive,
}, },
}); });
}
console.log("✅ Roles seeded");
// =========== USERS ===========
console.log("🔄 Seeding users...");
for (const u of users) {
// First verify the role exists
const roleExists = await prisma.role.findUnique({
where: { id: u.roleId }
});
if (!roleExists) {
console.error(`❌ Role with id ${u.roleId} not found for user ${u.nama}`);
continue;
} }
console.log("✅ Roles seeded");
await prisma.user.upsert({ // =========== USERS ===========
where: { id: u.id }, console.log("🔄 Seeding users...");
update: { for (const u of users) {
username: u.nama, // First verify the role exists
nomor: u.nomor, const roleExists = await prisma.role.findUnique({
roleId: u.roleId, where: { id: u.roleId },
isActive: u.isActive, });
},
create: { if (!roleExists) {
id: u.id, console.error(`❌ Role with id ${u.roleId} not found for user ${u.nama}`);
username: u.nama, continue;
nomor: u.nomor, }
roleId: u.roleId,
isActive: u.isActive, await prisma.user.upsert({
}, where: { id: u.id },
}); update: {
} username: u.nama,
console.log("✅ Users seeded"); nomor: u.nomor,
roleId: u.roleId,
isActive: u.isActive,
},
create: {
id: u.id,
username: u.nama,
nomor: u.nomor,
roleId: u.roleId,
isActive: u.isActive,
},
});
}
console.log("✅ Users seeded");
// =========== FILE STORAGE ===========
console.log("🔄 Seeding file storage...");
for (const f of fileStorage) {
await prisma.fileStorage.upsert({
where: { id: f.id },
update: {
name: f.name,
realName: f.realName,
path: f.path,
mimeType: f.mimeType,
link: f.link,
category: f.category,
},
create: {
id: f.id,
name: f.name,
realName: f.realName,
path: f.path,
mimeType: f.mimeType,
link: f.link,
category: f.category,
},
});
}
console.log("✅ File storage seeded");
// =========== LANDING PAGE =========== // =========== LANDING PAGE ===========
// =========== SUBMENU PROFILE =========== // =========== SUBMENU PROFILE ===========
// =========== PROFILE PEJABAT DESA =========== // =========== PROFILE PEJABAT DESA ===========
@@ -120,11 +148,13 @@ console.log("✅ Users seeded");
update: { update: {
name: p.name, name: p.name,
position: p.position, position: p.position,
imageId: p.imageId,
}, },
create: { create: {
id: p.id, id: p.id,
name: p.name, name: p.name,
position: p.position, position: p.position,
imageId: p.imageId,
}, },
}); });
} }
@@ -134,18 +164,35 @@ console.log("✅ Users seeded");
// =========== PROGRAM INOVASI =========== // =========== PROGRAM INOVASI ===========
for (const p of programInovasi) { for (const p of programInovasi) {
let imageId: string | null = null;
if (p.imageId) {
const imageExists = await prisma.fileStorage.findUnique({
where: { id: p.imageId },
});
if (imageExists) {
imageId = p.imageId;
} else {
console.warn(
`⚠️ imageId ${p.imageId} tidak ditemukan untuk ProgramInovasi ${p.name}`
);
}
}
await prisma.programInovasi.upsert({ await prisma.programInovasi.upsert({
where: { id: p.id }, where: { id: p.id },
update: { update: {
name: p.name, name: p.name,
description: p.description, description: p.description,
link: p.link, link: p.link,
imageId: p.imageId,
}, },
create: { create: {
id: p.id, id: p.id,
name: p.name, name: p.name,
description: p.description, description: p.description,
link: p.link, link: p.link,
imageId: p.imageId,
}, },
}); });
} }
@@ -158,11 +205,13 @@ console.log("✅ Users seeded");
update: { update: {
name: p.name, name: p.name,
iconUrl: p.iconUrl, iconUrl: p.iconUrl,
imageId: p.imageId,
}, },
create: { create: {
id: p.id, id: p.id,
name: p.name, name: p.name,
iconUrl: p.iconUrl, iconUrl: p.iconUrl,
imageId: p.imageId,
}, },
}); });
} }
@@ -308,11 +357,8 @@ console.log("✅ Users seeded");
// =========== SDGSDesa =========== // =========== SDGSDesa ===========
for (const l of sdgsDesa) { for (const l of sdgsDesa) {
await prisma.sDGSDesa.upsert({ await prisma.sdgsDesa.upsert({
where: { where: { id: l.id },
name: l.name,
jumlah: l.jumlah,
},
update: { update: {
name: l.name, name: l.name,
jumlah: l.jumlah, jumlah: l.jumlah,
@@ -330,8 +376,7 @@ console.log("✅ Users seeded");
for (const l of apbdes) { for (const l of apbdes) {
await prisma.aPBDes.upsert({ await prisma.aPBDes.upsert({
where: { where: {
name: l.name, id: l.id,
jumlah: l.jumlah,
}, },
update: { update: {
name: l.name, name: l.name,
@@ -558,29 +603,29 @@ console.log("✅ Users seeded");
} }
console.log("dasar hukum PPID success ..."); console.log("dasar hukum PPID success ...");
// =========== SUBMENU DAFTAR INFORMASI PUBLIK PPID =========== // =========== SUBMENU DAFTAR INFORMASI PUBLIK PPID ===========
for (const v of daftarInformasiPublik) { for (const v of daftarInformasiPublik) {
// Convert string date to Date object // Convert string date to Date object
const tanggal = new Date(v.tanggal); const tanggal = new Date(v.tanggal);
await prisma.daftarInformasiPublik.upsert({ await prisma.daftarInformasiPublik.upsert({
where: { where: {
id: v.id, id: v.id,
}, },
update: { update: {
jenisInformasi: v.jenisInformasi, jenisInformasi: v.jenisInformasi,
deskripsi: v.deskripsi, deskripsi: v.deskripsi,
tanggal: tanggal, tanggal: tanggal,
}, },
create: { create: {
id: v.id, id: v.id,
jenisInformasi: v.jenisInformasi, jenisInformasi: v.jenisInformasi,
deskripsi: v.deskripsi, deskripsi: v.deskripsi,
tanggal: tanggal, tanggal: tanggal,
}, },
}); });
} }
console.log("daftar informasi publik PPID success ..."); console.log("daftar informasi publik PPID success ...");
for (const l of pelayananPerizinanBerusaha) { for (const l of pelayananPerizinanBerusaha) {
await prisma.pelayananPerizinanBerusaha.upsert({ await prisma.pelayananPerizinanBerusaha.upsert({
@@ -849,12 +894,9 @@ console.log("✅ Users seeded");
console.log("hubungan organisasi success ..."); console.log("hubungan organisasi success ...");
for (const d of detailDataPengangguran) { for (const d of detailDataPengangguran) {
// Convert the year to a Date object (using January 1st of the year as the date)
const yearAsDate = new Date(d.year, 0, 1);
await prisma.detailDataPengangguran.upsert({ await prisma.detailDataPengangguran.upsert({
where: { where: {
month_year: { month: d.month, year: yearAsDate }, month_year: { month: d.month, year: d.year },
}, },
update: { update: {
totalUnemployment: d.totalUnemployment, totalUnemployment: d.totalUnemployment,
@@ -864,7 +906,7 @@ console.log("✅ Users seeded");
}, },
create: { create: {
month: d.month, month: d.month,
year: yearAsDate, year: d.year,
totalUnemployment: d.totalUnemployment, totalUnemployment: d.totalUnemployment,
educatedUnemployment: d.educatedUnemployment, educatedUnemployment: d.educatedUnemployment,
uneducatedUnemployment: d.uneducatedUnemployment, uneducatedUnemployment: d.uneducatedUnemployment,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

View File

@@ -23,6 +23,7 @@ export default function SpashScreen() {
<Paper p={"md"} miw={320}> <Paper p={"md"} miw={320}>
<Flex> <Flex>
<Image <Image
loading="lazy"
src={images["darmasaba-icon"]} src={images["darmasaba-icon"]}
alt="darmasaba" alt="darmasaba"
w={100} w={100}

View File

@@ -0,0 +1,83 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import React from 'react'
import {
IconLeaf,
IconTrophy,
IconTent,
IconChartLine,
IconRecycle,
IconTruck,
IconScale,
IconClipboard,
IconTrash,
IconHomeEco,
IconChristmasTreeFilled,
IconTrendingUp,
IconShieldFilled,
IconHome,
IconTree,
IconDroplet,
IconCash,
IconSchool,
IconShoppingCart,
IconHospital,
} from '@tabler/icons-react'
export type IconKey =
| 'ekowisata'
| 'kompetisi'
| 'wisata'
| 'ekonomi'
| 'sampah'
| 'truck'
| 'scale'
| 'clipboard'
| 'trash'
| 'lingkunganSehat'
| 'sumberOksigen'
| 'ekonomiBerkelanjutan'
| 'mencegahBencana'
| 'rumah'
| 'pohon'
| 'air'
| 'bantuan'
| 'pelatihan'
| 'subsidi'
| 'layananKesehatan'
const iconMap: Record<IconKey, React.FC<any>> = {
ekowisata: IconLeaf,
kompetisi: IconTrophy,
wisata: IconTent,
ekonomi: IconChartLine,
sampah: IconRecycle,
truck: IconTruck,
scale: IconScale,
clipboard: IconClipboard,
trash: IconTrash,
lingkunganSehat: IconHomeEco,
sumberOksigen: IconChristmasTreeFilled,
ekonomiBerkelanjutan: IconTrendingUp,
mencegahBencana: IconShieldFilled,
rumah: IconHome,
pohon: IconTree,
air: IconDroplet,
bantuan: IconCash,
pelatihan: IconSchool,
subsidi: IconShoppingCart,
layananKesehatan: IconHospital,
}
type Props = {
name: IconKey
size?: number
color?: string
}
export const IconMapper: React.FC<Props> = ({ name, size = 24, color }) => {
const IconComponent = iconMap[name]
if (!IconComponent) return null
return <IconComponent size={size} color={color} />
}

View File

@@ -3,16 +3,20 @@
import { Box, rem, Select } from '@mantine/core'; import { Box, rem, Select } from '@mantine/core';
import { import {
IconCash,
IconChartLine, IconChartLine,
IconChristmasTreeFilled, IconChristmasTreeFilled,
IconClipboardTextFilled, IconClipboardTextFilled,
IconDroplet, IconDroplet,
IconHome, IconHome,
IconHomeEco, IconHomeEco,
IconHospital,
IconLeaf, IconLeaf,
IconRecycle, IconRecycle,
IconScale, IconScale,
IconSchool,
IconShieldFilled, IconShieldFilled,
IconShoppingCart,
IconTent, IconTent,
IconTrashFilled, IconTrashFilled,
IconTree, IconTree,
@@ -32,13 +36,17 @@ const iconMap = {
scale: { label: 'Scale', icon: IconScale }, scale: { label: 'Scale', icon: IconScale },
clipboard: { label: 'Clipboard', icon: IconClipboardTextFilled }, clipboard: { label: 'Clipboard', icon: IconClipboardTextFilled },
trash: { label: 'Trash', icon: IconTrashFilled }, trash: { label: 'Trash', icon: IconTrashFilled },
lingkunganSehat: {label: 'Lingkungan Sehat', icon: IconHomeEco}, lingkunganSehat: { label: 'Lingkungan Sehat', icon: IconHomeEco },
sumberOksigen: {label: 'Sumber Oksigen', icon: IconChristmasTreeFilled}, sumberOksigen: { label: 'Sumber Oksigen', icon: IconChristmasTreeFilled },
ekonomiBerkelanjutan: {label: 'Ekonomi Berkelanjutan', icon: IconTrendingUp}, ekonomiBerkelanjutan: { label: 'Ekonomi Berkelanjutan', icon: IconTrendingUp },
mencegahBencana: {label: 'Mencegah Bencana', icon: IconShieldFilled}, mencegahBencana: { label: 'Mencegah Bencana', icon: IconShieldFilled },
rumah: {label: 'Rumah', icon: IconHome}, rumah: { label: 'Rumah', icon: IconHome },
pohon: {label: 'Pohon', icon: IconTree}, pohon: { label: 'Pohon', icon: IconTree },
air: {label: 'Air', icon: IconDroplet} air: { label: 'Air', icon: IconDroplet },
bantuan: { label: 'Bantuan', icon: IconCash },
pelatihan: { label: 'Pelatihan', icon: IconSchool },
subsidi: { label: 'Subsidi', icon: IconShoppingCart },
layananKesehatan: { label: 'Layanan Kesehatan', icon: IconHospital },
}; };

View File

@@ -2,16 +2,20 @@
import { Box, rem, Select } from '@mantine/core'; import { Box, rem, Select } from '@mantine/core';
import { import {
IconCash,
IconChartLine, IconChartLine,
IconChristmasTreeFilled, IconChristmasTreeFilled,
IconClipboardTextFilled, IconClipboardTextFilled,
IconDroplet, IconDroplet,
IconHome, IconHome,
IconHomeEco, IconHomeEco,
IconHospital,
IconLeaf, IconLeaf,
IconRecycle, IconRecycle,
IconScale, IconScale,
IconSchool,
IconShieldFilled, IconShieldFilled,
IconShoppingCart,
IconTent, IconTent,
IconTrashFilled, IconTrashFilled,
IconTree, IconTree,
@@ -36,7 +40,11 @@ const iconMap = {
mencegahBencana: {label: 'Mencegah Bencana', icon: IconShieldFilled}, mencegahBencana: {label: 'Mencegah Bencana', icon: IconShieldFilled},
rumah: {label: 'Rumah', icon: IconHome}, rumah: {label: 'Rumah', icon: IconHome},
pohon: {label: 'Pohon', icon: IconTree}, pohon: {label: 'Pohon', icon: IconTree},
air: {label: 'Air', icon: IconDroplet} air: {label: 'Air', icon: IconDroplet},
bantuan: {label: 'Bantuan', icon: IconCash},
pelatihan: {label: 'Pelatihan', icon: IconSchool},
subsidi: {label: 'Subsidi', icon: IconShoppingCart},
layananKesehatan: {label: 'Layanan Kesehatan', icon: IconHospital},
}; };
type IconKey = keyof typeof iconMap; type IconKey = keyof typeof iconMap;

View File

@@ -7,9 +7,13 @@ import { z } from "zod";
const templateApbDesa = z.object({ const templateApbDesa = z.object({
tahun: z.number().min(4, "Tahun minimal 4 karakter"), tahun: z.number().min(4, "Tahun minimal 4 karakter"),
pembiayaanIds: z.array(z.string().uuid()).nonempty("Pilih minimal 1 pembiayaan"), pembiayaanIds: z
.array(z.string().uuid())
.nonempty("Pilih minimal 1 pembiayaan"),
belanjaIds: z.array(z.string().uuid()).nonempty("Pilih minimal 1 belanja"), belanjaIds: z.array(z.string().uuid()).nonempty("Pilih minimal 1 belanja"),
pendapatanIds: z.array(z.string().uuid()).nonempty("Pilih minimal 1 pendapatan"), pendapatanIds: z
.array(z.string().uuid())
.nonempty("Pilih minimal 1 pendapatan"),
}); });
const ApbDesaDefaultForm = { const ApbDesaDefaultForm = {
@@ -55,31 +59,45 @@ const ApbDesa = proxy({
}, },
findMany: { findMany: {
data: null as data: null as
| Prisma.ApbDesaGetPayload<{ | Prisma.ApbDesaGetPayload<{
include: { include: {
pendapatan: true; pendapatan: true;
belanja: true; belanja: true;
pembiayaan: true; pembiayaan: true;
}; };
}>[] }>[]
| null, | null,
page: 1,
totalPages: 1,
loading: false, loading: false,
async load() { search: "",
load: async (page = 1, limit = 10, search = "") => {
ApbDesa.findMany.loading = true; // ✅ Akses langsung via nama path
ApbDesa.findMany.page = page;
ApbDesa.findMany.search = search;
try { try {
this.loading = true; const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.apbdesa[ const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.apbdesa[
"find-many" "find-many"
].get(); ].get({ query });
if (res.status === 200) {
this.data = res.data?.data ?? []; if (res.status === 200 && res.data?.success) {
ApbDesa.findMany.data = res.data.data ?? [];
ApbDesa.findMany.totalPages =
res.data.totalPages ?? 1;
} else { } else {
toast.error(res.data?.message || "Gagal mengambil APB Desa"); ApbDesa.findMany.data = [];
ApbDesa.findMany.totalPages = 1;
} }
} catch (error) { } catch (err) {
console.error("Find many error:", error); console.error("Gagal fetch APB Desa paginated:", err);
toast.error("Gagal mengambil APB Desa"); ApbDesa.findMany.data = [];
ApbDesa.findMany.totalPages = 1;
} finally { } finally {
this.loading = false; ApbDesa.findMany.loading = false;
} }
}, },
}, },
@@ -264,34 +282,32 @@ const pendapatan = proxy({
data: null as any[] | null, data: null as any[] | null,
page: 1, page: 1,
totalPages: 1, totalPages: 1,
total: 0,
loading: false, loading: false,
load: async (page = 1, limit = 10) => { search: "",
// Change to arrow function load: async (page = 1, limit = 10, search = "") => {
pendapatan.findMany.loading = true; // Use the full path to access the property pendapatan.findMany.loading = true; // ✅ Akses langsung via nama path
pendapatan.findMany.page = page; pendapatan.findMany.page = page;
pendapatan.findMany.search = search;
try { try {
const res = const query: any = { page, limit };
await ApiFetch.api.ekonomi.pendapatanaslidesa.pendapatanasli[ if (search) query.search = search;
"find-many"
].get({ const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.pendapatanasli[
query: { page, limit }, "find-many"
}); ].get({ query });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
pendapatan.findMany.data = res.data.data || []; pendapatan.findMany.data = res.data.data ?? [];
pendapatan.findMany.total = res.data.total || 0; pendapatan.findMany.totalPages =
pendapatan.findMany.totalPages = res.data.totalPages || 1; res.data.totalPages ?? 1;
} else { } else {
console.error("Failed to load pendapatan:", res.data?.message);
pendapatan.findMany.data = []; pendapatan.findMany.data = [];
pendapatan.findMany.total = 0;
pendapatan.findMany.totalPages = 1; pendapatan.findMany.totalPages = 1;
} }
} catch (error) { } catch (err) {
console.error("Error loading pendapatan:", error); console.error("Gagal fetch pendapatan asli desa paginated:", err);
pendapatan.findMany.data = []; pendapatan.findMany.data = [];
pendapatan.findMany.total = 0;
pendapatan.findMany.totalPages = 1; pendapatan.findMany.totalPages = 1;
} finally { } finally {
pendapatan.findMany.loading = false; pendapatan.findMany.loading = false;
@@ -308,12 +324,15 @@ const pendapatan = proxy({
return null; return null;
} }
try { try {
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/pendapatanasli/${id}`, { const response = await fetch(
method: "GET", `/api/ekonomi/pendapatanaslidesa/pendapatanasli/${id}`,
headers: { {
"Content-Type": "application/json", method: "GET",
}, headers: {
}); "Content-Type": "application/json",
},
}
);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
@@ -349,16 +368,19 @@ const pendapatan = proxy({
try { try {
pendapatan.update.loading = true; pendapatan.update.loading = true;
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/pendapatanasli/${this.id}`, { const response = await fetch(
method: "PUT", `/api/ekonomi/pendapatanaslidesa/pendapatanasli/${this.id}`,
headers: { {
"Content-Type": "application/json", method: "PUT",
}, headers: {
body: JSON.stringify({ "Content-Type": "application/json",
name: this.form.name, },
value: this.form.value, body: JSON.stringify({
}), name: this.form.name,
}); value: this.form.value,
}),
}
);
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({})); const errorData = await response.json().catch(() => ({}));
throw new Error( throw new Error(
@@ -495,23 +517,37 @@ const belanja = proxy({
name: string; name: string;
value: number; value: number;
}>, }>,
page: 1,
totalPages: 1,
loading: false, loading: false,
async load() { search: "",
load: async (page = 1, limit = 10, search = "") => {
belanja.findMany.loading = true; // ✅ Akses langsung via nama path
belanja.findMany.page = page;
belanja.findMany.search = search;
try { try {
this.loading = true; const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.belanja[ const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.belanja[
"find-many" "find-many"
].get(); ].get({ query });
if (res.status === 200) {
this.data = res.data?.data ?? []; if (res.status === 200 && res.data?.success) {
belanja.findMany.data = res.data.data ?? [];
belanja.findMany.totalPages =
res.data.totalPages ?? 1;
} else { } else {
toast.error(res.data?.message || "Gagal mengambil Belanja"); belanja.findMany.data = [];
belanja.findMany.totalPages = 1;
} }
} catch (error) { } catch (err) {
console.error("Find many error:", error); console.error("Gagal fetch Belanja paginated:", err);
toast.error("Gagal mengambil Belanja"); belanja.findMany.data = [];
belanja.findMany.totalPages = 1;
} finally { } finally {
this.loading = false; belanja.findMany.loading = false;
} }
}, },
}, },
@@ -525,12 +561,15 @@ const belanja = proxy({
return null; return null;
} }
try { try {
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/belanja/${id}`, { const response = await fetch(
method: "GET", `/api/ekonomi/pendapatanaslidesa/belanja/${id}`,
headers: { {
"Content-Type": "application/json", method: "GET",
}, headers: {
}); "Content-Type": "application/json",
},
}
);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
@@ -566,16 +605,19 @@ const belanja = proxy({
try { try {
belanja.update.loading = true; belanja.update.loading = true;
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/belanja/${this.id}`, { const response = await fetch(
method: "PUT", `/api/ekonomi/pendapatanaslidesa/belanja/${this.id}`,
headers: { {
"Content-Type": "application/json", method: "PUT",
}, headers: {
body: JSON.stringify({ "Content-Type": "application/json",
name: this.form.name, },
value: this.form.value, body: JSON.stringify({
}), name: this.form.name,
}); value: this.form.value,
}),
}
);
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({})); const errorData = await response.json().catch(() => ({}));
throw new Error( throw new Error(
@@ -710,23 +752,37 @@ const pembiayaan = proxy({
name: string; name: string;
value: number; value: number;
}>, }>,
page: 1,
totalPages: 1,
loading: false, loading: false,
async load() { search: "",
load: async (page = 1, limit = 10, search = "") => {
pembiayaan.findMany.loading = true; // ✅ Akses langsung via nama path
pembiayaan.findMany.page = page;
pembiayaan.findMany.search = search;
try { try {
this.loading = true; const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.pembiayaan[ const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.pembiayaan[
"find-many" "find-many"
].get(); ].get({ query });
if (res.status === 200) {
this.data = res.data?.data ?? []; if (res.status === 200 && res.data?.success) {
pembiayaan.findMany.data = res.data.data ?? [];
pembiayaan.findMany.totalPages =
res.data.totalPages ?? 1;
} else { } else {
toast.error(res.data?.message || "Gagal mengambil Pembiayaan"); pembiayaan.findMany.data = [];
pembiayaan.findMany.totalPages = 1;
} }
} catch (error) { } catch (err) {
console.error("Find many error:", error); console.error("Gagal fetch Pembiayaan paginated:", err);
toast.error("Gagal mengambil Pembiayaan"); pembiayaan.findMany.data = [];
pembiayaan.findMany.totalPages = 1;
} finally { } finally {
this.loading = false; pembiayaan.findMany.loading = false;
} }
}, },
}, },
@@ -740,12 +796,15 @@ const pembiayaan = proxy({
return null; return null;
} }
try { try {
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/pembiayaan/${id}`, { const response = await fetch(
method: "GET", `/api/ekonomi/pendapatanaslidesa/pembiayaan/${id}`,
headers: { {
"Content-Type": "application/json", method: "GET",
}, headers: {
}); "Content-Type": "application/json",
},
}
);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
@@ -781,16 +840,19 @@ const pembiayaan = proxy({
try { try {
pembiayaan.update.loading = true; pembiayaan.update.loading = true;
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/pembiayaan/${this.id}`, { const response = await fetch(
method: "PUT", `/api/ekonomi/pendapatanaslidesa/pembiayaan/${this.id}`,
headers: { {
"Content-Type": "application/json", method: "PUT",
}, headers: {
body: JSON.stringify({ "Content-Type": "application/json",
name: this.form.name, },
value: this.form.value, body: JSON.stringify({
}), name: this.form.name,
}); value: this.form.value,
}),
}
);
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({})); const errorData = await response.json().catch(() => ({}));
throw new Error( throw new Error(

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; 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";
@@ -71,12 +72,37 @@ const demografiPekerjaan = proxy({
omit: { isActive: true }; omit: { isActive: true };
}>[] }>[]
| null, | null,
async load() { page: 1,
const res = await ApiFetch.api.ekonomi.demografipekerjaan[ totalPages: 1,
"find-many" loading: false,
].get(); search: "",
if (res.status === 200) { load: async (page = 1, limit = 10, search = "") => {
demografiPekerjaan.findMany.data = res.data?.data ?? []; demografiPekerjaan.findMany.loading = true; // ✅ Akses langsung via nama path
demografiPekerjaan.findMany.page = page;
demografiPekerjaan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.demografipekerjaan[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
demografiPekerjaan.findMany.data = res.data.data ?? [];
demografiPekerjaan.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
demografiPekerjaan.findMany.data = [];
demografiPekerjaan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch demografi pekerjaan paginated:", err);
demografiPekerjaan.findMany.data = [];
demografiPekerjaan.findMany.totalPages = 1;
} finally {
demografiPekerjaan.findMany.loading = false;
} }
}, },
}, },
@@ -194,4 +220,4 @@ const demografiPekerjaan = proxy({
}, },
}, },
}); });
export default demografiPekerjaan export default demografiPekerjaan;

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; 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";
@@ -69,16 +70,37 @@ const jumlahPendudukMiskin = proxy({
select: { id: true; year: true; totalPoorPopulation: true }; select: { id: true; year: true; totalPoorPopulation: true };
}>[] }>[]
| null, | null,
loading: false, page: 1,
async load() { totalPages: 1,
const res = await ApiFetch.api.ekonomi.jumlahpendudukmiskin[ loading: false,
"find-many" search: "",
].get(); load: async (page = 1, limit = 10, search = "") => {
if (res.status === 200) { jumlahPendudukMiskin.findMany.loading = true; // ✅ Akses langsung via nama path
jumlahPendudukMiskin.findMany.data = res.data?.data ?? []; jumlahPendudukMiskin.findMany.page = page;
} jumlahPendudukMiskin.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.jumlahpendudukmiskin["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
jumlahPendudukMiskin.findMany.data = res.data.data ?? [];
jumlahPendudukMiskin.findMany.totalPages = res.data.totalPages ?? 1;
} else {
jumlahPendudukMiskin.findMany.data = [];
jumlahPendudukMiskin.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch jumlah penduduk miskin paginated:", err);
jumlahPendudukMiskin.findMany.data = [];
jumlahPendudukMiskin.findMany.totalPages = 1;
} finally {
jumlahPendudukMiskin.findMany.loading = false;
}
},
}, },
},
findUnique: { findUnique: {
data: null as Prisma.GrafikJumlahPendudukMiskinGetPayload<{ data: null as Prisma.GrafikJumlahPendudukMiskinGetPayload<{
select: { id: true; year: true; totalPoorPopulation: true }; select: { id: true; year: true; totalPoorPopulation: true };

View File

@@ -15,7 +15,8 @@ const templateJumlahPengngguran = z.object({
uneducatedUnemployment: z uneducatedUnemployment: z
.number() .number()
.min(1, "Pengangguran tidak pendidikan harus diisi"), .min(1, "Pengangguran tidak pendidikan harus diisi"),
percentageChange: z.number().min(0, "Persentase perubahan harus diisi"), percentageChange: z.number({ invalid_type_error: "Persentase perubahan harus angka" }),
}); });
type JumlahPengangguran = { type JumlahPengangguran = {
@@ -29,7 +30,7 @@ type JumlahPengangguran = {
const jumlahPengangguranForm: JumlahPengangguran = { const jumlahPengangguranForm: JumlahPengangguran = {
month: "", month: "",
year: 0, year: new Date().getFullYear(), // Default to current year
totalUnemployment: 0, totalUnemployment: 0,
educatedUnemployment: 0, educatedUnemployment: 0,
uneducatedUnemployment: 0, uneducatedUnemployment: 0,
@@ -60,13 +61,21 @@ const jumlahPengangguran = proxy({
form: jumlahPengangguranForm, form: jumlahPengangguranForm,
loading: false, loading: false,
async create() { async create() {
const cek = templateJumlahPengngguran.safeParse( // Ensure all number fields are actual numbers
jumlahPengangguran.create.form const formData = {
); ...jumlahPengangguran.create.form,
year: Number(jumlahPengangguran.create.form.year) || new Date().getFullYear(),
totalUnemployment: Number(jumlahPengangguran.create.form.totalUnemployment) || 0,
educatedUnemployment: Number(jumlahPengangguran.create.form.educatedUnemployment) || 0,
uneducatedUnemployment: Number(jumlahPengangguran.create.form.uneducatedUnemployment) || 0,
percentageChange: Number(jumlahPengangguran.create.form.percentageChange) || 0,
};
const cek = templateJumlahPengngguran.safeParse(formData);
if (!cek.success) { if (!cek.success) {
const err = `[${cek.error.issues const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`) .map((v) => `${v.path.join(".")} (${v.message})`)
.join("\n")}] required`; .join("\n")}]`;
toast.error(err); toast.error(err);
return null; return null;
} }
@@ -78,7 +87,7 @@ const jumlahPengangguran = proxy({
].post(jumlahPengangguran.create.form); ].post(jumlahPengangguran.create.form);
if (res.status === 200) { if (res.status === 200) {
const id = res.data?.data?.id; const id = res.data?.id;
if (id) { if (id) {
toast.success("Success create"); toast.success("Success create");
jumlahPengangguran.create.form = { ...jumlahPengangguranForm }; jumlahPengangguran.create.form = { ...jumlahPengangguranForm };
@@ -103,16 +112,40 @@ const jumlahPengangguran = proxy({
omit: { isActive: true }; omit: { isActive: true };
}>[] }>[]
| null, | null,
async load() { page: 1,
const res = totalPages: 1,
await ApiFetch.api.ekonomi.jumlahpengangguran.detaildatapengangguran[ loading: false,
"find-many" search: "",
].get(); load: async (page = 1, limit = 10, search = "") => {
if (res.status === 200) { jumlahPengangguran.findMany.loading = true; // ✅ Akses langsung via nama path
jumlahPengangguran.findMany.data = res.data?.data ?? []; jumlahPengangguran.findMany.page = page;
} jumlahPengangguran.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.jumlahpengangguran.detaildatapengangguran[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
jumlahPengangguran.findMany.data = res.data.data ?? [];
jumlahPengangguran.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
jumlahPengangguran.findMany.data = [];
jumlahPengangguran.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch jumlah pengangguran paginated:", err);
jumlahPengangguran.findMany.data = [];
jumlahPengangguran.findMany.totalPages = 1;
} finally {
jumlahPengangguran.findMany.loading = false;
}
},
}, },
},
findUnique: { findUnique: {
data: null as Prisma.DetailDataPengangguranGetPayload<{ data: null as Prisma.DetailDataPengangguranGetPayload<{

View File

@@ -8,7 +8,7 @@ import { z } from "zod";
const templateForm = z.object({ const templateForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"), nama: z.string().min(1, "Nama minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"), deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
ikonUrl: z.string().optional(), icon: z.string().min(1, "Icon minimal 1 karakter"),
statistik: z.object({ statistik: z.object({
tahun: z.string().min(1, "Tahun minimal 1 karakter"), tahun: z.string().min(1, "Tahun minimal 1 karakter"),
jumlah: z.string().min(1, "Jumlah minimal 1 karakter"), jumlah: z.string().min(1, "Jumlah minimal 1 karakter"),
@@ -18,7 +18,7 @@ const templateForm = z.object({
const defaultForm = { const defaultForm = {
nama: "", nama: "",
deskripsi: "", deskripsi: "",
ikonUrl: "", icon: "",
statistik: { statistik: {
tahun: "", tahun: "",
jumlah: "", jumlah: "",
@@ -148,7 +148,7 @@ const programKemiskinanState = proxy({
this.form = { this.form = {
nama: data.nama, nama: data.nama,
deskripsi: data.deskripsi, deskripsi: data.deskripsi,
ikonUrl: data.ikonUrl || "", icon: data.icon,
statistik: { statistik: {
tahun: data.statistik.tahun, tahun: data.statistik.tahun,
jumlah: data.statistik.jumlah, jumlah: data.statistik.jumlah,
@@ -189,7 +189,7 @@ const programKemiskinanState = proxy({
body: JSON.stringify({ body: JSON.stringify({
nama: this.form.nama, nama: this.form.nama,
deskripsi: this.form.deskripsi, deskripsi: this.form.deskripsi,
ikonUrl: this.form.ikonUrl, icon: this.form.icon,
statistik: { statistik: {
tahun: this.form.statistik.tahun, tahun: this.form.statistik.tahun,
jumlah: this.form.statistik.jumlah, jumlah: this.form.statistik.jumlah,

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; 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";
@@ -76,13 +77,37 @@ const grafikSektorUnggulan = proxy({
}; };
}>[] }>[]
| null, | null,
page: 1,
totalPages: 1,
loading: false, loading: false,
async load() { search: "",
const res = await ApiFetch.api.ekonomi.sektourunggulandesa[ load: async (page = 1, limit = 10, search = "") => {
"find-many" grafikSektorUnggulan.findMany.loading = true; // ✅ Akses langsung via nama path
].get(); grafikSektorUnggulan.findMany.page = page;
if (res.status === 200) { grafikSektorUnggulan.findMany.search = search;
grafikSektorUnggulan.findMany.data = res.data?.data ?? [];
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.sektourunggulandesa[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
grafikSektorUnggulan.findMany.data = res.data.data ?? [];
grafikSektorUnggulan.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
grafikSektorUnggulan.findMany.data = [];
grafikSektorUnggulan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch sektor unggulan desa paginated:", err);
grafikSektorUnggulan.findMany.data = [];
grafikSektorUnggulan.findMany.totalPages = 1;
} finally {
grafikSektorUnggulan.findMany.loading = false;
} }
}, },
}, },

View File

@@ -161,18 +161,34 @@ const posisiOrganisasi = proxy({
deskripsi: string | null; deskripsi: string | null;
hierarki: number; hierarki: number;
}>, }>,
async load() { page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
posisiOrganisasi.findMany.loading = true; // ✅ Akses langsung via nama path
posisiOrganisasi.findMany.page = page;
posisiOrganisasi.findMany.search = search;
try { try {
const res = await ApiFetch.api.ekonomi["struktur-organisasi"][ const query: any = { page, limit };
"posisi-organisasi" if (search) query.search = search;
]["find-many"].get();
if (res.status === 200) { const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["posisi-organisasi"]["find-many"].get({ query });
// The API now returns the id field, so we can use it directly
this.data = res.data?.data ?? []; if (res.status === 200 && res.data?.success) {
posisiOrganisasi.findMany.data = res.data.data ?? [];
posisiOrganisasi.findMany.totalPages = res.data.totalPages ?? 1;
} else {
posisiOrganisasi.findMany.data = [];
posisiOrganisasi.findMany.totalPages = 1;
} }
} catch (error) { } catch (err) {
console.error("Find many error:", error); console.error("Gagal fetch posisi organisasi paginated:", err);
this.data = []; posisiOrganisasi.findMany.data = [];
posisiOrganisasi.findMany.totalPages = 1;
} finally {
posisiOrganisasi.findMany.loading = false;
} }
}, },
}, },

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; 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";
@@ -75,16 +76,37 @@ const grafikBerdasarkanUsiaKerjaNganggur = proxy({
omit: { isActive: true }; omit: { isActive: true };
}>[] }>[]
| null, | null,
loading: false, page: 1,
async load() { totalPages: 1,
const res = await ApiFetch.api.ekonomi.grafikusiakerjayangmenganggur[ loading: false,
"find-many" search: "",
].get(); load: async (page = 1, limit = 10, search = "") => {
if (res.status === 200) { grafikBerdasarkanUsiaKerjaNganggur.findMany.loading = true; // ✅ Akses langsung via nama path
grafikBerdasarkanUsiaKerjaNganggur.findMany.data = res.data?.data ?? []; grafikBerdasarkanUsiaKerjaNganggur.findMany.page = page;
} grafikBerdasarkanUsiaKerjaNganggur.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.grafikusiakerjayangmenganggur["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
grafikBerdasarkanUsiaKerjaNganggur.findMany.data = res.data.data ?? [];
grafikBerdasarkanUsiaKerjaNganggur.findMany.totalPages = res.data.totalPages ?? 1;
} else {
grafikBerdasarkanUsiaKerjaNganggur.findMany.data = [];
grafikBerdasarkanUsiaKerjaNganggur.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch grafik berdasarkan usia kerja yang menganggur paginated:", err);
grafikBerdasarkanUsiaKerjaNganggur.findMany.data = [];
grafikBerdasarkanUsiaKerjaNganggur.findMany.totalPages = 1;
} finally {
grafikBerdasarkanUsiaKerjaNganggur.findMany.loading = false;
}
},
}, },
},
findUnique: { findUnique: {
data: null as Prisma.GrafikMenganggurBerdasarkanUsiaGetPayload<{ data: null as Prisma.GrafikMenganggurBerdasarkanUsiaGetPayload<{
omit: { isActive: true }; omit: { isActive: true };
@@ -259,15 +281,36 @@ const grafikBerdasarkanPendidikan = proxy({
omit: { isActive: true }; omit: { isActive: true };
}>[] }>[]
| null, | null,
loading: false, page: 1,
async load() { totalPages: 1,
const res = await ApiFetch.api.ekonomi.grafikmenganggurberdasarkanpendidikan[ loading: false,
"find-many" search: "",
].get(); load: async (page = 1, limit = 10, search = "") => {
if (res.status === 200) { grafikBerdasarkanPendidikan.findMany.loading = true; // ✅ Akses langsung via nama path
grafikBerdasarkanPendidikan.findMany.data = res.data?.data ?? []; grafikBerdasarkanPendidikan.findMany.page = page;
} grafikBerdasarkanPendidikan.findMany.search = search;
},
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.grafikmenganggurberdasarkanpendidikan["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
grafikBerdasarkanPendidikan.findMany.data = res.data.data ?? [];
grafikBerdasarkanPendidikan.findMany.totalPages = res.data.totalPages ?? 1;
} else {
grafikBerdasarkanPendidikan.findMany.data = [];
grafikBerdasarkanPendidikan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch grafik berdasarkan pendidikan paginated:", err);
grafikBerdasarkanPendidikan.findMany.data = [];
grafikBerdasarkanPendidikan.findMany.totalPages = 1;
} finally {
grafikBerdasarkanPendidikan.findMany.loading = false;
}
},
}, },
findUnique: { findUnique: {
data: null as Prisma.GrafikMenganggurBerdasarkanPendidikanGetPayload<{ data: null as Prisma.GrafikMenganggurBerdasarkanPendidikanGetPayload<{

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; 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";
@@ -7,25 +8,13 @@ import { z } from "zod";
const templateForm = z.object({ const templateForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"), nama: z.string().min(1, "Nama minimal 1 karakter"),
imageId: z.string().nonempty(), imageId: z.string().nonempty(),
kontakItems: z.array( kategoriId: z.array(z.string()).min(1, "Minimal pilih satu kategori"),
z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
nomorTelepon: z.string().min(1, "Nomor Telepon minimal 1 karakter"),
imageId: z.string().nonempty(),
})
),
}); });
const defaultForm = { const defaultForm = {
nama: "", nama: "",
imageId: "", imageId: "",
kontakItems: [ kategoriId: [] as string[],
{
nama: "",
nomorTelepon: "",
imageId: "",
},
],
}; };
const kontakDaruratKeamananState = proxy({ const kontakDaruratKeamananState = proxy({
@@ -61,20 +50,50 @@ const kontakDaruratKeamananState = proxy({
}, },
}, },
findMany: { findMany: {
data: null as data: null as Array<
| Prisma.KontakDaruratKeamananGetPayload<{ Prisma.KontakDaruratKeamananGetPayload<{
include: { include: {
kontakItems: true; kategori: true;
image: true; image: true;
kontakItems: {
include: {
kontakItem: true;
};
}; };
}>[] };
| null, }>
async load() { > | null,
const res = await ApiFetch.api.keamanan.kontakdaruratkeamanan[ page: 1,
"find-many" totalPages: 1,
].get(); loading: false,
if (res.status === 200) { search: "",
kontakDaruratKeamananState.findMany.data = res.data?.data ?? []; load: async (page = 1, limit = 10, search = "") => {
kontakDaruratKeamananState.findMany.loading = true; // ✅ Akses langsung via nama path
kontakDaruratKeamananState.findMany.page = page;
kontakDaruratKeamananState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.keamanan.kontakdaruratkeamanan[
"findMany"
].get({ query });
if (res.status === 200 && res.data?.success) {
kontakDaruratKeamananState.findMany.data = res.data.data ?? [];
kontakDaruratKeamananState.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
kontakDaruratKeamananState.findMany.data = [];
kontakDaruratKeamananState.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch kontak darurat paginated:", err);
kontakDaruratKeamananState.findMany.data = [];
kontakDaruratKeamananState.findMany.totalPages = 1;
} finally {
kontakDaruratKeamananState.findMany.loading = false;
} }
}, },
}, },
@@ -83,10 +102,15 @@ const kontakDaruratKeamananState = proxy({
include: { include: {
kontakItems: { kontakItems: {
include: { include: {
image: true; kontakItem: {
include: {
image: true;
}
};
}; };
}; };
image: true; image: true;
kategori: true;
}; };
}> | null, }> | null,
loading: false, loading: false,
@@ -168,14 +192,8 @@ const kontakDaruratKeamananState = proxy({
this.id = data.id; this.id = data.id;
this.form = { this.form = {
nama: data.nama, nama: data.nama,
imageId: data.imageId, imageId: data.imageId || '',
kontakItems: [ kategoriId: data.kontakItems?.map((item: any) => item.kontakItemId) || []
{
nama: data.kontakItems.nama,
nomorTelepon: data.kontakItems.nomorTelepon,
imageId: data.kontakItems.imageId,
},
],
}; };
return data; return data;
} else { } else {
@@ -213,13 +231,7 @@ const kontakDaruratKeamananState = proxy({
body: JSON.stringify({ body: JSON.stringify({
nama: this.form.nama, nama: this.form.nama,
imageId: this.form.imageId, imageId: this.form.imageId,
kontakItems: [ kategoriId: this.form.kategoriId,
{
nama: this.form.kontakItems[0].nama,
nomorTelepon: this.form.kontakItems[0].nomorTelepon,
imageId: this.form.kontakItems[0].imageId,
},
],
}), }),
} }
); );
@@ -256,4 +268,257 @@ const kontakDaruratKeamananState = proxy({
}, },
}); });
export default kontakDaruratKeamananState; const templateFormItem = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
nomorTelepon: z.string().min(1, "Nomor Telepon minimal 1 karakter"),
imageId: z.string().nonempty(),
});
const defaultFormItem = {
nama: "",
nomorTelepon: "",
imageId: "",
};
const kontakDaruratItem = proxy({
create: {
form: { ...defaultFormItem },
loading: false,
async create() {
const cek = templateFormItem.safeParse(
kontakDaruratItem.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kontakDaruratItem.create.loading = true;
const res = await ApiFetch.api.keamanan.kontakitem[
"create"
].post(kontakDaruratItem.create.form);
if (res.status === 200) {
kontakDaruratItem.findMany.load();
return toast.success("success create");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
kontakDaruratItem.create.loading = false;
}
},
},
findMany: {
data: null as Array<
Prisma.KontakItemGetPayload<{
include: {
image: true;
};
}>
> | null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
kontakDaruratItem.findMany.loading = true; // ✅ Akses langsung via nama path
kontakDaruratItem.findMany.page = page;
kontakDaruratItem.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.keamanan.kontakitem[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
kontakDaruratItem.findMany.data = res.data.data ?? [];
kontakDaruratItem.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
kontakDaruratItem.findMany.data = [];
kontakDaruratItem.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch kontak darurat paginated:", err);
kontakDaruratItem.findMany.data = [];
kontakDaruratItem.findMany.totalPages = 1;
} finally {
kontakDaruratItem.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.KontakItemGetPayload<{
include: {
kategori: true;
image: true;
};
}> | null,
loading: false,
async load(id: string) {
try {
const res = await fetch(`/api/keamanan/kontakitem/${id}`);
if (res.ok) {
const data = await res.json();
kontakDaruratItem.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
kontakDaruratItem.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
kontakDaruratItem.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
kontakDaruratItem.delete.loading = true;
const response = await fetch(
`/api/keamanan/kontakitem/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Kontak item berhasil dihapus");
await kontakDaruratItem.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus kontak item");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus kontak item");
} finally {
kontakDaruratItem.delete.loading = false;
}
},
},
update: {
id: "",
form: { ...defaultFormItem },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/keamanan/kontakitem/${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,
nomorTelepon: data.nomorTelepon,
imageId: data.imageId,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading kontak darurat:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateFormItem.safeParse(
kontakDaruratItem.update.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kontakDaruratItem.update.loading = true;
const response = await fetch(
`/api/keamanan/kontakitem/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: this.form.nama,
nomorTelepon: this.form.nomorTelepon,
imageId: this.form.imageId,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update kontak item");
await kontakDaruratItem.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate kontak item");
}
} catch (error) {
console.error("Error updating kontak item:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate kontak item"
);
return false;
} finally {
kontakDaruratItem.update.loading = false;
}
},
reset() {
kontakDaruratItem.update.id = "";
kontakDaruratItem.update.form = { ...defaultFormItem };
},
},
})
const kontakDarurat = proxy({
kontakDaruratKeamananState,
kontakDaruratItem,
});
export default kontakDarurat;

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; 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";
@@ -97,13 +98,37 @@ const laporanPublikState = proxy({
include: { penanganan: true }; include: { penanganan: true };
}>[] }>[]
| null, | null,
async load() { page: 1,
const res = await ApiFetch.api.keamanan.laporanpublik["find-many"].get(); totalPages: 1,
if (res.status === 200) { loading: false,
laporanPublikState.findMany.data = res.data?.data ?? []; search: "",
} load: async (page = 1, limit = 10, search = "") => {
laporanPublikState.findMany.loading = true; // ✅ Akses langsung via nama path
laporanPublikState.findMany.page = page;
laporanPublikState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.keamanan.laporanpublik["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
laporanPublikState.findMany.data = res.data.data ?? [];
laporanPublikState.findMany.totalPages = res.data.totalPages ?? 1;
} else {
laporanPublikState.findMany.data = [];
laporanPublikState.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch laporan publik paginated:", err);
laporanPublikState.findMany.data = [];
laporanPublikState.findMany.totalPages = 1;
} finally {
laporanPublikState.findMany.loading = false;
}
},
}, },
},
findUnique: { findUnique: {
data: null as Prisma.LaporanPublikGetPayload<{ data: null as Prisma.LaporanPublikGetPayload<{
include: { penanganan: true }; include: { penanganan: true };

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; 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";
@@ -88,12 +89,37 @@ const pencegahanKriminalitasState = proxy({
}; };
}>[] }>[]
| null, | null,
async load() { page: 1,
const res = await ApiFetch.api.keamanan.pencegahankriminalitas[ totalPages: 1,
"find-many" loading: false,
].get(); search: "",
if (res.status === 200) { load: async (page = 1, limit = 10, search = "") => {
pencegahanKriminalitasState.findMany.data = res.data?.data ?? []; pencegahanKriminalitasState.findMany.loading = true; // ✅ Akses langsung via nama path
pencegahanKriminalitasState.findMany.page = page;
pencegahanKriminalitasState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.keamanan.pencegahankriminalitas[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
pencegahanKriminalitasState.findMany.data = res.data.data ?? [];
pencegahanKriminalitasState.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
pencegahanKriminalitasState.findMany.data = [];
pencegahanKriminalitasState.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch pencegahan kriminalitas paginated:", err);
pencegahanKriminalitasState.findMany.data = [];
pencegahanKriminalitasState.findMany.totalPages = 1;
} finally {
pencegahanKriminalitasState.findMany.loading = false;
} }
}, },
}, },

View File

@@ -23,7 +23,12 @@ type ProgramInovasiForm = Prisma.ProgramInovasiGetPayload<{
const programInovasi = proxy({ const programInovasi = proxy({
create: { create: {
form: {} as ProgramInovasiForm, form: {
name: "",
description: "",
imageId: "",
link: ""
} as ProgramInovasiForm,
loading: false, loading: false,
async create() { async create() {
// Ensure all required fields are non-null // Ensure all required fields are non-null

View File

@@ -94,7 +94,7 @@ const sdgsDesa = proxy({
}, },
}, },
findUnique: { findUnique: {
data: null as Prisma.SDGSDesaGetPayload<{ data: null as Prisma.SdgsDesaGetPayload<{
include: { include: {
image: true; image: true;
}; };

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { IconFileText, IconBuildingStore, IconSparkles, IconUsers } from '@tabler/icons-react'; import { IconFileText, IconBuildingStore, IconSparkles, IconUsers } from '@tabler/icons-react';
@@ -62,43 +62,50 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
<Stack gap="lg"> <Stack gap="lg">
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Layanan</Title> <Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Layanan</Title>
<Tabs <Tabs
color={colors['blue-button']} color={colors["blue-button"]}
variant='pills' variant="pills"
value={activeTab} value={activeTab}
onChange={handleTabChange} onChange={handleTabChange}
radius="lg" radius="lg"
keepMounted={false} keepMounted={false}
> >
<TabsList {/* ✅ Scroll horizontal wrapper */}
p="sm" <ScrollArea type="auto" offsetScrollbars>
style={{ <TabsList
background: "linear-gradient(135deg, #e7ebf7, #f9faff)", p="sm"
borderRadius: "1rem", style={{
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
}} borderRadius: "1rem",
> boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
{tabs.map((tab, i) => ( display: "flex",
<Tooltip flexWrap: "nowrap",
key={i} gap: "0.5rem",
label={tab.tooltip} paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
position="bottom" }}
withArrow >
transitionProps={{ transition: 'pop', duration: 200 }} {tabs.map((tab, i) => (
> <Tooltip
<TabsTab key={i}
value={tab.value} label={tab.tooltip}
leftSection={tab.icon} position="bottom"
style={{ withArrow
fontWeight: 600, transitionProps={{ transition: 'pop', duration: 200 }}
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
> >
{tab.label} <TabsTab
</TabsTab> value={tab.value}
</Tooltip> leftSection={tab.icon}
))} style={{
</TabsList> fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => ( {tabs.map((tab, i) => (
<TabsPanel <TabsPanel

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { IconNews, IconCategory } from '@tabler/icons-react'; import { IconNews, IconCategory } from '@tabler/icons-react';
@@ -49,43 +49,50 @@ function LayoutTabsBerita({ children }: { children: React.ReactNode }) {
<Stack gap="lg"> <Stack gap="lg">
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Berita Desa</Title> <Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Berita Desa</Title>
<Tabs <Tabs
color={colors['blue-button']} color={colors["blue-button"]}
variant="pills" variant="pills"
value={activeTab} value={activeTab}
onChange={handleTabChange} onChange={handleTabChange}
radius="lg" radius="lg"
keepMounted={false} keepMounted={false}
> >
<TabsList {/* ✅ Scroll horizontal wrapper */}
p="sm" <ScrollArea type="auto" offsetScrollbars>
style={{ <TabsList
background: "linear-gradient(135deg, #e7ebf7, #f9faff)", p="sm"
borderRadius: "1rem", style={{
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
}} borderRadius: "1rem",
> boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
{tabs.map((tab, i) => ( display: "flex",
<Tooltip flexWrap: "nowrap",
key={i} gap: "0.5rem",
label={tab.tooltip} paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
position="bottom" }}
withArrow >
transitionProps={{ transition: 'pop', duration: 200 }} {tabs.map((tab, i) => (
> <Tooltip
<TabsTab key={i}
value={tab.value} label={tab.tooltip}
leftSection={tab.icon} position="bottom"
style={{ withArrow
fontWeight: 600, transitionProps={{ transition: 'pop', duration: 200 }}
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
> >
{tab.label} <TabsTab
</TabsTab> value={tab.value}
</Tooltip> leftSection={tab.icon}
))} style={{
</TabsList> fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => ( {tabs.map((tab, i) => (
<TabsPanel <TabsPanel

View File

@@ -96,9 +96,11 @@ function ListBerita({ search }: { search: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd style={{ width: '30%' }}> <TableTd style={{ width: '30%' }}>
<Text fw={500} truncate="end" lineClamp={1}> <Box w={200}>
{item.judul} <Text fw={500} truncate="end" lineClamp={1}>
</Text> {item.judul}
</Text>
</Box>
</TableTd> </TableTd>
<TableTd style={{ width: '20%' }}> <TableTd style={{ width: '20%' }}>
<Text fz="sm" c="dimmed"> <Text fz="sm" c="dimmed">

View File

@@ -110,7 +110,7 @@ export default function ListImage() {
radius="md" radius="md"
onClick={() => { onClick={() => {
stateFileStorage stateFileStorage
.del({ name: v.name }) .del({ id: v.id })
.finally(() => toast("Foto berhasil dihapus")); .finally(() => toast("Foto berhasil dihapus"));
}} }}
> >
@@ -143,14 +143,15 @@ export default function ListImage() {
<Flex justify="center"> <Flex justify="center">
<Pagination <Pagination
total={total} total={total}
value={stateFileStorage.page} // Changed from page to value
size="md" size="md"
radius="md" radius="md"
withEdges withEdges
onChange={(page) => { onChange={(page) => {
stateFileStorage.page = page; stateFileStorage.load({ page });
stateFileStorage.load();
}} }}
/> />
</Flex> </Flex>
)} )}
</Stack> </Stack>

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { IconPhoto, IconVideo } from '@tabler/icons-react'; import { IconPhoto, IconVideo } from '@tabler/icons-react';
@@ -48,43 +48,50 @@ function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
<Stack gap="lg"> <Stack gap="lg">
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Gallery</Title> <Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Gallery</Title>
<Tabs <Tabs
color={colors['blue-button']} color={colors["blue-button"]}
variant='pills' variant="pills"
value={activeTab} value={activeTab}
onChange={handleTabChange} onChange={handleTabChange}
radius="lg" radius="lg"
keepMounted={false} keepMounted={false}
> >
<TabsList {/* ✅ Scroll horizontal wrapper */}
p="sm" <ScrollArea type="auto" offsetScrollbars>
style={{ <TabsList
background: "linear-gradient(135deg, #e7ebf7, #f9faff)", p="sm"
borderRadius: "1rem", style={{
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
}} borderRadius: "1rem",
> boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
{tabs.map((tab, i) => ( display: "flex",
<Tooltip flexWrap: "nowrap",
key={i} gap: "0.5rem",
label={tab.tooltip} paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
position="bottom" }}
withArrow >
transitionProps={{ transition: 'pop', duration: 200 }} {tabs.map((tab, i) => (
> <Tooltip
<TabsTab key={i}
value={tab.value} label={tab.tooltip}
leftSection={tab.icon} position="bottom"
style={{ withArrow
fontWeight: 600, transitionProps={{ transition: 'pop', duration: 200 }}
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
> >
{tab.label} <TabsTab
</TabsTab> value={tab.value}
</Tooltip> leftSection={tab.icon}
))} style={{
</TabsList> fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => ( {tabs.map((tab, i) => (
<TabsPanel <TabsPanel

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { IconListDetails, IconCategory } from '@tabler/icons-react'; import { IconListDetails, IconCategory } from '@tabler/icons-react';
@@ -48,37 +48,44 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
<Stack gap="lg"> <Stack gap="lg">
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Pengumuman</Title> <Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Pengumuman</Title>
<Tabs <Tabs
color={colors['blue-button']} color={colors["blue-button"]}
variant='pills' variant="pills"
value={activeTab} value={activeTab}
onChange={handleTabChange} onChange={handleTabChange}
radius="lg" radius="lg"
keepMounted={false} keepMounted={false}
> >
<TabsList {/* ✅ Scroll horizontal wrapper */}
p="sm" <ScrollArea type="auto" offsetScrollbars>
style={{ <TabsList
background: "linear-gradient(135deg, #e7ebf7, #f9faff)", p="sm"
borderRadius: "1rem", style={{
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
}} borderRadius: "1rem",
> boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
{tabs.map((tab, i) => ( display: "flex",
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}> flexWrap: "nowrap",
<TabsTab gap: "0.5rem",
value={tab.value} paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
leftSection={tab.icon} }}
style={{ >
fontWeight: 600, {tabs.map((tab, i) => (
fontSize: "0.9rem", <Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
transition: "all 0.2s ease", <TabsTab
}} value={tab.value}
> leftSection={tab.icon}
{tab.label} style={{
</TabsTab> fontWeight: 600,
</Tooltip> fontSize: "0.9rem",
))} transition: "all 0.2s ease",
</TabsList> }}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => ( {tabs.map((tab, i) => (
<TabsPanel <TabsPanel

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { IconCategory, IconListCheck } from '@tabler/icons-react'; import { IconCategory, IconListCheck } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
@@ -48,37 +48,44 @@ function LayoutTabsPotensi({ children }: { children: React.ReactNode }) {
<Stack gap="lg"> <Stack gap="lg">
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Potensi</Title> <Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Potensi</Title>
<Tabs <Tabs
color={colors['blue-button']} color={colors["blue-button"]}
variant='pills' variant="pills"
value={activeTab} value={activeTab}
onChange={handleTabChange} onChange={handleTabChange}
radius="lg" radius="lg"
keepMounted={false} keepMounted={false}
> >
<TabsList {/* ✅ Scroll horizontal wrapper */}
p="sm" <ScrollArea type="auto" offsetScrollbars>
style={{ <TabsList
background: "linear-gradient(135deg, #e7ebf7, #f9faff)", p="sm"
borderRadius: "1rem", style={{
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
}} borderRadius: "1rem",
> boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
{tabs.map((tab, i) => ( display: "flex",
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}> flexWrap: "nowrap",
<TabsTab gap: "0.5rem",
value={tab.value} paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
leftSection={tab.icon} }}
style={{ >
fontWeight: 600, {tabs.map((tab, i) => (
fontSize: "0.9rem", <Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
transition: "all 0.2s ease", <TabsTab
}} value={tab.value}
> leftSection={tab.icon}
{tab.label} style={{
</TabsTab> fontWeight: 600,
</Tooltip> fontSize: "0.9rem",
))} transition: "all 0.2s ease",
</TabsList> }}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => ( {tabs.map((tab, i) => (
<TabsPanel <TabsPanel

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { IconUser, IconUsers, IconCalendar } from '@tabler/icons-react'; import { IconUser, IconUsers, IconCalendar } from '@tabler/icons-react';
@@ -55,56 +55,63 @@ function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
<Stack gap="lg"> <Stack gap="lg">
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Profile Desa</Title> <Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Profile Desa</Title>
<Tabs <Tabs
color={colors['blue-button']} color={colors["blue-button"]}
variant='pills' variant="pills"
value={activeTab} value={activeTab}
onChange={handleTabChange} onChange={handleTabChange}
radius="lg" radius="lg"
keepMounted={false} keepMounted={false}
> >
<TabsList {/* ✅ Scroll horizontal wrapper */}
p="sm" <ScrollArea type="auto" offsetScrollbars>
style={{ <TabsList
background: "linear-gradient(135deg, #e7ebf7, #f9faff)", p="sm"
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
}}
>
{tabs.map((tab, i) => (
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
{tabs.map((tab, i) => (
<TabsPanel
key={i}
value={tab.value}
style={{ style={{
padding: "1.5rem", background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem", borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)", boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}} }}
> >
{/* Konten dummy, bisa diganti sesuai routing */} {tabs.map((tab, i) => (
<>{children}</> <Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
</TabsPanel> <TabsTab
))} value={tab.value}
</Tabs> leftSection={tab.icon}
</Stack> style={{
); fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel
key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{/* Konten dummy, bisa diganti sesuai routing */}
<>{children}</>
</TabsPanel>
))}
</Tabs>
</Stack>
);
} }
export default LayoutTabsDetail; export default LayoutTabsDetail;

View File

@@ -1,71 +1,148 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; import {
Stack,
Tabs,
TabsList,
TabsPanel,
TabsTab,
Title,
Tooltip,
ScrollArea,
} from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import {
IconFileAnalytics,
IconCoins,
IconShoppingCart,
IconWallet,
} from '@tabler/icons-react';
function LayoutTabs({ children }: { children: React.ReactNode }) { function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter();
const pathname = usePathname() const pathname = usePathname();
const tabs = [ const tabs = [
{ {
label: "APB Desa", label: "APB Desa",
value: "apbdesa", value: "apbdesa",
href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa" href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa",
icon: <IconFileAnalytics size={18} stroke={1.8} />,
tooltip: "Lihat ringkasan Anggaran Pendapatan dan Belanja Desa",
}, },
{ {
label: "Pendapatan", label: "Pendapatan",
value: "pendapatan", value: "pendapatan",
href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan" href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan",
icon: <IconCoins size={18} stroke={1.8} />,
tooltip: "Kelola data pendapatan desa",
}, },
{ {
label: "Belanja", label: "Belanja",
value: "belanja", value: "belanja",
href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja" href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja",
icon: <IconShoppingCart size={18} stroke={1.8} />,
tooltip: "Atur data belanja desa",
}, },
{ {
label: "Pembiayaan", label: "Pembiayaan",
value: "pembiayaan", value: "pembiayaan",
href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan" href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan",
icon: <IconWallet size={18} stroke={1.8} />,
tooltip: "Kelola data pembiayaan desa",
}, },
]; ];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value); const currentTab = tabs.find((tab) => tab.href === pathname);
const [activeTab, setActiveTab] = useState<string | null>(
currentTab?.value || tabs[0].value
);
const handleTabChange = (value: string | null) => { const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value) const tab = tabs.find((t) => t.value === value);
if (tab) { if (tab) {
router.push(tab.href) router.push(tab.href);
} }
setActiveTab(value) setActiveTab(value);
} };
useEffect(() => { useEffect(() => {
const match = tabs.find(tab => tab.href === pathname) const match = tabs.find((tab) => tab.href === pathname);
if (match) { if (match) {
setActiveTab(match.value) setActiveTab(match.value);
} }
}, [pathname]) }, [pathname]);
return ( return (
<Stack> <Stack gap="lg">
<Title order={3}>Pendapatan Asli Desa</Title> <Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> Pendapatan Asli Desa
<TabsList p={"xs"} bg={"#BBC8E7FF"}> </Title>
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> <Tabs
))} color={colors['blue-button']}
</TabsList> variant="pills"
{tabs.map((e, i) => ( value={activeTab}
<TabsPanel key={i} value={e.value}> onChange={handleTabChange}
{/* Konten dummy, bisa diganti tergantung routing */} radius="lg"
<></> keepMounted={false}
>
{/* ✅ Scroll horizontal wrapper */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem",
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: "pop", duration: 200 }}
>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
flexShrink: 0,
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel
key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{children}
</TabsPanel> </TabsPanel>
))} ))}
</Tabs> </Tabs>
{children}
</Stack> </Stack>
); );
} }

View File

@@ -3,7 +3,19 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'; import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, MultiSelect, Paper, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
MultiSelect,
Paper,
Skeleton,
Stack,
Text,
TextInput,
Title,
Tooltip,
Group,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -23,7 +35,7 @@ function EditAPBDesa() {
pembiayaanIds: apbState.update.form.pembiayaanIds || [], pembiayaanIds: apbState.update.form.pembiayaanIds || [],
}); });
// Load APB desa by id saat pertama kali // Load APB desa by id
useEffect(() => { useEffect(() => {
const loadAPBdesa = async () => { const loadAPBdesa = async () => {
const id = params?.id as string; const id = params?.id as string;
@@ -46,12 +58,10 @@ function EditAPBDesa() {
}; };
loadAPBdesa(); loadAPBdesa();
}, [params?.id]); // ✅ hapus beritaState dari dependency }, [params?.id]);
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
// Update global state with form data
apbState.update.form = { apbState.update.form = {
...apbState.update.form, ...apbState.update.form,
tahun: Number(formData.tahun), tahun: Number(formData.tahun),
@@ -70,65 +80,95 @@ function EditAPBDesa() {
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack color={colors["blue-button"]} size={30} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button
</Box> variant="subtle"
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}> onClick={() => router.back()}
<Stack gap={"xs"}> p="xs"
<Title order={3}>Edit APB Desa</Title> radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit APB Desa
</Title>
</Group>
{/* Form Card */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Tahun */}
<TextInput <TextInput
type='number' type="number"
value={formData.tahun} value={formData.tahun}
onChange={(val) => { onChange={(e) =>
setFormData({ ...formData, tahun: val.target.value }); setFormData({ ...formData, tahun: e.target.value })
}} }
label={<Text fz={"sm"} fw={"bold"}>Tahun</Text>} label={<Text fz="sm" fw="bold">Tahun</Text>}
placeholder="masukkan tahun" placeholder="Masukkan tahun anggaran"
required
/> />
{/* Selects */}
<SelectPendapatan <SelectPendapatan
selectedIds={formData.pendapatanIds} selectedIds={formData.pendapatanIds}
onSelectionChange={(ids) => { onSelectionChange={(ids) =>
setFormData({ ...formData, pendapatanIds: ids }); setFormData({ ...formData, pendapatanIds: ids })
}} }
/> />
<SelectBelanja <SelectBelanja
selectedIds={formData.belanjaIds} selectedIds={formData.belanjaIds}
onSelectionChange={(ids) => { onSelectionChange={(ids) =>
setFormData({ ...formData, belanjaIds: ids }); setFormData({ ...formData, belanjaIds: ids })
}} }
/> />
<SelectPembiayaan <SelectPembiayaan
selectedIds={formData.pembiayaanIds} selectedIds={formData.pembiayaanIds}
onSelectionChange={(ids) => { onSelectionChange={(ids) =>
setFormData({ ...formData, pembiayaanIds: ids }); setFormData({ ...formData, pembiayaanIds: ids })
}} }
/> />
<Button onClick={handleSubmit}>Simpan</Button>
{/* Save Button */}
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>
); );
/* --- Sub Components --- */
/* Select Pendapatan */ function SelectPendapatan({ selectedIds, onSelectionChange }: { selectedIds: string[]; onSelectionChange: (ids: string[]) => void }) {
interface SelectPendapatanProps {
selectedIds: string[];
onSelectionChange: (ids: string[]) => void;
}
function SelectPendapatan({
selectedIds = [],
onSelectionChange,
}: SelectPendapatanProps) {
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan); const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
useShallowEffect(() => { useShallowEffect(() => {
pendapatanState.findMany.load().then(() => { pendapatanState.findMany.load();
console.log("Pendapatan berhasil dimuat:", pendapatanState.findMany.data);
});
}, []); }, []);
if (!pendapatanState.findMany.data) { if (!pendapatanState.findMany.data) {
@@ -137,10 +177,10 @@ function EditAPBDesa() {
return ( return (
<MultiSelect <MultiSelect
label={<Text fz={"sm"} fw={"bold"}>Pendapatan</Text>} label={<Text fz="sm" fw="bold">Pendapatan</Text>}
data={pendapatanState.findMany.data.map(p => ({ data={pendapatanState.findMany.data.map((p: any) => ({
value: p.id, value: p.id,
label: p.name label: p.name,
}))} }))}
value={selectedIds} value={selectedIds}
onChange={onSelectionChange} onChange={onSelectionChange}
@@ -152,22 +192,11 @@ function EditAPBDesa() {
); );
} }
/* Select Belanja */ function SelectBelanja({ selectedIds, onSelectionChange }: { selectedIds: string[]; onSelectionChange: (ids: string[]) => void }) {
interface SelectBelanjaProps {
selectedIds: string[];
onSelectionChange: (ids: string[]) => void;
}
function SelectBelanja({
selectedIds = [],
onSelectionChange,
}: SelectBelanjaProps) {
const belanjaState = useProxy(PendapatanAsliDesa.belanja); const belanjaState = useProxy(PendapatanAsliDesa.belanja);
useShallowEffect(() => { useShallowEffect(() => {
belanjaState.findMany.load().then(() => { belanjaState.findMany.load();
console.log("Belanja berhasil dimuat:", belanjaState.findMany.data);
});
}, []); }, []);
if (!belanjaState.findMany.data) { if (!belanjaState.findMany.data) {
@@ -176,10 +205,10 @@ function EditAPBDesa() {
return ( return (
<MultiSelect <MultiSelect
label={<Text fz={"sm"} fw={"bold"}>Belanja</Text>} label={<Text fz="sm" fw="bold">Belanja</Text>}
data={belanjaState.findMany.data.map(b => ({ data={belanjaState.findMany.data.map((b: any) => ({
value: b.id, value: b.id,
label: b.name label: b.name,
}))} }))}
value={selectedIds} value={selectedIds}
onChange={onSelectionChange} onChange={onSelectionChange}
@@ -191,22 +220,11 @@ function EditAPBDesa() {
); );
} }
/* Select Pembiayaan */ function SelectPembiayaan({ selectedIds, onSelectionChange }: { selectedIds: string[]; onSelectionChange: (ids: string[]) => void }) {
interface SelectPembiayaanProps {
selectedIds: string[];
onSelectionChange: (ids: string[]) => void;
}
function SelectPembiayaan({
selectedIds = [],
onSelectionChange,
}: SelectPembiayaanProps) {
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan); const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
useShallowEffect(() => { useShallowEffect(() => {
pembiayaanState.findMany.load().then(() => { pembiayaanState.findMany.load();
console.log("Pembiayaan berhasil dimuat:", pembiayaanState.findMany.data);
});
}, []); }, []);
if (!pembiayaanState.findMany.data) { if (!pembiayaanState.findMany.data) {
@@ -215,10 +233,10 @@ function EditAPBDesa() {
return ( return (
<MultiSelect <MultiSelect
label={<Text fz={"sm"} fw={"bold"}>Pembiayaan</Text>} label={<Text fz="sm" fw="bold">Pembiayaan</Text>}
data={pembiayaanState.findMany.data.map(b => ({ data={pembiayaanState.findMany.data.map((p: any) => ({
value: b.id, value: p.id,
label: b.name label: p.name,
}))} }))}
value={selectedIds} value={selectedIds}
onChange={onSelectionChange} onChange={onSelectionChange}

View File

@@ -2,148 +2,204 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'; import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Skeleton,
Stack,
Text,
Tooltip
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function DetailAPBDesa() { function DetailAPBDesa() {
const apbState = useProxy(PendapatanAsliDesa.ApbDesa) const apbState = useProxy(PendapatanAsliDesa.ApbDesa);
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams() const params = useParams();
const router = useRouter() const router = useRouter();
useShallowEffect(() => { useShallowEffect(() => {
console.log("PARAM ID:", params?.id) apbState.findUnique.load(params?.id as string);
apbState.findUnique.load(params?.id as string) }, []);
}, [])
const formatRupiah = (value: number) => { const formatRupiah = (value: number) =>
return new Intl.NumberFormat('id-ID', { new Intl.NumberFormat('id-ID', {
style: 'currency', style: 'currency',
currency: 'IDR', currency: 'IDR',
minimumFractionDigits: 0, minimumFractionDigits: 0,
}).format(value); }).format(value);
};
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
apbState.delete.byId(selectedId) apbState.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa") router.push(
'/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa'
);
} }
} };
if (!apbState.findUnique.data) { if (!apbState.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={40} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = apbState.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> <Button
<Button variant="subtle" onClick={() => router.back()}> variant="subtle"
<IconArrowBack color={colors['blue-button']} size={25} /> onClick={() => router.back()}
</Button> leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
</Box> mb={15}
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}> >
<Stack> Kembali
<Text fz={"xl"} fw={"bold"}>Detail APB Desa</Text> </Button>
{apbState.findUnique.data ? (
<Paper key={apbState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}> <Paper
<Stack gap={"xs"}> withBorder
<Box> w={{ base: '100%', md: '70%', lg: '50%' }}
<Text fw={"bold"} fz={"lg"}>Tahun</Text> bg={colors['white-1']}
<Text fz={"lg"}>{apbState.findUnique.data?.tahun}</Text> p="lg"
</Box> radius="md"
<Box> shadow="sm"
<Stack gap={"xs"}> >
<Text fw={"bold"} fz={"lg"}>Detail Pembiayaan</Text> <Stack gap="md">
{(apbState.findUnique.data?.pembiayaan || []).map((item) => ( <Text fz="2xl" fw="bold" c={colors['blue-button']}>
<Text fz={"lg"} key={item.id}> Detail APB Desa
{item.name}: {formatRupiah(Number(item.value))} </Text>
</Text>
))} <Paper bg={colors['BG-trans']} p="md" radius="md" shadow="xs">
<Text fz={"lg"} fw={"bold"}> <Stack gap="sm">
Total: {formatRupiah((apbState.findUnique.data?.pembiayaan || []) <Box>
.reduce((sum, item) => sum + Number(item.value), 0))} <Text fz="lg" fw="bold">
Tahun
</Text>
<Text fz="md" c="dimmed">
{data.tahun}
</Text>
</Box>
<Box>
<Stack gap="xs">
<Text fw="bold" fz="lg">
Detail Pembiayaan
</Text>
{(data?.pembiayaan || []).map((item) => (
<Text fz="md" c="dimmed" key={item.id}>
{item.name}: {formatRupiah(Number(item.value))}
</Text> </Text>
</Stack> ))}
</Box> <Text fz="md" fw="bold">
<Box> Total:{' '}
<Stack gap={"xs"}> {formatRupiah(
<Text fw={"bold"} fz={"lg"}>Detail Belanja</Text> (data?.pembiayaan || []).reduce(
{(apbState.findUnique.data?.belanja || []).map((item) => ( (sum, item) => sum + Number(item.value),
<Text fz={"lg"} key={item.id}> 0
{item.name}: {formatRupiah(Number(item.value))} )
</Text> )}
))} </Text>
<Text fz={"lg"} fw={"bold"}> </Stack>
Total: {formatRupiah((apbState.findUnique.data?.belanja || []) </Box>
.reduce((sum, item) => sum + Number(item.value), 0))}
<Box>
<Stack gap="xs">
<Text fw="bold" fz="lg">
Detail Belanja
</Text>
{(data?.belanja || []).map((item) => (
<Text fz="md" c="dimmed" key={item.id}>
{item.name}: {formatRupiah(Number(item.value))}
</Text> </Text>
</Stack> ))}
</Box> <Text fz="md" fw="bold">
<Box> Total:{' '}
<Stack gap={"xs"}> {formatRupiah(
<Text fw={"bold"} fz={"lg"}>Detail Pendapatan</Text> (data?.belanja || []).reduce(
{(apbState.findUnique.data?.pendapatan || []).map((item) => ( (sum, item) => sum + Number(item.value),
<Text fz={"lg"} key={item.id}> 0
{item.name}: {formatRupiah(Number(item.value))} )
</Text> )}
))} </Text>
<Text fz={"lg"} fw={"bold"}> </Stack>
Total: {formatRupiah((apbState.findUnique.data?.pendapatan || []) </Box>
.reduce((sum, item) => sum + Number(item.value), 0))}
<Box>
<Stack gap="xs">
<Text fw="bold" fz="lg">
Detail Pendapatan
</Text>
{(data?.pendapatan || []).map((item) => (
<Text fz="md" c="dimmed" key={item.id}>
{item.name}: {formatRupiah(Number(item.value))}
</Text> </Text>
</Stack> ))}
</Box> <Text fz="md" fw="bold">
<Flex gap={"xs"} mt={10}> Total:{' '}
{formatRupiah(
(data?.pendapatan || []).reduce(
(sum, item) => sum + Number(item.value),
0
)
)}
</Text>
</Stack>
</Box>
<Group gap="sm" mt={10}>
<Tooltip label="Hapus APB Desa" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (apbState.findUnique.data) { setSelectedId(data.id);
setSelectedId(apbState.findUnique.data.id); setModalHapus(true);
setModalHapus(true);
}
}} }}
disabled={apbState.delete.loading || !apbState.findUnique.data} variant="light"
color={"red"} radius="md"
size="md"
> >
<IconX size={20} /> <IconTrash size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit APB Desa" withArrow position="top">
<Button <Button
onClick={() => { color="green"
if (apbState.findUnique.data) { onClick={() =>
router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/${apbState.findUnique.data.id}/edit`); router.push(
} `/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/${data.id}/edit`
}} )
disabled={!apbState.findUnique.data} }
color={"green"} variant="light"
radius="md"
size="md"
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Stack> </Group>
</Paper> </Stack>
) : null} </Paper>
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus APB Desa ini?' text="Apakah Anda yakin ingin menghapus APB Desa ini?"
/> />
</Box> </Box>
); );

View File

@@ -1,15 +1,28 @@
'use client' 'use client';
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'; import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, MultiSelect, Paper, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
MultiSelect,
Paper,
Skeleton,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreateAPBDesa() { function CreateAPBDesa() {
const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa) const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa);
const router = useRouter() const router = useRouter();
const resetForm = () => { const resetForm = () => {
apbDesaState.create.form = { apbDesaState.create.form = {
@@ -17,74 +30,101 @@ function CreateAPBDesa() {
pendapatanIds: [], pendapatanIds: [],
belanjaIds: [], belanjaIds: [],
pembiayaanIds: [], pembiayaanIds: [],
} };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
await apbDesaState.create.submit() await apbDesaState.create.submit();
resetForm() resetForm();
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa") router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa');
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Box> <IconArrowBack color={colors['blue-button']} size={24} />
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}> </Button>
<Stack gap={"xs"}> </Tooltip>
<Title order={3}>Create APB Desa</Title> <Title order={4} ml="sm" c="dark">
Tambah APB Desa
</Title>
</Group>
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
type='number' type="number"
value={apbDesaState.create.form.tahun} value={apbDesaState.create.form.tahun}
onChange={(val) => { onChange={(val) => {
apbDesaState.create.form.tahun = Number(val.target.value); apbDesaState.create.form.tahun = Number(val.target.value);
}} }}
label={<Text fz={"sm"} fw={"bold"}>Tahun</Text>} label={<Text fz="sm" fw="bold">Tahun</Text>}
placeholder="masukkan tahun" placeholder="Masukkan tahun anggaran"
required
/> />
<SelectPendapatan <SelectPendapatan
selectedIds={apbDesaState.create.form.pendapatanIds} selectedIds={apbDesaState.create.form.pendapatanIds}
onSelectionChange={(ids) => { onSelectionChange={(ids) => {
apbDesaState.create.form.pendapatanIds = ids; apbDesaState.create.form.pendapatanIds = ids;
}} }}
/> />
<SelectBelanja <SelectBelanja
selectedIds={apbDesaState.create.form.belanjaIds} selectedIds={apbDesaState.create.form.belanjaIds}
onSelectionChange={(ids) => { onSelectionChange={(ids) => {
apbDesaState.create.form.belanjaIds = ids; apbDesaState.create.form.belanjaIds = ids;
}} }}
/> />
<SelectPembiayaan <SelectPembiayaan
selectedIds={apbDesaState.create.form.pembiayaanIds} selectedIds={apbDesaState.create.form.pembiayaanIds}
onSelectionChange={(ids) => { onSelectionChange={(ids) => {
apbDesaState.create.form.pembiayaanIds = ids; apbDesaState.create.form.pembiayaanIds = ids;
}} }}
/> />
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
{/* Action */}
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>
); );
/* Select Pendapatan */ /* ---------- Select Pendapatan ---------- */
interface SelectPendapatanProps { interface SelectPendapatanProps {
selectedIds: string[]; selectedIds: string[];
onSelectionChange: (ids: string[]) => void; onSelectionChange: (ids: string[]) => void;
} }
function SelectPendapatan({ selectedIds = [], onSelectionChange }: SelectPendapatanProps) {
function SelectPendapatan({
selectedIds = [],
onSelectionChange,
}: SelectPendapatanProps) {
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan); const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
useShallowEffect(() => { useShallowEffect(() => {
pendapatanState.findMany.load().then(() => { pendapatanState.findMany.load();
console.log("Pendapatan berhasil dimuat:", pendapatanState.findMany.data);
});
}, []); }, []);
if (!pendapatanState.findMany.data) { if (!pendapatanState.findMany.data) {
@@ -93,10 +133,10 @@ function CreateAPBDesa() {
return ( return (
<MultiSelect <MultiSelect
label={<Text fz={"sm"} fw={"bold"}>Pendapatan</Text>} label={<Text fz="sm" fw="bold">Pendapatan</Text>}
data={pendapatanState.findMany.data.map(p => ({ data={pendapatanState.findMany.data.map((p) => ({
value: p.id, value: p.id,
label: p.name label: p.name,
}))} }))}
value={selectedIds} value={selectedIds}
onChange={onSelectionChange} onChange={onSelectionChange}
@@ -108,22 +148,16 @@ function CreateAPBDesa() {
); );
} }
/* Select Belanja */ /* ---------- Select Belanja ---------- */
interface SelectBelanjaProps { interface SelectBelanjaProps {
selectedIds: string[]; selectedIds: string[];
onSelectionChange: (ids: string[]) => void; onSelectionChange: (ids: string[]) => void;
} }
function SelectBelanja({ selectedIds = [], onSelectionChange }: SelectBelanjaProps) {
function SelectBelanja({
selectedIds = [],
onSelectionChange,
}: SelectBelanjaProps) {
const belanjaState = useProxy(PendapatanAsliDesa.belanja); const belanjaState = useProxy(PendapatanAsliDesa.belanja);
useShallowEffect(() => { useShallowEffect(() => {
belanjaState.findMany.load().then(() => { belanjaState.findMany.load();
console.log("Belanja berhasil dimuat:", belanjaState.findMany.data);
});
}, []); }, []);
if (!belanjaState.findMany.data) { if (!belanjaState.findMany.data) {
@@ -132,10 +166,10 @@ function CreateAPBDesa() {
return ( return (
<MultiSelect <MultiSelect
label={<Text fz={"sm"} fw={"bold"}>Belanja</Text>} label={<Text fz="sm" fw="bold">Belanja</Text>}
data={belanjaState.findMany.data.map(b => ({ data={belanjaState.findMany.data.map((b) => ({
value: b.id, value: b.id,
label: b.name label: b.name,
}))} }))}
value={selectedIds} value={selectedIds}
onChange={onSelectionChange} onChange={onSelectionChange}
@@ -147,22 +181,16 @@ function CreateAPBDesa() {
); );
} }
/* Select Pembiayaan */ /* ---------- Select Pembiayaan ---------- */
interface SelectPembiayaanProps { interface SelectPembiayaanProps {
selectedIds: string[]; selectedIds: string[];
onSelectionChange: (ids: string[]) => void; onSelectionChange: (ids: string[]) => void;
} }
function SelectPembiayaan({ selectedIds = [], onSelectionChange }: SelectPembiayaanProps) {
function SelectPembiayaan({
selectedIds = [],
onSelectionChange,
}: SelectPembiayaanProps) {
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan); const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
useShallowEffect(() => { useShallowEffect(() => {
pembiayaanState.findMany.load().then(() => { pembiayaanState.findMany.load();
console.log("Pembiayaan berhasil dimuat:", pembiayaanState.findMany.data);
});
}, []); }, []);
if (!pembiayaanState.findMany.data) { if (!pembiayaanState.findMany.data) {
@@ -171,10 +199,10 @@ function CreateAPBDesa() {
return ( return (
<MultiSelect <MultiSelect
label={<Text fz={"sm"} fw={"bold"}>Pembiayaan</Text>} label={<Text fz="sm" fw="bold">Pembiayaan</Text>}
data={pembiayaanState.findMany.data.map(b => ({ data={pembiayaanState.findMany.data.map((b) => ({
value: b.id, value: b.id,
label: b.name label: b.name,
}))} }))}
value={selectedIds} value={selectedIds}
onChange={onSelectionChange} onChange={onSelectionChange}

View File

@@ -1,23 +1,38 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core'; import {
Box,
Button,
Center,
Group,
Paper,
Pagination,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa'; import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
function APBDesa() { function APBDesa() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='APB Desa' title="APB Desa"
placeholder='pencarian' placeholder="Cari tahun atau nominal..."
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -28,77 +43,147 @@ function APBDesa() {
} }
function ListAPBDesa({ search }: { search: string }) { function ListAPBDesa({ search }: { search: string }) {
const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa) const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa);
const router = useRouter(); const router = useRouter();
const formatRupiah = (value: number) => { const {
return new Intl.NumberFormat('id-ID', { data,
style: 'currency', page,
currency: 'IDR', totalPages,
loading,
load,
} = apbDesaState.findMany;
const formatRupiah = (value: number) =>
new Intl.NumberFormat("id-ID", {
style: "currency",
currency: "IDR",
minimumFractionDigits: 0, minimumFractionDigits: 0,
}).format(value); }).format(value);
};
useShallowEffect(() => { useShallowEffect(() => {
apbDesaState.findMany.load(); load(page, 10, search);
}, []) }, [page, search]);
const filteredData = (apbDesaState.findMany.data || []).filter(item => { const filteredData = data || [];
const keyword = search.toLowerCase();
return (
item.tahun.toString().toLowerCase().includes(keyword) ||
item.pembiayaan.map((item) => item.value.toString()).includes(keyword) ||
item.belanja.map((item) => item.value.toString()).includes(keyword) ||
item.pendapatan.map((item) => item.value.toString()).includes(keyword)
);
});
if (!apbDesaState.findMany.data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors["white-1"]} p="lg" shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List APB Desa' <Text fw={600} fz="lg">
href='/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create' List APB Desa
/> </Text>
<Table striped withTableBorder withRowBorders> <Tooltip label="Tambah APB Desa" withArrow>
<TableThead> <Button
<TableTr> leftSection={<IconPlus size={18} />}
<TableTh>Tahun</TableTh> color="blue"
<TableTh>Pembiayaan</TableTh> variant="light"
<TableTh>Belanja</TableTh> onClick={() =>
<TableTh>Pendapatan</TableTh> router.push(
<TableTh>Detail</TableTh> "/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create"
</TableTr> )
</TableThead> }
<TableTbody> >
{filteredData.map((item) => ( Tambah Baru
<TableTr key={item.id}> </Button>
<TableTd>{item.tahun}</TableTd> </Tooltip>
<TableTd>{formatRupiah(item.pembiayaan.reduce((sum, item) => sum + Number(item.value), 0))}</TableTd> </Group>
<TableTd>{formatRupiah(item.belanja.reduce((sum, item) => sum + Number(item.value), 0))}</TableTd> <Box style={{ overflowX: "auto" }}>
<TableTd>{formatRupiah(item.pendapatan.reduce((sum, item) => sum + Number(item.value), 0))}</TableTd> <Table highlightOnHover>
<TableTd> <TableThead>
<Button <TableTr>
bg={"green"} <TableTh style={{ width: "15%" }}>Tahun</TableTh>
onClick={() => <TableTh style={{ width: "25%" }}>Pembiayaan</TableTh>
router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/${item.id}`) <TableTh style={{ width: "25%" }}>Belanja</TableTh>
} <TableTh style={{ width: "25%" }}>Pendapatan</TableTh>
> <TableTh style={{ width: "10%" }}>Aksi</TableTh>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr> </TableTr>
))} </TableThead>
</TableTbody> <TableTbody>
</Table> {filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.tahun}</TableTd>
<TableTd>
{formatRupiah(
item.pembiayaan.reduce(
(sum, val) => sum + Number(val.value),
0
)
)}
</TableTd>
<TableTd>
{formatRupiah(
item.belanja.reduce(
(sum, val) => sum + Number(val.value),
0
)
)}
</TableTd>
<TableTd>
{formatRupiah(
item.pendapatan.reduce(
(sum, val) => sum + Number(val.value),
0
)
)}
</TableTd>
<TableTd>
<Tooltip label="Lihat Detail" withArrow>
<Button
variant="light"
color="green"
onClick={() =>
router.push(
`/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/${item.id}`
)
}
>
<IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button>
</Tooltip>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={5}>
<Center py={20}>
<Text color="dimmed">
Tidak ada data APB Desa yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: "smooth" });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box> </Box>
); );
} }

View File

@@ -2,7 +2,16 @@
'use client' 'use client'
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'; import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
TextInput,
Title,
Tooltip
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -72,40 +81,71 @@ function EditBelanja() {
} }
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button
</Box> variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Jenis Belanja
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> {/* Card */}
<Stack gap={"xs"}> <Paper
<Title order={4}>Edit Jenis Pendapatan</Title> w={{ base: '100%', md: '50%' }}
<TextInput bg={colors['white-1']}
value={formData.name} p="lg"
onChange={(val) => { radius="md"
setFormData({ ...formData, name: val.target.value }); shadow="sm"
}} style={{ border: '1px solid #e0e0e0' }}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Pendapatan</Text>} >
placeholder='Masukkan nama Jenis Pendapatan' <Stack gap="md">
/> <TextInput
<TextInput label="Nama Jenis Belanja"
label={<Text fw={"bold"} fz={"sm"}>Nilai</Text>} placeholder="Masukkan nama jenis belanja"
placeholder='Masukkan nilai' value={formData.name}
value={formatRupiah(formData.value)} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
onChange={(val) => { required
const raw = val.currentTarget.value; />
const cleanValue = unformatRupiah(raw);
setFormData({ ...formData, value: cleanValue }); <TextInput
}} label="Nilai"
/> placeholder="Masukkan nilai"
<Group> value={formatRupiah(formData.value)}
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> onChange={(e) => {
</Group> const raw = e.currentTarget.value;
</Stack> const cleanValue = unformatRupiah(raw);
</Paper> setFormData({ ...formData, value: cleanValue });
</Box> }}
required
/>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
); );
} }

View File

@@ -1,17 +1,30 @@
'use client' 'use client';
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'; import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { toast } from 'react-toastify';
function CreateBelanja() { function CreateBelanja() {
const belanjaState = useProxy(PendapatanAsliDesa.belanja) const belanjaState = useProxy(PendapatanAsliDesa.belanja);
const router = useRouter() const router = useRouter();
const formatRupiah = (value: number | string) => { const formatRupiah = (value: number | string) => {
const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, '')); const number =
typeof value === 'number' ? value : Number(value.replace(/\D/g, ''));
return new Intl.NumberFormat('id-ID', { return new Intl.NumberFormat('id-ID', {
style: 'currency', style: 'currency',
currency: 'IDR', currency: 'IDR',
@@ -25,48 +38,83 @@ function CreateBelanja() {
const resetForm = () => { const resetForm = () => {
belanjaState.create.form = { belanjaState.create.form = {
name: "", name: '',
value: 0, value: 0,
} };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
await belanjaState.create.submit(); if (!belanjaState.create.form.name || !belanjaState.create.form.value) {
resetForm() return toast.warn('Lengkapi semua field terlebih dahulu');
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja") }
}
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'}> await belanjaState.create.submit();
<Stack gap={"xs"}> resetForm();
<Title order={4}>Create Jenis Belanja</Title> router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja');
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan back button */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Jenis Belanja
</Title>
</Group>
{/* Card Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label={<Text fw="bold" fz="sm">Nama Jenis Belanja</Text>}
placeholder="Masukkan nama jenis belanja"
value={belanjaState.create.form.name} value={belanjaState.create.form.name}
onChange={(val) => { onChange={(e) => (belanjaState.create.form.name = e.target.value)}
belanjaState.create.form.name = val.target.value; required
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Belanja</Text>}
placeholder='Masukkan nama jenis belanja'
/> />
<TextInput <TextInput
type='text' type="text"
label={<Text fw="bold" fz="sm">Nilai</Text>}
placeholder="Masukkan nilai belanja"
value={formatRupiah(belanjaState.create.form.value)} value={formatRupiah(belanjaState.create.form.value)}
onChange={(val) => { onChange={(e) => {
const raw = val.currentTarget.value; const raw = e.currentTarget.value;
const cleanValue = unformatRupiah(raw); belanjaState.create.form.value = unformatRupiah(raw);
belanjaState.create.form.value = cleanValue;
}} }}
label={<Text fw={"bold"} fz={"sm"}>Nilai</Text>} required
placeholder='Masukkan nilai'
/> />
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> <Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,24 +1,41 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa'; import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
function Belanja() { function Belanja() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Belanja' title="Belanja"
placeholder='pencarian' placeholder="Cari belanja berdasarkan nama atau nilai..."
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -29,108 +46,175 @@ function Belanja() {
} }
function ListBelanja({ search }: { search: string }) { function ListBelanja({ search }: { search: string }) {
const belanjaState = useProxy(PendapatanAsliDesa.belanja) const belanjaState = useProxy(PendapatanAsliDesa.belanja);
const router = useRouter(); const router = useRouter();
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const formatRupiah = (value: number) => { const {
return new Intl.NumberFormat('id-ID', { data,
loading,
load,
page,
totalPages,
} = belanjaState.findMany;
const formatRupiah = (value: number) =>
new Intl.NumberFormat('id-ID', {
style: 'currency', style: 'currency',
currency: 'IDR', currency: 'IDR',
minimumFractionDigits: 0, minimumFractionDigits: 0,
}).format(value); }).format(value);
};
const totalBelanja = belanjaState.findMany.data.reduce((sum, item) => sum + item.value, 0); const totalBelanja = data?.reduce((sum, item) => sum + item.value, 0) || 0;
const handleDelete = () => { const handleDelete = () => {
if (selectedId) { if (selectedId) {
belanjaState.delete.byId(selectedId) belanjaState.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
belanjaState.findMany.load() load(page, 10, search);
} }
} };
useShallowEffect(() => { useShallowEffect(() => {
belanjaState.findMany.load(); load(page, 10, search);
}, []) }, [page, search]);
const filteredData = (belanjaState.findMany.data || []).filter(item => { const filteredData = data || [];
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.value.toString().toLowerCase().includes(keyword)
);
});
if (!belanjaState.findMany.data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
) );
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Belanja' <Title order={4}>Daftar Belanja</Title>
href='/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja/create' <Tooltip label="Tambah Belanja" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
<TableThead> color="blue"
<TableTr> variant="light"
<TableTh>Nama</TableTh> onClick={() =>
<TableTh>Nilai</TableTh> router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja/create')
<TableTh>Persentase</TableTh> }
<TableTh>Edit</TableTh> >
<TableTh>Delete</TableTh> Tambah Baru
</TableTr> </Button>
</TableThead> </Tooltip>
<TableTbody> </Group>
{filteredData.map((item) => (
<TableTr key={item.id}> <Box style={{ overflowX: 'auto' }}>
<TableTd>{item.name}</TableTd> <Table highlightOnHover striped withTableBorder withRowBorders>
<TableTd>{formatRupiah(item.value)}</TableTd> <TableThead>
<TableTd>{((item.value / totalBelanja) * 100).toFixed(0)}%</TableTd> <TableTr>
<TableTd> <TableTh>Nama</TableTh>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja/${item.id}`)}> <TableTh>Nilai</TableTh>
<IconEdit size={20} /> <TableTh>Persentase</TableTh>
</Button> <TableTh>Aksi</TableTh>
</TableTd>
<TableTd>
<Button
color='red'
disabled={belanjaState.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr> </TableTr>
))} </TableThead>
<TableTr> <TableTbody>
<TableTd colSpan={4}> {filteredData.length > 0 ? (
<Text fw={'bold'}>Total</Text> <>
</TableTd> {filteredData.map((item) => (
<TableTd> <TableTr key={item.id}>
{formatRupiah(belanjaState.findMany.data.reduce((total, item) => total + item.value, 0))} <TableTd>
</TableTd> <Text fw={500} truncate="end" lineClamp={1}>
</TableTr> {item.name}
</TableTbody> </Text>
</Table> </TableTd>
<TableTd>{formatRupiah(item.value)}</TableTd>
<TableTd>
{totalBelanja > 0
? ((item.value / totalBelanja) * 100).toFixed(0) + '%'
: '0%'}
</TableTd>
<TableTd>
<Group gap="xs">
<Tooltip label="Edit" withArrow>
<Button
size="xs"
variant="light"
color="green"
onClick={() =>
router.push(
`/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja/${item.id}`
)
}
>
<IconEdit size={16} />
</Button>
</Tooltip>
<Tooltip label="Hapus" withArrow>
<Button
size="xs"
variant="light"
color="red"
disabled={belanjaState.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={16} />
</Button>
</Tooltip>
</Group>
</TableTd>
</TableTr>
))}
<TableTr>
<TableTd colSpan={2}>
<Text fw="bold">Total</Text>
</TableTd>
<TableTd colSpan={2}>
<Text fw="bold">{formatRupiah(totalBelanja)}</Text>
</TableTd>
</TableTr>
</>
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text c="dimmed">Tidak ada data belanja yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
{/* Pagination */}
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
{/* Modal Konfirmasi Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleDelete} onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus belanja ini?' text="Apakah anda yakin ingin menghapus belanja ini?"
/> />
</Box> </Box>
); );

View File

@@ -2,7 +2,16 @@
'use client' 'use client'
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'; import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
TextInput,
Title,
Tooltip
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -20,7 +29,7 @@ function EditPembiayaan() {
}); });
const formatRupiah = (value: number | string) => { const formatRupiah = (value: number | string) => {
const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, '')); const number = typeof value === 'number' ? value : Number(value.toString().replace(/\D/g, ''));
return new Intl.NumberFormat('id-ID', { return new Intl.NumberFormat('id-ID', {
style: 'currency', style: 'currency',
currency: 'IDR', currency: 'IDR',
@@ -46,8 +55,8 @@ function EditPembiayaan() {
}); });
} }
} catch (error) { } catch (error) {
console.error("Error loading pembiayaan:", error); console.error('Error loading pembiayaan:', error);
toast.error("Gagal memuat data pembiayaan"); toast.error('Gagal memuat data pembiayaan');
} }
}; };
@@ -60,52 +69,83 @@ function EditPembiayaan() {
...pembiayaanState.update.form, ...pembiayaanState.update.form,
name: formData.name, name: formData.name,
value: Number(formData.value), value: Number(formData.value),
} };
await pembiayaanState.update.update(); await pembiayaanState.update.update();
toast.success("Jenis Pembiayaan berhasil diperbarui!"); toast.success('Jenis Pembiayaan berhasil diperbarui!');
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan"); router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan');
} catch (error) { } catch (error) {
console.error("Error updating jenis pembiayaan:", error); console.error('Error updating jenis pembiayaan:', error);
toast.error("Terjadi kesalahan saat memperbarui jenis pembiayaan"); toast.error('Terjadi kesalahan saat memperbarui jenis pembiayaan');
} }
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header dengan Back Button */}
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button
</Box> variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Jenis Pembiayaan
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> {/* Card Form */}
<Stack gap={"xs"}> <Paper
<Title order={4}>Edit Jenis Pembiayaan</Title> w={{ base: '100%', md: '50%' }}
<TextInput bg={colors['white-1']}
value={formData.name} p="lg"
onChange={(val) => { radius="md"
setFormData({ ...formData, name: val.target.value }); shadow="sm"
}} style={{ border: '1px solid #e0e0e0' }}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Pembiayaan</Text>} >
placeholder='Masukkan nama Jenis Pembiayaan' <Stack gap="md">
/> <TextInput
<TextInput label="Nama Jenis Pembiayaan"
label={<Text fw={"bold"} fz={"sm"}>Nilai</Text>} placeholder="Masukkan nama jenis pembiayaan"
placeholder='Masukkan nilai' value={formData.name}
value={formatRupiah(formData.value)} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
onChange={(val) => { required
const raw = val.currentTarget.value; />
const cleanValue = unformatRupiah(raw);
setFormData({ ...formData, value: cleanValue }); <TextInput
}} label="Nilai"
/> placeholder="Masukkan nilai"
<Group> value={formatRupiah(formData.value)}
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> onChange={(e) => {
</Group> const raw = e.currentTarget.value;
</Stack> const cleanValue = unformatRupiah(raw);
</Paper> setFormData({ ...formData, value: cleanValue });
</Box> }}
required
/>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
); );
} }

View File

@@ -1,18 +1,30 @@
'use client' 'use client';
import React from 'react'; import React from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'; import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput, Group, Text } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
Title,
TextInput,
Text,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { toast } from 'react-toastify';
function CreatePembiayaan() { function CreatePembiayaan() {
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan) const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
const router = useRouter() const router = useRouter();
const formatRupiah = (value: number | string) => { const formatRupiah = (value: number | string) => {
const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, '')); const number =
typeof value === 'number' ? value : Number(value.replace(/\D/g, ''));
return new Intl.NumberFormat('id-ID', { return new Intl.NumberFormat('id-ID', {
style: 'currency', style: 'currency',
currency: 'IDR', currency: 'IDR',
@@ -26,48 +38,85 @@ function CreatePembiayaan() {
const resetForm = () => { const resetForm = () => {
pembiayaanState.create.form = { pembiayaanState.create.form = {
name: "", name: '',
value: 0, value: 0,
} };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
await pembiayaanState.create.submit(); if (!pembiayaanState.create.form.name || !pembiayaanState.create.form.value) {
resetForm() return toast.warn('Nama dan nilai wajib diisi');
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan") }
}
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'}> await pembiayaanState.create.submit();
<Stack gap={"xs"}> resetForm();
<Title order={4}>Create Jenis Pembiayaan</Title> router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan');
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Jenis Pembiayaan
</Title>
</Group>
{/* Form Card */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label={<Text fw="bold" fz="sm">Nama Jenis Pembiayaan</Text>}
placeholder="Masukkan nama jenis pembiayaan"
value={pembiayaanState.create.form.name} value={pembiayaanState.create.form.name}
onChange={(val) => { onChange={(e) => {
pembiayaanState.create.form.name = val.target.value; pembiayaanState.create.form.name = e.currentTarget.value;
}} }}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Pembiayaan</Text>} required
placeholder='Masukkan nama jenis pembiayaan'
/> />
<TextInput <TextInput
type='text' type="text"
label={<Text fw="bold" fz="sm">Nilai</Text>}
placeholder="Masukkan nilai"
value={formatRupiah(pembiayaanState.create.form.value)} value={formatRupiah(pembiayaanState.create.form.value)}
onChange={(val) => { onChange={(e) => {
const raw = val.currentTarget.value; const raw = e.currentTarget.value;
const cleanValue = unformatRupiah(raw); pembiayaanState.create.form.value = unformatRupiah(raw);
pembiayaanState.create.form.value = cleanValue;
}} }}
label={<Text fw={"bold"} fz={"sm"}>Nilai</Text>} required
placeholder='Masukkan nilai'
/> />
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> <Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,14 +1,31 @@
'use client' 'use client'
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import {
Box,
Button,
Center,
Group,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
Pagination,
} from '@mantine/core';
import React, { useState } from 'react'; import React, { useState } from 'react';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa'; import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import colors from '@/con/colors'; import colors from '@/con/colors';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function Pembiayaan() { function Pembiayaan() {
@@ -16,8 +33,8 @@ function Pembiayaan() {
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Pembiayaan' title="Pembiayaan"
placeholder='pencarian' placeholder="Cari nama pembiayaan..."
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -28,111 +45,171 @@ function Pembiayaan() {
} }
function ListPembiayaan({ search }: { search: string }) { function ListPembiayaan({ search }: { search: string }) {
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan) const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
const router = useRouter(); const router = useRouter();
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const formatRupiah = (value: number) => { const {
return new Intl.NumberFormat('id-ID', { data,
page,
totalPages,
loading,
load,
} = pembiayaanState.findMany;
const formatRupiah = (value: number) =>
new Intl.NumberFormat('id-ID', {
style: 'currency', style: 'currency',
currency: 'IDR', currency: 'IDR',
minimumFractionDigits: 0, minimumFractionDigits: 0,
}).format(value); }).format(value);
};
const totalPembiayaan = pembiayaanState.findMany.data.reduce((sum, item) => sum + item.value, 0); const totalPembiayaan = (data || []).reduce((sum, item) => sum + item.value, 0);
const handleDelete = () => { const handleDelete = () => {
if (selectedId) { if (selectedId) {
pembiayaanState.delete.byId(selectedId) pembiayaanState.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
pembiayaanState.findMany.load() load(page, 10, search);
} }
} };
useShallowEffect(() => { useShallowEffect(() => {
pembiayaanState.findMany.load(); load(page, 10, search);
}, []) }, [page, search]);
const filteredData = (pembiayaanState.findMany.data || []).filter(item => { if (loading || !data) {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.value.toString().toLowerCase().includes(keyword)
);
});
if (!pembiayaanState.findMany.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const filteredData = data || [];
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Pembiayaan' <Title order={4}>Daftar Pembiayaan</Title>
href='/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create' <Tooltip label="Tambah Pembiayaan" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
<TableThead> color="blue"
<TableTr> variant="light"
<TableTh>Nama</TableTh> onClick={() =>
<TableTh>Nilai</TableTh> router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create')
<TableTh>Persentase</TableTh> }
<TableTh>Edit</TableTh> >
<TableTh>Delete</TableTh> Tambah Baru
</TableTr> </Button>
</TableThead> </Tooltip>
<TableTbody> </Group>
{filteredData.map((item) => (
<TableTr key={item.id}> <Box style={{ overflowX: 'auto' }}>
<TableTd>{item.name}</TableTd> <Table highlightOnHover striped withTableBorder withRowBorders>
<TableTd>{formatRupiah(item.value)}</TableTd> <TableThead>
<TableTd>{((item.value / totalPembiayaan) * 100).toFixed(0)}%</TableTd> <TableTr>
<TableTd> <TableTh>Nama</TableTh>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/${item.id}`)}> <TableTh>Nilai</TableTh>
<IconEdit size={20} /> <TableTh>Persentase</TableTh>
</Button> <TableTh style={{ width: '20%' }}>Aksi</TableTh>
</TableTd>
<TableTd>
<Button
color='red'
disabled={pembiayaanState.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr> </TableTr>
))} </TableThead>
<TableTr> <TableTbody>
<TableTd colSpan={4}> {filteredData.length > 0 ? (
<Text fw={'bold'}>Total</Text> <>
</TableTd> {filteredData.map((item) => (
<TableTd> <TableTr key={item.id}>
{formatRupiah(pembiayaanState.findMany.data.reduce((total, item) => total + item.value, 0))} <TableTd>
</TableTd> <Text fw={500} truncate="end" lineClamp={1}>
</TableTr> {item.name}
</TableTbody> </Text>
</Table> </TableTd>
<TableTd>{formatRupiah(item.value)}</TableTd>
<TableTd>
{totalPembiayaan > 0
? ((item.value / totalPembiayaan) * 100).toFixed(0) + '%'
: '0%'}
</TableTd>
<TableTd>
<Group gap="xs">
<Button
color="green"
variant="light"
onClick={() =>
router.push(
`/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/${item.id}`
)
}
>
<IconEdit size={18} />
</Button>
<Button
color="red"
variant="light"
disabled={pembiayaanState.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={18} />
</Button>
</Group>
</TableTd>
</TableTr>
))}
{/* Total Row */}
<TableTr>
<TableTd colSpan={2}>
<Text fw="bold">Total</Text>
</TableTd>
<TableTd colSpan={2}>{formatRupiah(totalPembiayaan)}</TableTd>
</TableTr>
</>
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text c="dimmed">Tidak ada data pembiayaan yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
{/* Pagination */}
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
{/* Modal Konfirmasi Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleDelete} onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus pembiayaan ini?' text="Apakah anda yakin ingin menghapus pembiayaan ini?"
/> />
</Box> </Box>
) );
} }
export default Pembiayaan; export default Pembiayaan;

View File

@@ -2,7 +2,16 @@
'use client' 'use client'
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'; import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
TextInput,
Title,
Tooltip
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -20,7 +29,7 @@ function EditPendapatan() {
}); });
const formatRupiah = (value: number | string) => { const formatRupiah = (value: number | string) => {
const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, '')); const number = typeof value === 'number' ? value : Number(value.toString().replace(/\D/g, ''));
return new Intl.NumberFormat('id-ID', { return new Intl.NumberFormat('id-ID', {
style: 'currency', style: 'currency',
currency: 'IDR', currency: 'IDR',
@@ -28,15 +37,13 @@ function EditPendapatan() {
}).format(number); }).format(number);
}; };
const unformatRupiah = (value: string) => { const unformatRupiah = (value: string) => Number(value.replace(/\D/g, ''));
return Number(value.replace(/\D/g, ''));
};
useEffect(() => { useEffect(() => {
const loadPendapatan = async () => { const id = params?.id as string;
const id = params?.id as string; if (!id) return;
if (!id) return;
const loadPendapatan = async () => {
try { try {
const data = await pendapatanState.update.load(id); const data = await pendapatanState.update.load(id);
if (data) { if (data) {
@@ -46,8 +53,8 @@ function EditPendapatan() {
}); });
} }
} catch (error) { } catch (error) {
console.error("Error loading pendapatan:", error); console.error('Error loading pendapatan:', error);
toast.error("Gagal memuat data pendapatan"); toast.error('Gagal memuat data pendapatan');
} }
}; };
@@ -60,52 +67,83 @@ function EditPendapatan() {
...pendapatanState.update.form, ...pendapatanState.update.form,
name: formData.name, name: formData.name,
value: Number(formData.value), value: Number(formData.value),
} };
await pendapatanState.update.update(); await pendapatanState.update.update();
toast.success("Jenis Pendapatan berhasil diperbarui!"); toast.success('Jenis Pendapatan berhasil diperbarui!');
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan"); router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan');
} catch (error) { } catch (error) {
console.error("Error updating jenis pendapatan:", error); console.error('Error updating jenis pendapatan:', error);
toast.error("Terjadi kesalahan saat memperbarui jenis pendapatan"); toast.error('Terjadi kesalahan saat memperbarui jenis pendapatan');
} }
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header with Back Button */}
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button
</Box> variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Jenis Pendapatan
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> {/* Card Form */}
<Stack gap={"xs"}> <Paper
<Title order={4}>Edit Jenis Pendapatan</Title> w={{ base: '100%', md: '50%' }}
<TextInput bg={colors['white-1']}
value={formData.name} p="lg"
onChange={(val) => { radius="md"
setFormData({ ...formData, name: val.target.value }); shadow="sm"
}} style={{ border: '1px solid #e0e0e0' }}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Pendapatan</Text>} >
placeholder='Masukkan nama Jenis Pendapatan' <Stack gap="md">
/> <TextInput
<TextInput label="Nama Jenis Pendapatan"
label={<Text fw={"bold"} fz={"sm"}>Nilai</Text>} placeholder="Masukkan nama jenis pendapatan"
placeholder='Masukkan nilai' value={formData.name}
value={formatRupiah(formData.value)} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
onChange={(val) => { required
const raw = val.currentTarget.value; />
const cleanValue = unformatRupiah(raw);
setFormData({ ...formData, value: cleanValue }); <TextInput
}} label="Nilai"
/> placeholder="Masukkan nilai"
<Group> value={formatRupiah(formData.value)}
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> onChange={(e) => {
</Group> const raw = e.currentTarget.value;
</Stack> const cleanValue = unformatRupiah(raw);
</Paper> setFormData({ ...formData, value: cleanValue });
</Box> }}
required
/>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
); );
} }

View File

@@ -1,14 +1,24 @@
'use client' 'use client';
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'; import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreatePendapatan() { function CreatePendapatan() {
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan) const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
const router = useRouter() const router = useRouter();
const formatRupiah = (value: number | string) => { const formatRupiah = (value: number | string) => {
const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, '')); const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, ''));
@@ -25,48 +35,90 @@ function CreatePendapatan() {
const resetForm = () => { const resetForm = () => {
pendapatanState.create.form = { pendapatanState.create.form = {
name: "", name: '',
value: 0, value: 0,
} };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
await pendapatanState.create.submit(); await pendapatanState.create.submit();
resetForm() resetForm();
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan") router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan');
} };
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'}> return (
<Stack gap={"xs"}> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Title order={4}>Create Jenis Pendapatan</Title> {/* Header dengan tombol back + judul */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Jenis Pendapatan
</Title>
</Group>
{/* Card Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
value={pendapatanState.create.form.name} value={pendapatanState.create.form.name}
onChange={(val) => { onChange={(val) => {
pendapatanState.create.form.name = val.target.value; pendapatanState.create.form.name = val.target.value;
}} }}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Pendapatan</Text>} label={
placeholder='Masukkan nama jenis pendapatan' <Text fw="bold" fz="sm">
Nama Jenis Pendapatan
</Text>
}
placeholder="Masukkan nama jenis pendapatan"
required
/> />
<TextInput <TextInput
type='text' type="text"
value={formatRupiah(pendapatanState.create.form.value)} value={formatRupiah(pendapatanState.create.form.value)}
onChange={(val) => { onChange={(val) => {
const raw = val.currentTarget.value; const raw = val.currentTarget.value;
const cleanValue = unformatRupiah(raw); const cleanValue = unformatRupiah(raw);
pendapatanState.create.form.value = cleanValue; pendapatanState.create.form.value = cleanValue;
}} }}
label={<Text fw={"bold"} fz={"sm"}>Nilai</Text>} label={
placeholder='Masukkan nilai' <Text fw="bold" fz="sm">
Nilai
</Text>
}
placeholder="Masukkan nilai"
required
/> />
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> <Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,16 +1,33 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa'; import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
function Pendapatan() { function Pendapatan() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
@@ -18,7 +35,7 @@ function Pendapatan() {
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Pendapatan' title='Pendapatan'
placeholder='pencarian' placeholder='Cari pendapatan...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -29,105 +46,166 @@ function Pendapatan() {
} }
function ListPendapatan({ search }: { search: string }) { function ListPendapatan({ search }: { search: string }) {
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan) const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
const router = useRouter(); const router = useRouter();
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const formatRupiah = (value: number) => { const {
return new Intl.NumberFormat('id-ID', { data,
page,
totalPages,
loading,
load,
} = pendapatanState.findMany;
const formatRupiah = (value: number) =>
new Intl.NumberFormat('id-ID', {
style: 'currency', style: 'currency',
currency: 'IDR', currency: 'IDR',
minimumFractionDigits: 0, minimumFractionDigits: 0,
}).format(value); }).format(value);
};
const handleDelete = () => { const handleDelete = () => {
if (selectedId) { if (selectedId) {
pendapatanState.delete.byId(selectedId) pendapatanState.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
pendapatanState.findMany.load() load(page, 10, search);
} }
} };
useShallowEffect(() => { useShallowEffect(() => {
pendapatanState.findMany.load(); load(page, 10, search);
}, []) }, [page, search]);
const filteredData = (pendapatanState.findMany.data || []).filter(item => { const filteredData = data || [];
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.value.toString().toLowerCase().includes(keyword)
);
});
if (!pendapatanState.findMany.data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
) );
} }
const totalValue = filteredData.reduce((total, item) => total + item.value, 0);
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Pendapatan' <Title order={4}>Daftar Pendapatan</Title>
href='/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create' <Tooltip label="Tambah Pendapatan Baru" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
<TableThead> color="blue"
<TableTr> variant="light"
<TableTh>Nama</TableTh> onClick={() =>
<TableTh>Nilai</TableTh> router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create')
<TableTh>Edit</TableTh> }
<TableTh>Delete</TableTh> >
</TableTr> Tambah Baru
</TableThead> </Button>
<TableTbody> </Tooltip>
{filteredData.map((item) => ( </Group>
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd> <Box style={{ overflowX: 'auto' }}>
<TableTd>{formatRupiah(item.value)}</TableTd> <Table highlightOnHover striped withTableBorder withRowBorders>
<TableTd> <TableThead>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/${item.id}`)}> <TableTr>
<IconEdit size={20} /> <TableTh style={{ width: '40%' }}>Nama</TableTh>
</Button> <TableTh style={{ width: '25%' }}>Nilai</TableTh>
</TableTd> <TableTh style={{ width: '15%' }}>Edit</TableTh>
<TableTd> <TableTh style={{ width: '15%' }}>Delete</TableTh>
<Button
color='red'
disabled={pendapatanState.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr> </TableTr>
))} </TableThead>
<TableTr> <TableTbody>
<TableTd colSpan={4}> {filteredData.length > 0 ? (
<Text fw={'bold'}>Total</Text> <>
</TableTd> {filteredData.map((item) => (
<TableTd> <TableTr key={item.id}>
{formatRupiah(pendapatanState.findMany.data.reduce((total, item) => total + item.value, 0))} <TableTd>
</TableTd> <Text fw={500} truncate="end" lineClamp={1}>
</TableTr> {item.name}
</TableTbody> </Text>
</Table> </TableTd>
<TableTd>{formatRupiah(item.value)}</TableTd>
<TableTd>
<Button
variant="light"
color="green"
onClick={() =>
router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/${item.id}`)
}
>
<IconEdit size={18} />
<Text ml={5}>Edit</Text>
</Button>
</TableTd>
<TableTd>
<Button
variant="light"
color="red"
disabled={pendapatanState.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={18} />
<Text ml={5}>Hapus</Text>
</Button>
</TableTd>
</TableTr>
))}
{/* Row total */}
<TableTr>
<TableTd colSpan={1}>
<Text fw={'bold'}>Total</Text>
</TableTd>
<TableTd colSpan={3}>
<Text fw={'bold'}>{formatRupiah(totalValue)}</Text>
</TableTd>
</TableTr>
</>
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data pendapatan yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
{/* Pagination */}
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
{/* Modal Konfirmasi Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleDelete} onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus pendapatan ini?' text="Apakah anda yakin ingin menghapus pendapatan ini?"
/> />
</Box> </Box>
); );

View File

@@ -1,8 +1,17 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
TextInput,
Title,
Tooltip
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react'; import { useEffect } from 'react';
@@ -11,33 +20,33 @@ import { useProxy } from 'valtio/utils';
import demografiPekerjaan from '../../../_state/ekonomi/demografi-pekerjaan'; import demografiPekerjaan from '../../../_state/ekonomi/demografi-pekerjaan';
function EditDemografiPekerjaan() { function EditDemografiPekerjaan() {
const router = useRouter() const router = useRouter();
const params = useParams() as { id: string } const params = useParams() as { id: string };
const stateDemografi = useProxy(demografiPekerjaan) const stateDemografi = useProxy(demografiPekerjaan);
const id = params.id const id = params.id;
useEffect(() => { useEffect(() => {
if (!id) return; if (!id) return;
stateDemografi.update.id = id; stateDemografi.update.id = id;
stateDemografi.findUnique.load(id) stateDemografi.findUnique
.load(id)
.then(() => { .then(() => {
const data = stateDemografi.findUnique.data; const data = stateDemografi.findUnique.data;
if (data) { if (data) {
stateDemografi.update.form = { stateDemografi.update.form = {
pekerjaan: String(data.pekerjaan || ''), pekerjaan: String(data.pekerjaan || ''),
lakiLaki: Number(data.lakiLaki || 0), lakiLaki: Number(data.lakiLaki || 0),
perempuan: Number(data.perempuan || 0) perempuan: Number(data.perempuan || 0),
}; };
} }
}) })
.catch(error => { .catch((error) => {
console.error('Error loading data:', error); console.error('Error loading data:', error);
toast.error('Gagal memuat data'); toast.error('Gagal memuat data');
}); });
}, [id]); }, [id]);
// Di handleSubmit, ubah menjadi:
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
stateDemografi.update.id = id; stateDemografi.update.id = id;
@@ -51,52 +60,88 @@ function EditDemografiPekerjaan() {
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack size={20} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button
</Box> variant="subtle"
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}> onClick={() => router.back()}
<Stack> p="xs"
<Title order={3}>Edit Demografi Pekerjaan</Title> radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Demografi Pekerjaan
</Title>
</Group>
{/* Form Card */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label="Pekerjaan" label="Pekerjaan"
placeholder="masukkan pekerjaan" placeholder="Masukkan jenis pekerjaan"
value={stateDemografi.update.form.pekerjaan} value={stateDemografi.update.form.pekerjaan}
onChange={(val) => { onChange={(e) =>
stateDemografi.update.form.pekerjaan = val.currentTarget.value; (stateDemografi.update.form.pekerjaan = e.currentTarget.value)
}} }
required
/> />
<TextInput <TextInput
label="Jumlah Pekerja Laki - Laki" label="Jumlah Pekerja Laki-laki"
type="number" type="number"
placeholder="masukkan jumlah pekerja laki - laki" placeholder="Masukkan jumlah pekerja laki-laki"
value={stateDemografi.update.form.lakiLaki} value={stateDemografi.update.form.lakiLaki}
onChange={(val) => { onChange={(e) =>
stateDemografi.update.form.lakiLaki = Number(val.currentTarget.value); (stateDemografi.update.form.lakiLaki = Number(
}} e.currentTarget.value
))
}
required
/> />
<TextInput <TextInput
label="Jumlah Pekerja Perempuan" label="Jumlah Pekerja Perempuan"
type="number" type="number"
placeholder="masukkan jumlah pekerja perempuan" placeholder="Masukkan jumlah pekerja perempuan"
value={stateDemografi.update.form.perempuan} value={stateDemografi.update.form.perempuan}
onChange={(val) => { onChange={(e) =>
stateDemografi.update.form.perempuan = Number(val.currentTarget.value); (stateDemografi.update.form.perempuan = Number(
}} e.currentTarget.value
))
}
required
/> />
<Button
mt={10} <Group justify="flex-end">
bg={colors['blue-button']} <Button
onClick={handleSubmit} onClick={handleSubmit}
> radius="md"
Simpan Perubahan size="md"
</Button> style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>
) );
} }
export default EditDemografiPekerjaan; export default EditDemografiPekerjaan;

View File

@@ -1,8 +1,18 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -12,15 +22,15 @@ import demografiPekerjaan from '../../../_state/ekonomi/demografi-pekerjaan';
function CreateDemografiPekerjaan() { function CreateDemografiPekerjaan() {
const stateDemografi = useProxy(demografiPekerjaan); const stateDemografi = useProxy(demografiPekerjaan);
const [chartData, setChartData] = useState<any[]>([]); const [chartData, setChartData] = useState<any[]>([]);
const router = useRouter() const router = useRouter();
const resetForm = () => { const resetForm = () => {
stateDemografi.create.form = { stateDemografi.create.form = {
pekerjaan: "", pekerjaan: '',
lakiLaki: 0, lakiLaki: 0,
perempuan: 0, perempuan: 0,
} };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
const id = await stateDemografi.create.create(); const id = await stateDemografi.create.create();
@@ -32,58 +42,85 @@ function CreateDemografiPekerjaan() {
} }
} }
resetForm(); resetForm();
router.push("/admin/ekonomi/demografi-pekerjaan"); router.push('/admin/ekonomi/demografi-pekerjaan');
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack size={20} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button
</Box> variant="subtle"
<Box> onClick={() => router.back()}
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> p="xs"
<Title order={4}>Tambah Demografi Pekerjaan</Title> radius="md"
<Stack gap={"xs"}> >
<TextInput <IconArrowBack color={colors['blue-button']} size={24} />
label="Pekerjaan" </Button>
type="text" </Tooltip>
value={stateDemografi.create.form.pekerjaan} <Title order={4} ml="sm" c="dark">
placeholder="Masukkan pekerjaan" Tambah Demografi Pekerjaan
onChange={(val) => { </Title>
stateDemografi.create.form.pekerjaan = val.currentTarget.value; </Group>
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Pekerjaan"
type="text"
value={stateDemografi.create.form.pekerjaan}
placeholder="Masukkan pekerjaan"
onChange={(val) => {
stateDemografi.create.form.pekerjaan = val.currentTarget.value;
}}
required
/>
<TextInput
label="Jumlah Pekerja Laki-Laki"
type="number"
value={stateDemografi.create.form.lakiLaki}
placeholder="Masukkan jumlah pekerja laki-laki"
onChange={(val) => {
stateDemografi.create.form.lakiLaki = Number(val.currentTarget.value);
}}
required
/>
<TextInput
label="Jumlah Pekerja Perempuan"
type="number"
value={stateDemografi.create.form.perempuan}
placeholder="Masukkan jumlah pekerja perempuan"
onChange={(val) => {
stateDemografi.create.form.perempuan = Number(val.currentTarget.value);
}}
required
/>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
/> >
<TextInput Simpan
label="Jumlah Pekerja Laki - Laki" </Button>
type="number" </Group>
value={stateDemografi.create.form.lakiLaki} </Stack>
placeholder="Masukkan jumlah pekerja laki - laki" </Paper>
onChange={(val) => {
stateDemografi.create.form.lakiLaki = Number(val.currentTarget.value);
}}
/>
<TextInput
label="Jumlah Pekerja Perempuan"
type="number"
value={stateDemografi.create.form.perempuan}
placeholder="Masukkan jumlah pekerja perempuan"
onChange={(val) => {
stateDemografi.create.form.perempuan = Number(val.currentTarget.value);
}}
/>
<Group>
<Button
bg={colors['blue-button']}
mt={10}
onClick={handleSubmit}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Box> </Box>
); );
} }

View File

@@ -1,14 +1,32 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { BarChart } from '@mantine/charts'; import { BarChart } from '@mantine/charts';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import {
Box,
Button,
Center,
Group,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
Pagination,
Flex,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
import demografiPekerjaan from '../../_state/ekonomi/demografi-pekerjaan'; import demografiPekerjaan from '../../_state/ekonomi/demografi-pekerjaan';
@@ -18,7 +36,7 @@ function DemografiPekerjaan() {
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Demografi Pekerjaan' title='Demografi Pekerjaan'
placeholder='pencarian' placeholder='Cari pekerjaan atau jumlah pekerja...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -34,131 +52,193 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
pekerjaan: string; pekerjaan: string;
lakiLaki: number; lakiLaki: number;
perempuan: number; perempuan: number;
} };
const router = useRouter(); const router = useRouter();
const stateDemografi = useProxy(demografiPekerjaan) const stateDemografi = useProxy(demografiPekerjaan);
const [chartData, setChartData] = useState<DemografiPekerjaan[]>([]); const [chartData, setChartData] = useState<DemografiPekerjaan[]>([]);
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready const [mounted, setMounted] = useState(false);
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const {
data,
page,
totalPages,
loading,
load,
} = stateDemografi.findMany;
const handleDelete = () => { const handleDelete = () => {
if (selectedId) { if (selectedId) {
stateDemografi.delete.byId(selectedId) stateDemografi.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
stateDemografi.findMany.load()
} }
} };
useShallowEffect(() => { useShallowEffect(() => {
setMounted(true) setMounted(true);
stateDemografi.findMany.load() load(page, 10, search);
}, []) }, [page, search]);
useEffect(() => { useEffect(() => {
setMounted(true); if (data) {
if (stateDemografi.findMany.data) { setChartData(
setChartData(stateDemografi.findMany.data.map((item) => ({ data.map((item) => ({
id: item.id, id: item.id,
pekerjaan: item.pekerjaan, pekerjaan: item.pekerjaan,
lakiLaki: Number(item.lakiLaki), lakiLaki: Number(item.lakiLaki),
perempuan: Number(item.perempuan), perempuan: Number(item.perempuan),
}))); }))
);
} }
}, [stateDemografi.findMany.data]); }, [data]);
const filteredData = (stateDemografi.findMany.data || []).filter(item => { const filteredData = data || [];
const keyword = search.toLowerCase();
if (loading || !data) {
return ( return (
item.pekerjaan.toLowerCase().includes(keyword) || <Stack py={10}>
item.lakiLaki.toString().toLowerCase().includes(keyword) || <Skeleton height={600} radius="md" />
item.perempuan.toString().toLowerCase().includes(keyword) </Stack>
); );
}); }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Demografi Pekerjaan' <Title order={4}>List Demografi Pekerjaan</Title>
href='/admin/ekonomi/demografi-pekerjaan/create' <Tooltip label="Tambah Data Pekerjaan" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
<TableThead> color="blue"
<TableTr> variant="light"
<TableTh>Pekerjaan</TableTh> onClick={() => router.push('/admin/ekonomi/demografi-pekerjaan/create')}
<TableTh>Jumlah Pekerja Laki - Laki</TableTh> >
<TableTh>Jumlah Pekerja Perempuan</TableTh> Tambah Baru
<TableTh>Edit</TableTh> </Button>
<TableTh>Delete</TableTh> </Tooltip>
</TableTr> </Group>
</TableThead>
<TableTbody> <Box style={{ overflowX: 'auto' }}>
{filteredData.map((item) => ( <Table highlightOnHover>
<TableTr key={item.id}> <TableThead>
<TableTd>{item.pekerjaan}</TableTd> <TableTr>
<TableTd>{item.lakiLaki}</TableTd> <TableTh>Pekerjaan</TableTh>
<TableTd>{item.perempuan}</TableTd> <TableTh>Laki - Laki</TableTh>
<TableTd> <TableTh>Perempuan</TableTh>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/demografi-pekerjaan/${item.id}`)}> <TableTh>Edit</TableTh>
<IconEdit size={20} /> <TableTh>Hapus</TableTh>
</Button>
</TableTd>
<TableTd>
<Button
color='red'
disabled={stateDemografi.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr> </TableTr>
))} </TableThead>
</TableTbody> <TableTbody>
</Table> {filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.pekerjaan}</TableTd>
<TableTd>{item.lakiLaki}</TableTd>
<TableTd>{item.perempuan}</TableTd>
<TableTd>
<Button
variant="light"
color="green"
onClick={() =>
router.push(`/admin/ekonomi/demografi-pekerjaan/${item.id}`)
}
>
<IconEdit size={18} />
</Button>
</TableTd>
<TableTd>
<Button
variant="light"
color="red"
disabled={stateDemografi.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={18} />
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data demografi pekerjaan yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
{/* Pagination */}
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
{/* Chart */} {/* Chart */}
{!mounted && !chartData ? ( <Box mt={30} style={{ width: '100%', minHeight: 400 }}>
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}> <Paper bg={colors['white-1']} p="md" radius="md" withBorder>
<Paper bg={colors['white-1']} p={'md'}> <Stack gap={"xs"}>
<Title pb={10} order={3}>Data Kelahiran & Kematian</Title> <Title pb={10} order={4}>
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text> Grafik Demografi Pekerjaan
</Paper> </Title>
</Box> {mounted && chartData.length > 0 ? (
) : ( <BarChart
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}> h={450}
<Paper bg={colors['white-1']} p={'md'}> data={chartData}
<Title pb={10} order={4}>Data Kelahiran & Kematian</Title> dataKey="pekerjaan"
{mounted && chartData.length > 0 && ( type="stacked"
<Box w={{ base: '100%', md: '30%' }}> series={[
<BarChart { name: 'lakiLaki', color: '#5082EE', label: 'Laki - Laki' },
h={450} { name: 'perempuan', color: '#6EDF9C', label: 'Perempuan' },
data={chartData} ]}
dataKey="pekerjaan" />
type="stacked" ) : (
series={[ <Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
{ name: 'lakiLaki', color: '#5082EE', label: 'Laki - Laki' },
{ name: 'perempuan', color: '#6EDF9C', label: 'Perempuan' },
]}
/>
</Box>
)} )}
</Paper> <Box py={10}>
</Box> <Group justify='center'>
)} <Flex align="center" gap={10}>
<Box bg="#5082EE" w={20} h={20} />
<Text>Laki - Laki</Text>
</Flex>
<Flex align="center" gap={10}>
<Box bg="#6EDF9C" w={20} h={20} />
<Text>Perempuan</Text>
</Flex>
</Group>
</Box>
</Stack>
</Paper>
</Box>
{/* Modal Konfirmasi Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleDelete} onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus demografi pekerjaan ini?' text="Apakah anda yakin ingin menghapus demografi pekerjaan ini?"
/> />
</Box> </Box>
); );

View File

@@ -3,78 +3,116 @@
import jumlahPendudukMiskin from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-penduduk-miskin'; import jumlahPendudukMiskin from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-penduduk-miskin';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, TextInput, Title } from '@mantine/core'; import { Box, Button, Paper, Stack, TextInput, Title, Group, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { toast } from 'react-toastify';
function EditJumlahPendudukMiskin() { function EditJumlahPendudukMiskin() {
const router = useRouter() const router = useRouter();
const params = useParams() as { id: string } const params = useParams() as { id: string };
const stateJPM = useProxy(jumlahPendudukMiskin) const stateJPM = useProxy(jumlahPendudukMiskin);
const id = params.id const id = params.id;
// Load data saat komponen mount
useEffect(() => { useEffect(() => {
if (id) { if (!id) return;
stateJPM.findUnique.load(id).then(() => {
const data = stateJPM.findUnique.data const loadData = async () => {
try {
await stateJPM.findUnique.load(id);
const data = stateJPM.findUnique.data;
if (data) { if (data) {
stateJPM.update.form = { stateJPM.update.form = {
year: data.year || 0, year: data.year || 0,
totalPoorPopulation: data.totalPoorPopulation || 0, totalPoorPopulation: data.totalPoorPopulation || 0,
} };
} }
}) } catch (error) {
} console.error('Gagal memuat data:', error);
}, [id]) toast.error('Gagal memuat data jumlah penduduk miskin');
}
};
loadData();
}, [id]);
const handleSubmit = async () => { const handleSubmit = async () => {
// Set the ID before submitting try {
stateJPM.update.id = id; stateJPM.update.id = id;
await stateJPM.update.submit(); await stateJPM.update.submit();
router.push('/admin/ekonomi/jumlah-penduduk-miskin') toast.success('Data jumlah penduduk miskin berhasil diperbarui!');
} router.push('/admin/ekonomi/jumlah-penduduk-miskin');
return ( } catch (error) {
<Box> console.error('Gagal menyimpan data:', error);
<Box mb={10}> toast.error('Terjadi kesalahan saat menyimpan data');
<Button variant="subtle" onClick={() => router.back()}> }
<IconArrowBack size={20} /> };
</Button>
</Box> return (
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Stack> <Group mb="md">
<Title order={3}>Edit Jumlah Penduduk Miskin</Title> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Jumlah Penduduk Miskin
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label="Tahun" label="Tahun"
placeholder="masukkan tahun" placeholder="Masukkan tahun"
type="number"
required
value={stateJPM.update.form.year} value={stateJPM.update.form.year}
onChange={(val) => { onChange={(val) => {
stateJPM.update.form.year = Number(val.currentTarget.value); stateJPM.update.form.year = Number(val.currentTarget.value);
}} }}
/> />
<TextInput <TextInput
label="Jumlah Penduduk Miskin" label="Jumlah Penduduk Miskin"
placeholder="Masukkan jumlah penduduk miskin"
type="number" type="number"
placeholder="masukkan jumlah penduduk miskin" required
value={stateJPM.update.form.totalPoorPopulation} value={stateJPM.update.form.totalPoorPopulation}
onChange={(val) => { onChange={(val) => {
stateJPM.update.form.totalPoorPopulation = Number(val.currentTarget.value); stateJPM.update.form.totalPoorPopulation = Number(val.currentTarget.value);
}} }}
/> />
<Button
mt={10} <Group justify="right">
bg={colors['blue-button']} <Button
onClick={handleSubmit} onClick={handleSubmit}
> radius="md"
Simpan size="md"
</Button> style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>
) );
} }
export default EditJumlahPendudukMiskin; export default EditJumlahPendudukMiskin;

View File

@@ -1,26 +1,25 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client';
import grafikkepuasan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan'; import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import colors from '@/con/colors';
import jumlahPendudukMiskin from '../../../_state/ekonomi/jumlah-penduduk-miskin'; import jumlahPendudukMiskin from '../../../_state/ekonomi/jumlah-penduduk-miskin';
function CreateJumlahPendudukMiskin() { export default function CreateJumlahPendudukMiskin() {
const stateJPM = useProxy(jumlahPendudukMiskin); const stateJPM = useProxy(jumlahPendudukMiskin);
const [chartData, setChartData] = useState<any[]>([]); const [chartData, setChartData] = useState<any[]>([]);
const router = useRouter() const router = useRouter();
const resetForm = () => { const resetForm = () => {
stateJPM.create.form = { stateJPM.create.form = {
year: 0, year: new Date().getFullYear(),
totalPoorPopulation: 0, totalPoorPopulation: 0,
} };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
const id = await stateJPM.create.create(); const id = await stateJPM.create.create();
@@ -32,51 +31,72 @@ function CreateJumlahPendudukMiskin() {
} }
} }
resetForm(); resetForm();
router.push("/admin/ekonomi/jumlah-penduduk-miskin"); router.push('/admin/ekonomi/jumlah-penduduk-miskin');
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack size={20} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Box> <IconArrowBack color={colors['blue-button']} size={24} />
<Box> </Button>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> </Tooltip>
<Title order={4}>Tambah Grafik Hasil Kepuasan Masyarakat</Title> <Title order={4} ml="sm" c="dark">
<Stack gap={"xs"}> Tambah Jumlah Penduduk Miskin
<TextInput </Title>
label="Tahun" </Group>
type="number"
value={stateJPM.create.form.year} {/* Form Paper */}
placeholder="Masukkan tahun" <Paper
onChange={(val) => { w={{ base: '100%', md: '50%' }}
stateJPM.create.form.year = Number(val.currentTarget.value); bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Tahun"
type="number"
value={stateJPM.create.form.year || ''}
placeholder="Masukkan tahun"
onChange={(e) => {
const value = e.currentTarget.value;
stateJPM.create.form.year = value ? Number(value) : 0;
}}
required
/>
<TextInput
label="Jumlah Penduduk Miskin"
type="number"
value={stateJPM.create.form.totalPoorPopulation}
placeholder="Masukkan jumlah penduduk miskin"
onChange={(e) => {
stateJPM.create.form.totalPoorPopulation = Number(e.currentTarget.value);
}}
required
/>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
/> >
<TextInput Simpan
label="Jumlah Penduduk Miskin" </Button>
type="number" </Group>
value={stateJPM.create.form.totalPoorPopulation} </Stack>
placeholder="Masukkan jumlah penduduk miskin" </Paper>
onChange={(val) => {
stateJPM.create.form.totalPoorPopulation = Number(val.currentTarget.value);
}}
/>
<Group>
<Button
bg={colors['blue-button']}
mt={10}
onClick={handleSubmit}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Box> </Box>
); );
} }
export default CreateJumlahPendudukMiskin;

View File

@@ -1,15 +1,14 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useMediaQuery, useShallowEffect } from '@mantine/hooks'; import { useShallowEffect, useMediaQuery } from '@mantine/hooks';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import jumlahPendudukMiskin from '../../_state/ekonomi/jumlah-penduduk-miskin'; import jumlahPendudukMiskin from '../../_state/ekonomi/jumlah-penduduk-miskin';
import { Bar, BarChart, Legend, XAxis, YAxis, Tooltip } from 'recharts'; import { Bar, BarChart, Legend, XAxis, YAxis, Tooltip as RechartsTooltip } from 'recharts';
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
function JumlahPendudukMiskin() { function JumlahPendudukMiskin() {
@@ -18,7 +17,7 @@ function JumlahPendudukMiskin() {
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Jumlah Penduduk Miskin' title='Jumlah Penduduk Miskin'
placeholder='pencarian' placeholder='Cari tahun atau jumlah penduduk miskin...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -29,138 +28,158 @@ function JumlahPendudukMiskin() {
} }
function ListJumlahPendudukMiskin({ search }: { search: string }) { function ListJumlahPendudukMiskin({ search }: { search: string }) {
type JPMGrafik = { type JPMGrafik = { id: string; year: number; totalPoorPopulation: number }
id: string;
year: number;
totalPoorPopulation: number;
}
const stateJPM = useProxy(jumlahPendudukMiskin); const stateJPM = useProxy(jumlahPendudukMiskin);
const [chartData, setChartData] = useState<JPMGrafik[]>([]); const [chartData, setChartData] = useState<JPMGrafik[]>([]);
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready const [mounted, setMounted] = useState(false);
const isTablet = useMediaQuery('(max-width: 1024px)') const [modalHapus, setModalHapus] = useState(false);
const isMobile = useMediaQuery('(max-width: 768px)') const [selectedId, setSelectedId] = useState<string | null>(null);
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter(); const router = useRouter();
const handleDelete = () => { const isTablet = useMediaQuery('(max-width:1024px)');
if (selectedId) { const isMobile = useMediaQuery('(max-width:768px)');
stateJPM.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
stateJPM.findMany.load()
}
}
const {
data,
page,
loading,
load,
totalPages,
} = stateJPM.findMany;
// Load data
useShallowEffect(() => { useShallowEffect(() => {
setMounted(true) setMounted(true);
stateJPM.findMany.load() load(page, 10, search);
}, []) }, [page, search]);
useEffect(() => { useEffect(() => {
setMounted(true);
if (stateJPM.findMany.data) { if (stateJPM.findMany.data) {
setChartData(stateJPM.findMany.data.map((item) => ({ setChartData(stateJPM.findMany.data.map(item => ({
id: item.id, id: item.id,
year: Number(item.year), year: Number(item.year),
totalPoorPopulation: Number(item.totalPoorPopulation), totalPoorPopulation: Number(item.totalPoorPopulation)
}))); })));
} }
}, [stateJPM.findMany.data]); }, [stateJPM.findMany.data]);
const filteredData = (stateJPM.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.year.toString().toLowerCase().includes(keyword) ||
item.totalPoorPopulation.toString().toLowerCase().includes(keyword)
);
});
if (!stateJPM.findMany.data) { const handleDelete = () => {
if (selectedId) {
stateJPM.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
stateJPM.findMany.load();
}
}
if (loading || !data) {
return ( return (
<Stack> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
return ( return (
<Box py={10}> <Box py={10}>
<Stack gap={'xs'}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Paper bg={colors['white-1']} p={'md'}> <Group justify="space-between" mb="md">
<JudulList <Title order={4}>Daftar Jumlah Penduduk Miskin</Title>
title='List Jumlah Penduduk Miskin' <Tooltip label="Tambah Data" withArrow>
href='/admin/ekonomi/jumlah-penduduk-miskin/create' <Button
/> leftSection={<IconEdit size={18} />}
<Table striped withTableBorder withRowBorders> color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/jumlah-penduduk-miskin/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Tahun</TableTh> <TableTh style={{ width: '25%' }}>Tahun</TableTh>
<TableTh>Jumlah Penduduk Miskin</TableTh> <TableTh style={{ width: '35%' }}>Jumlah Penduduk Miskin</TableTh>
<TableTh>Edit</TableTh> <TableTh style={{ width: '20%' }}>Edit</TableTh>
<TableTh>Delete</TableTh> <TableTh style={{ width: '20%' }}>Delete</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? filteredData.map(item => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.year}</TableTd> <TableTd>{item.year}</TableTd>
<TableTd>{item.totalPoorPopulation}</TableTd> <TableTd>{item.totalPoorPopulation}</TableTd>
<TableTd> <TableTd>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-miskin/${item.id}`)}> <Button variant='light' color="green" onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-miskin/${item.id}`)}>
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button variant='light' color="red" disabled={stateJPM.delete.loading} onClick={() => { setSelectedId(item.id); setModalHapus(true) }}>
color='red'
disabled={stateJPM.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} /> <IconTrash size={20} />
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} )) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody> </TableTbody>
</Table> </Table>
</Paper> </Box>
</Paper>
{/* Chart */} <Center>
{!mounted && !chartData ? ( <Pagination
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}> value={page}
<Paper bg={colors['white-1']} p={'md'}> onChange={(newPage) => {
<Title pb={10} order={3}>Grafik Jumlah Penduduk Miskin</Title> load(newPage, 10);
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text> window.scrollTo({ top: 0, behavior: 'smooth' });
</Paper> }}
</Box> total={totalPages}
) : ( mt="md"
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}> mb="md"
<Paper bg={colors['white-1']} p={'md'}> color="blue"
<Title pb={10} order={4}>Grafik Jumlah Penduduk Miskin</Title> radius="md"
{mounted && chartData.length > 0 && ( />
<BarChart width={isMobile ? 450 : isTablet ? 500 : 550} height={350} data={chartData} > </Center>
<XAxis dataKey="year" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="totalPoorPopulation" fill={colors['blue-button']} name="Jumlah Penduduk Miskin" />
</BarChart>
)}
</Paper>
</Box>
)}
</Stack>
{/* Modal Konfirmasi Hapus */} {/* Chart */}
<Paper bg={colors['white-1']} p="md" mt="lg" withBorder radius="md">
<Stack>
<Box mt="lg" style={{ width: '100%', minHeight: 350 }}>
<Title order={4} mb="sm">Grafik Jumlah Penduduk Miskin</Title>
{mounted && chartData.length > 0 ? (
<BarChart width={isMobile ? 450 : isTablet ? 500 : 550} height={350} data={chartData}>
<XAxis dataKey="year" />
<YAxis />
<RechartsTooltip />
<Legend />
<Bar dataKey="totalPoorPopulation" fill={colors['blue-button']} name="Jumlah Penduduk Miskin" />
</BarChart>
) : (
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Box>
</Stack>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleDelete} onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus grafik jumlah penduduk miskin ini?' text='Apakah anda yakin ingin menghapus data ini?'
/> />
</Box> </Box>
); );

View File

@@ -1,62 +1,124 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; import {
Stack,
Tabs,
TabsList,
TabsPanel,
TabsTab,
Title,
Tooltip,
ScrollArea,
} from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { IconUsers, IconSchool } from '@tabler/icons-react';
function LayoutTabs({ children }: { children: React.ReactNode }) { function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter();
const pathname = usePathname() const pathname = usePathname();
const tabs = [
{
label: "Pengangguran Berdasarkan Usia",
value: "pengangguranberdasarkanusia",
href: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia"
},
{
label: "Pengangguran Berdasarkan Pendidikan",
value: "pengangguranberdasarkanpendidikan",
href: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan"
},
];
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 tabs = [
const tab = tabs.find(t => t.value === value) {
if (tab) { label: "Pengangguran Berdasarkan Usia",
router.push(tab.href) value: "pengangguranberdasarkanusia",
} href: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia",
setActiveTab(value) icon: <IconUsers size={18} stroke={1.8} />,
} tooltip: "Data pengangguran menurut kelompok usia",
},
{
label: "Pengangguran Berdasarkan Pendidikan",
value: "pengangguranberdasarkanpendidikan",
href: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan",
icon: <IconSchool size={18} stroke={1.8} />,
tooltip: "Data pengangguran menurut tingkat pendidikan",
},
];
useEffect(() => { const currentTab = tabs.find(tab => tab.href === pathname);
const match = tabs.find(tab => tab.href === pathname) const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
if (match) {
setActiveTab(match.value)
}
}, [pathname])
return ( const handleTabChange = (value: string | null) => {
<Stack> const tab = tabs.find(t => t.value === value);
<Title order={3}>Jumlah Penduduk Usia Kerja yang Menganggur</Title> if (tab) router.push(tab.href);
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> setActiveTab(value);
<TabsList p={"xs"} bg={"#BBC8E7FF"}> };
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> useEffect(() => {
))} const match = tabs.find(tab => tab.href === pathname);
</TabsList> if (match) setActiveTab(match.value);
{tabs.map((e, i) => ( }, [pathname]);
<TabsPanel key={i} value={e.value}>
{/* Konten dummy, bisa diganti tergantung routing */} return (
<></> <Stack gap="lg">
</TabsPanel> <Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
))} Jumlah Penduduk Usia Kerja yang Menganggur
</Tabs> </Title>
<Tabs
color={colors['blue-button']}
variant="pills"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem",
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: "pop", duration: 200 }}
>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
flexShrink: 0,
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel
key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{children} {children}
</Stack> </TabsPanel>
); ))}
</Tabs>
</Stack>
);
} }
export default LayoutTabs; export default LayoutTabs;

View File

@@ -1,108 +1,117 @@
'use client' 'use client';
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur'; import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditGrafikBerdasarkanPendidikan() { function EditGrafikBerdasarkanPendidikan() {
const router = useRouter() const router = useRouter();
const params = useParams() as { id: string } const params = useParams() as { id: string };
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan) const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan);
const id = params.id const id = params.id;
useEffect(() => { useEffect(() => {
if(id){ if (id) {
stategrafik.findUnique.load(id).then(() => { stategrafik.findUnique.load(id).then(() => {
const data = stategrafik.findUnique.data const data = stategrafik.findUnique.data;
if(data){ if (data) {
stategrafik.update.form = { stategrafik.update.form = {
SD: data.SD || '', SD: data.SD || '',
SMP: data.SMP || '', SMP: data.SMP || '',
SMA: data.SMA || '', SMA: data.SMA || '',
D3: data.D3 || '', D3: data.D3 || '',
S1: data.S1 || '', S1: data.S1 || '',
} };
} }
}) });
} }
}, [id]) }, [id]);
const handleSubmit = async () => { const handleSubmit = async () => {
stategrafik.update.id = id; stategrafik.update.id = id;
await stategrafik.update.submit(); await stategrafik.update.submit();
router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan') router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan');
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack size={20} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Grafik Pengangguran Berdasarkan Pendidikan
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="SD"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.update.form.SD}
onChange={(val) => (stategrafik.update.form.SD = val.currentTarget.value)}
/>
<TextInput
label="SMP"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.update.form.SMP}
onChange={(val) => (stategrafik.update.form.SMP = val.currentTarget.value)}
/>
<TextInput
label="SMA"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.update.form.SMA}
onChange={(val) => (stategrafik.update.form.SMA = val.currentTarget.value)}
/>
<TextInput
label="D3"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.update.form.D3}
onChange={(val) => (stategrafik.update.form.D3 = val.currentTarget.value)}
/>
<TextInput
label="S1"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.update.form.S1}
onChange={(val) => (stategrafik.update.form.S1 = val.currentTarget.value)}
/>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box> </Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Edit Grafik Pengangguran Berdasarkan Pendidikan</Title>
<TextInput
label="SD"
type='number'
placeholder="masukkan jumlah"
value={stategrafik.update.form.SD}
onChange={(val) => {
stategrafik.update.form.SD = val.currentTarget.value;
}}
/>
<TextInput
label="SMP"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.update.form.SMP}
onChange={(val) => {
stategrafik.update.form.SMP = val.currentTarget.value;
}}
/>
<TextInput
label="SMA"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.update.form.SMA}
onChange={(val) => {
stategrafik.update.form.SMA = val.currentTarget.value;
}}
/>
<TextInput
label="D3"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.update.form.D3}
onChange={(val) => {
stategrafik.update.form.D3 = val.currentTarget.value;
}}
/>
<TextInput
label="S1"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.update.form.S1}
onChange={(val) => {
stategrafik.update.form.S1 = val.currentTarget.value;
}}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Submit
</Button>
</Stack>
</Paper>
</Box>
); );
} }

View File

@@ -1,32 +1,30 @@
'use client' 'use client';
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react'; import React from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import grafikBerdasarkanJenisKelamin from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanJenisKelamin'; import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { useState } from 'react'; import { useState } from 'react';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput } from '@mantine/core'; import { Box, Button, Paper, Stack, Title, TextInput, Group, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
function CreateGrafikBerdasarkanPendidikan() { function CreateGrafikBerdasarkanPendidikan() {
const router = useRouter(); const router = useRouter();
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan) const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan);
const [donutData, setDonutData] = useState<any[]>([]); const [donutData, setDonutData] = useState<any[]>([]);
const resetForm = () => { const resetForm = () => {
stategrafik.create.form = { stategrafik.create.form = {
...stategrafik.create.form, ...stategrafik.create.form,
SD: "", SD: '',
SMP: "", SMP: '',
SMA: "", SMA: '',
D3: "", D3: '',
S1: "", S1: '',
} };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
const id = await stategrafik.create.create(); const id = await stategrafik.create.create();
@@ -38,73 +36,91 @@ function CreateGrafikBerdasarkanPendidikan() {
} }
} }
resetForm(); resetForm();
router.push("/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan") router.push(
} '/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan'
);
};
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack size={20} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}> </Tooltip>
<Stack> <Title order={4} ml="sm" c="dark">
<Title order={3}>Create Grafik Pengangguran Berdasarkan Pendidikan</Title> Tambah Data Pengangguran Berdasarkan Pendidikan
<TextInput </Title>
label="SD" </Group>
type='number'
placeholder="masukkan jumlah" <Paper
value={stategrafik.create.form.SD} w={{ base: '100%', md: '50%' }}
onChange={(val) => { bg={colors['white-1']}
stategrafik.create.form.SD = val.currentTarget.value; p="lg"
}} radius="md"
/> shadow="sm"
<TextInput style={{ border: '1px solid #e0e0e0' }}
label="SMP" >
type="number" <Stack gap="md">
placeholder="masukkan jumlah" <TextInput
value={stategrafik.create.form.SMP} label="SD"
onChange={(val) => { type="number"
stategrafik.create.form.SMP = val.currentTarget.value; placeholder="Masukkan jumlah"
}} value={stategrafik.create.form.SD}
/> onChange={(val) => (stategrafik.create.form.SD = val.currentTarget.value)}
<TextInput required
label="SMA" />
type="number" <TextInput
placeholder="masukkan jumlah" label="SMP"
value={stategrafik.create.form.SMA} type="number"
onChange={(val) => { placeholder="Masukkan jumlah"
stategrafik.create.form.SMA = val.currentTarget.value; value={stategrafik.create.form.SMP}
}} onChange={(val) => (stategrafik.create.form.SMP = val.currentTarget.value)}
/> required
<TextInput />
label="D3" <TextInput
type="number" label="SMA"
placeholder="masukkan jumlah" type="number"
value={stategrafik.create.form.D3} placeholder="Masukkan jumlah"
onChange={(val) => { value={stategrafik.create.form.SMA}
stategrafik.create.form.D3 = val.currentTarget.value; onChange={(val) => (stategrafik.create.form.SMA = val.currentTarget.value)}
}} required
/> />
<TextInput <TextInput
label="S1" label="D3"
type="number" type="number"
placeholder="masukkan jumlah" placeholder="Masukkan jumlah"
value={stategrafik.create.form.S1} value={stategrafik.create.form.D3}
onChange={(val) => { onChange={(val) => (stategrafik.create.form.D3 = val.currentTarget.value)}
stategrafik.create.form.S1 = val.currentTarget.value; required
}} />
/> <TextInput
<Button label="S1"
mt={10} type="number"
bg={colors['blue-button']} placeholder="Masukkan jumlah"
onClick={handleSubmit} value={stategrafik.create.form.S1}
> onChange={(val) => (stategrafik.create.form.S1 = val.currentTarget.value)}
Submit required
</Button> />
</Stack>
</Paper> <Group justify="right" mt="md">
</Box> <Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
); );
} }

View File

@@ -1,59 +1,62 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { Box, Button, Center, Flex, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts'; import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur'; import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur';
function GrafikBerdasarkanPendidikan() { function GrafikBerdasarkanPendidikan() {
const [search, setSearch] = useState("") const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
<Stack gap={"xs"}>
<HeaderSearch <HeaderSearch
title='Detail Data Pengangguran Berdasarkan Pendidikan' title='Detail Data Pengangguran Berdasarkan Pendidikan'
placeholder='pencarian' placeholder='Cari data pendidikan...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListGrafikBerdasarkanPendidikan search={search}/> <ListGrafikBerdasarkanPendidikan search={search} />
</Stack>
</Box> </Box>
); );
} }
function ListGrafikBerdasarkanPendidikan({search}: {search: string}) { function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan) const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan);
const [donutData, setDonutData] = useState<any[]>([]); const [donutData, setDonutData] = useState<any[]>([]);
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter(); const router = useRouter();
const handleDelete = async () => { const handleDelete = async () => {
if (selectedId) { if (selectedId) {
await stategrafik.delete.byId(selectedId) await stategrafik.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
stategrafik.findMany.load();
stategrafik.findMany.load()
} }
} };
const {
data,
page,
totalPages,
loading,
load,
} = stategrafik.findMany;
useShallowEffect(() => { useShallowEffect(() => {
setMounted(true); setMounted(true);
stategrafik.findMany.load() load(page, 10, search);
}, []); }, [page, search]);
useEffect(() => { useEffect(() => {
if (stategrafik.findMany.data) { if (stategrafik.findMany.data) {
@@ -70,36 +73,37 @@ function ListGrafikBerdasarkanPendidikan({search}: {search: string}) {
{ name: 'S1', value: S1, color: '#1018A8FF', key: 'S1' }, { name: 'S1', value: S1, color: '#1018A8FF', key: 'S1' },
]); ]);
} }
}, [stategrafik.findMany.data]) }, [stategrafik.findMany.data]);
const filteredData = (stategrafik.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
if (loading || !data) {
return ( return (
item.SD.toString().toLowerCase().includes(keyword) || <Stack py={10}>
item.SMP.toString().toLowerCase().includes(keyword) || <Skeleton height={500} radius="md" />
item.SMA.toString().toLowerCase().includes(keyword) || </Stack>
item.D3.toString().toLowerCase().includes(keyword) ||
item.S1.toString().toLowerCase().includes(keyword)
); );
});
if (!stategrafik.findMany.data) {
return (
<Box>
<Skeleton h={500} />
</Box>
)
} }
return ( return (
<Box> <Box py={10}>
<Stack gap={"xs"}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Paper bg={colors['white-1']} p={"md"}> {/* Header */}
<JudulList <Flex justify="space-between" align="center" mb="md">
title='List Grafik Pengangguran Berdasarkan Pendidikan' <Title order={4}>List Pengangguran Berdasarkan Usia Kerja</Title>
href='/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create' <Tooltip label="Tambah Data" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Flex>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>SD</TableTh> <TableTh>SD</TableTh>
@@ -114,8 +118,10 @@ function ListGrafikBerdasarkanPendidikan({search}: {search: string}) {
<TableTbody> <TableTbody>
{filteredData.length === 0 ? ( {filteredData.length === 0 ? (
<TableTr> <TableTr>
<TableTd colSpan={6}> <TableTd colSpan={7}>
<Text ta='center' c='dimmed'>Belum ada data grafik responden</Text> <Center py={20}>
<Text color="dimmed">Belum ada data grafik responden</Text>
</Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
) : ( ) : (
@@ -127,82 +133,87 @@ function ListGrafikBerdasarkanPendidikan({search}: {search: string}) {
<TableTd>{item.D3}</TableTd> <TableTd>{item.D3}</TableTd>
<TableTd>{item.S1}</TableTd> <TableTd>{item.S1}</TableTd>
<TableTd> <TableTd>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/${item.id}`)}> <Tooltip label="Edit Data" withArrow>
<IconEdit size={20} /> <Button color="green" variant="light" onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/${item.id}`)}>
</Button> <IconEdit size={18} />
</Button>
</Tooltip>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Tooltip label="Hapus Data" withArrow>
color='red' <Button
disabled={stategrafik.delete.loading} color="red"
onClick={() => { variant="light"
setSelectedId(item.id) disabled={stategrafik.delete.loading}
setModalHapus(true) onClick={() => {
}}> setSelectedId(item.id);
<IconTrash size={20} /> setModalHapus(true);
</Button> }}>
<IconTrash size={18} />
</Button>
</Tooltip>
</TableTd> </TableTd>
</TableTr> </TableTr>
)) ))
)} )}
</TableTbody> </TableTbody>
</Table> </Table>
</Paper> </Box>
</Paper>
{/* Chart */} <Center>
<Box> <Pagination
<Paper bg={colors['white-1']} p={'md'}> value={page}
<Stack> onChange={(newPage) => {
<Title pb={10} order={3}>Grafik Pengangguran Berdasarkan Pendidikan</Title> load(newPage, 10);
{mounted && donutData.length > 0 ? (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}> window.scrollTo({ top: 0, behavior: 'smooth' });
<PieChart }}
width={800} height={300} total={totalPages}
data={donutData} mt="md"
> mb="md"
color="blue"
radius="md"
/>
</Center>
{/* Chart */}
<Box mt="md">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack>
<Title order={3} pb={10}>Grafik Pengangguran Berdasarkan Pendidikan</Title>
{mounted && donutData.length > 0 ? (
<Box style={{ width: '100%', minHeight: 250 }}>
<PieChart width={800} height={300} data={donutData}>
<Pie <Pie
dataKey="value" dataKey="value"
nameKey="name" nameKey="name"
data={donutData} data={donutData}
cx={500} cx={400}
cy={150} cy={150}
innerRadius={60} innerRadius={60}
outerRadius={115} outerRadius={115}
label={true} label
> >
{donutData.map((entry, index) => ( {donutData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} /> <Cell key={`cell-${index}`} fill={entry.color} />
))} ))}
</Pie> </Pie>
</PieChart> </PieChart>
<Flex gap={"md"} align={"center"}> <Stack gap="xs" mt="sm">
<Box bg={colors['blue-button']} w={20} h={20} /> {donutData.map((entry) => (
<Text>SD : {donutData.find((entry) => entry.name === 'SD')?.value}</Text> <Flex key={entry.key} gap="sm" align="center">
</Flex> <Box w={20} h={20} bg={entry.color} />
<Flex gap={"md"} align={"center"}> <Text>{entry.name} : {entry.value}</Text>
<Box bg={'#10A85AFF'} w={20} h={20} /> </Flex>
<Text>SMP : {donutData.find((entry) => entry.name === 'SMP')?.value}</Text> ))}
</Flex> </Stack>
<Flex gap={"md"} align={"center"}>
<Box bg={'#C07B13FF'} w={20} h={20} />
<Text>SMA : {donutData.find((entry) => entry.name === 'SMA')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#1094A8FF'} w={20} h={20} />
<Text>D3 : {donutData.find((entry) => entry.name === 'D3')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#A83610FF'} w={20} h={20} />
<Text>S1 : {donutData.find((entry) => entry.name === 'S1')?.value}</Text>
</Flex>
</Box> </Box>
) : ( ) : (
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text> <Text color="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
)} )}
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>
</Stack>
{/* Modal Konfirmasi Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus

View File

@@ -1,98 +1,130 @@
'use client'
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client';
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, TextInput, Title } from '@mantine/core'; import { Box, Button, Paper, Stack, TextInput, Title, Group, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { toast } from 'react-toastify';
function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() { function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() {
const router = useRouter() const router = useRouter();
const params = useParams() as { id: string } const params = useParams() as { id: string };
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur) const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur);
const id = params.id const id = params.id;
useEffect(() => { useEffect(() => {
if(id){ if (id) {
stategrafik.findUnique.load(id).then(() => { stategrafik.findUnique.load(id).then(() => {
const data = stategrafik.findUnique.data const data = stategrafik.findUnique.data;
if(data){ if (data) {
stategrafik.update.form = { stategrafik.update.form = {
usia18_25: data.usia18_25 || '', usia18_25: data.usia18_25 || '',
usia26_35: data.usia26_35 || '', usia26_35: data.usia26_35 || '',
usia36_45: data.usia36_45 || '', usia36_45: data.usia36_45 || '',
usia46_keatas: data.usia46_keatas || '', usia46_keatas: data.usia46_keatas || '',
} };
} }
}) });
} }
}, [id]) }, [id]);
const handleSubmit = async () => { const handleSubmit = async () => {
stategrafik.update.id = id; try {
await stategrafik.update.submit(); stategrafik.update.id = id;
router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia') await stategrafik.update.submit();
} toast.success('Data grafik berhasil diperbarui!');
router.push(
'/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia'
);
} catch (error) {
console.error(error);
toast.error('Terjadi kesalahan saat memperbarui data grafik');
}
};
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack size={20} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Grafik Pengangguran Berdasarkan Usia Kerja
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Usia 18 - 25"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.update.form.usia18_25}
onChange={(val) => {
stategrafik.update.form.usia18_25 = val.currentTarget.value;
}}
required
/>
<TextInput
label="Usia 26 - 35"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.update.form.usia26_35}
onChange={(val) => {
stategrafik.update.form.usia26_35 = val.currentTarget.value;
}}
required
/>
<TextInput
label="Usia 36 - 45"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.update.form.usia36_45}
onChange={(val) => {
stategrafik.update.form.usia36_45 = val.currentTarget.value;
}}
required
/>
<TextInput
label="Usia 46 +"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.update.form.usia46_keatas}
onChange={(val) => {
stategrafik.update.form.usia46_keatas = val.currentTarget.value;
}}
required
/>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box> </Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Edit Grafik Pengangguran Berdasarkan Usia Kerja</Title>
<TextInput
label="Usia 18 - 25"
type='number'
placeholder="masukkan jumlah"
value={stategrafik.update.form.usia18_25}
onChange={(val) => {
stategrafik.update.form.usia18_25 = val.currentTarget.value;
}}
/>
<TextInput
label="Usia 26 - 35"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.update.form.usia26_35}
onChange={(val) => {
stategrafik.update.form.usia26_35 = val.currentTarget.value;
}}
/>
<TextInput
label="Usia 36 - 45"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.update.form.usia36_45}
onChange={(val) => {
stategrafik.update.form.usia36_45 = val.currentTarget.value;
}}
/>
<TextInput
label="Usia 46 +"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.update.form.usia46_keatas}
onChange={(val) => {
stategrafik.update.form.usia46_keatas = val.currentTarget.value;
}}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Submit
</Button>
</Stack>
</Paper>
</Box>
); );
} }

View File

@@ -1,31 +1,29 @@
'use client'
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react'; 'use client';
import { useRouter } from 'next/navigation';
import grafikBerdasarkanJenisKelamin from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanJenisKelamin';
import { useProxy } from 'valtio/utils';
import { useState } from 'react';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
import React, { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput, Group, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() { function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() {
const router = useRouter(); const router = useRouter();
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur) const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur);
const [donutData, setDonutData] = useState<any[]>([]); const [donutData, setDonutData] = useState<any[]>([]);
const resetForm = () => { const resetForm = () => {
stategrafik.create.form = { stategrafik.create.form = {
...stategrafik.create.form, ...stategrafik.create.form,
usia18_25: "", usia18_25: '',
usia26_35: "", usia26_35: '',
usia36_45: "", usia36_45: '',
usia46_keatas: "", usia46_keatas: '',
} };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
const id = await stategrafik.create.create(); const id = await stategrafik.create.create();
@@ -37,64 +35,84 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() {
} }
} }
resetForm(); resetForm();
router.push("/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia") router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia');
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack size={20} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Box> <IconArrowBack color={colors['blue-button']} size={24} />
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}> </Button>
<Stack> </Tooltip>
<Title order={3}>Create Grafik Pengangguran Berdasarkan Usia Kerja</Title> <Title order={4} ml="sm" c="dark">
<TextInput Tambah Data Pengangguran Berdasarkan Usia
label="Usia 18 - 25" </Title>
type='number' </Group>
placeholder="masukkan jumlah"
value={stategrafik.create.form.usia18_25} {/* Form Paper */}
onChange={(val) => { <Paper
stategrafik.create.form.usia18_25 = val.currentTarget.value; w={{ base: '100%', md: '50%' }}
}} bg={colors['white-1']}
/> p="lg"
<TextInput radius="md"
label="Usia 26 - 35" shadow="sm"
type="number" style={{ border: '1px solid #e0e0e0' }}
placeholder="masukkan jumlah" >
value={stategrafik.create.form.usia26_35} <Stack gap="md">
onChange={(val) => { <TextInput
stategrafik.create.form.usia26_35 = val.currentTarget.value; label="Usia 18 - 25"
}} type="number"
/> placeholder="Masukkan jumlah"
<TextInput value={stategrafik.create.form.usia18_25}
label="Usia 36 - 45" onChange={(val) => (stategrafik.create.form.usia18_25 = val.currentTarget.value)}
type="number" required
placeholder="masukkan jumlah" />
value={stategrafik.create.form.usia36_45} <TextInput
onChange={(val) => { label="Usia 26 - 35"
stategrafik.create.form.usia36_45 = val.currentTarget.value; type="number"
}} placeholder="Masukkan jumlah"
/> value={stategrafik.create.form.usia26_35}
<TextInput onChange={(val) => (stategrafik.create.form.usia26_35 = val.currentTarget.value)}
label="Usia 46 +" required
type="number" />
placeholder="masukkan jumlah" <TextInput
value={stategrafik.create.form.usia46_keatas} label="Usia 36 - 45"
onChange={(val) => { type="number"
stategrafik.create.form.usia46_keatas = val.currentTarget.value; placeholder="Masukkan jumlah"
}} value={stategrafik.create.form.usia36_45}
/> onChange={(val) => (stategrafik.create.form.usia36_45 = val.currentTarget.value)}
<Button required
mt={10} />
bg={colors['blue-button']} <TextInput
onClick={handleSubmit} label="Usia 46 +"
> type="number"
Submit placeholder="Masukkan jumlah"
</Button> value={stategrafik.create.form.usia46_keatas}
</Stack> onChange={(val) => (stategrafik.create.form.usia46_keatas = val.currentTarget.value)}
</Paper> required
</Box> />
{/* Submit Button */}
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
); );
} }

View File

@@ -1,60 +1,62 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { Box, Button, Center, Flex, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts'; import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur'; import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur';
function GrafikBerdasarkanUsiaKerjaYangMenganggur() { function GrafikBerdasarkanUsiaKerjaYangMenganggur() {
const [search, setSearch] = useState("") const [search, setSearch] = useState('');
return ( return (
<Box> <Box>
<Stack gap={"xs"}>
<HeaderSearch <HeaderSearch
title='Detail Data Pengangguran' title='Detail Data Pengangguran'
placeholder='pencarian' placeholder='Cari usia...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListGrafikBerdasarkanUsiaKerjaYangMenganggur search={search} /> <ListGrafikBerdasarkanUsiaKerjaYangMenganggur search={search} />
</Stack>
</Box> </Box>
); );
} }
function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({search}: {search: string}) { function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: string }) {
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur) const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur);
const [donutData, setDonutData] = useState<any[]>([]); const [donutData, setDonutData] = useState<any[]>([]);
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter(); const router = useRouter();
const handleDelete = async () => { const handleDelete = async () => {
if (selectedId) { if (selectedId) {
await stategrafik.delete.byId(selectedId) await stategrafik.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
stategrafik.findMany.load();
stategrafik.findMany.load()
} }
} };
const {
data,
page,
totalPages,
loading,
load,
} = stategrafik.findMany;
useShallowEffect(() => { useShallowEffect(() => {
setMounted(true); setMounted(true);
stategrafik.findMany.load() load(page, 10, search)
}, []); }, [page, search]);
useEffect(() => { useEffect(() => {
if (stategrafik.findMany.data) { if (stategrafik.findMany.data) {
@@ -69,139 +71,149 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({search}: {search: string}
{ name: 'usia46_keatas', value: totalUsia46_keatas, color: '#1094A8FF', key: 'usia46_keatas' }, { name: 'usia46_keatas', value: totalUsia46_keatas, color: '#1094A8FF', key: 'usia46_keatas' },
]); ]);
} }
}, [stategrafik.findMany.data]) }, [stategrafik.findMany.data]);
const filteredData = (stategrafik.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
if (loading || !data) {
return ( return (
item.usia18_25.toString().toLowerCase().includes(keyword) || <Stack py={10}>
item.usia26_35.toString().toLowerCase().includes(keyword) || <Skeleton height={500} radius="md" />
item.usia36_45.toString().toLowerCase().includes(keyword) || </Stack>
item.usia46_keatas.toString().toLowerCase().includes(keyword)
); );
});
if (!stategrafik.findMany.data) {
return (
<Box>
<Skeleton h={500} />
</Box>
)
} }
return ( return (
<Box> <Box py={10}>
<Stack gap={"xs"}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Paper bg={colors['white-1']} p={"md"}> <Stack>
<JudulList {/* Header */}
title='List Pengangguran Berdasarkan Usia Kerja' <Flex justify="space-between" align="center" mb="md">
href='/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create' <Title order={4}>List Pengangguran Berdasarkan Usia Kerja</Title>
/> <Tooltip label="Tambah Data" withArrow>
<Table striped withTableBorder withRowBorders> <Button
<TableThead> leftSection={<IconPlus size={18} />}
<TableTr> color="blue"
<TableTh>Usia 18-25</TableTh> variant="light"
<TableTh>Usia 26-35</TableTh> onClick={() => router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create')}
<TableTh>Usia 36-45</TableTh> >
<TableTh>Usia 46 +</TableTh> Tambah Baru
<TableTh>Edit</TableTh> </Button>
<TableTh>Delete</TableTh> </Tooltip>
</TableTr> </Flex>
</TableThead>
<TableTbody> {/* Table */}
{filteredData.length === 0 ? ( <Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr> <TableTr>
<TableTd colSpan={6}> <TableTh>Usia 18-25</TableTh>
<Text ta='center' c='dimmed'>Belum ada data grafik responden</Text> <TableTh>Usia 26-35</TableTh>
</TableTd> <TableTh>Usia 36-45</TableTh>
<TableTh>Usia 46 +</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr> </TableTr>
) : ( </TableThead>
filteredData.map((item) => ( <TableTbody>
<TableTr key={item.id}> {filteredData.length > 0 ? (
<TableTd>{item.usia18_25}</TableTd> filteredData.map(item => (
<TableTd>{item.usia26_35}</TableTd> <TableTr key={item.id}>
<TableTd>{item.usia36_45}</TableTd> <TableTd>{item.usia18_25}</TableTd>
<TableTd>{item.usia46_keatas}</TableTd> <TableTd>{item.usia26_35}</TableTd>
<TableTd> <TableTd>{item.usia36_45}</TableTd>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/${item.id}`)}> <TableTd>{item.usia46_keatas}</TableTd>
<IconEdit size={20} /> <TableTd>
</Button> <Button color="green" onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/${item.id}`)}>
</TableTd> <IconEdit size={20} />
<TableTd> </Button>
<Button </TableTd>
color='red' <TableTd>
disabled={stategrafik.delete.loading} <Button color="red" disabled={stategrafik.delete.loading} onClick={() => { setSelectedId(item.id); setModalHapus(true); }}>
onClick={() => { <IconTrash size={20} />
setSelectedId(item.id) </Button>
setModalHapus(true) </TableTd>
}}> </TableTr>
<IconTrash size={20} /> ))
</Button> ) : (
<TableTr>
<TableTd colSpan={6}>
<Center py={20}>
<Text color="dimmed">Belum ada data grafik responden</Text>
</Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
)) )}
)} </TableTbody>
</TableTbody> </Table>
</Box>
</Table> </Stack>
</Paper> </Paper>
{/* Chart */} <Center>
<Box> <Pagination
<Paper bg={colors['white-1']} p={'md'}> value={page}
<Stack> onChange={(newPage) => {
<Title pb={10} order={3}>Grafik Pengangguran Berdasarkan Usia Kerja</Title> load(newPage, 10);
{mounted && donutData.length > 0 ? (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}> window.scrollTo({ top: 0, behavior: 'smooth' });
<PieChart }}
width={800} height={300} total={totalPages}
data={donutData} mt="md"
> mb="md"
<Pie color="blue"
dataKey="value" radius="md"
nameKey="name" />
data={donutData} </Center>
cx={500}
cy={150} {/* Chart */}
innerRadius={60} <Paper bg={colors['white-1']} p="md" mt="lg" withBorder radius="md">
outerRadius={115} <Stack>
label={true} <Title order={3} pb={10}>Grafik Pengangguran Berdasarkan Usia Kerja</Title>
> {mounted && donutData.length > 0 ? (
{donutData.map((entry, index) => ( <Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
<Cell key={`cell-${index}`} fill={entry.color} /> <PieChart width={800} height={300} data={donutData}>
))} <Pie dataKey="value" nameKey="name" data={donutData} cx={400} cy={150} innerRadius={60} outerRadius={115} label>
</Pie> {donutData.map((entry, index) => (
</PieChart> <Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
</PieChart>
<Stack mt="sm" gap="xs">
<Flex gap={"md"} align={"center"}> <Flex gap={"md"} align={"center"}>
<Box bg={colors['blue-button']} w={20} h={20} /> <Box bg={colors['blue-button']} w={20} h={20} />
<Text>Usia 18-25 : {donutData.find((entry) => entry.name === 'usia18_25')?.value}</Text> <Text>Usia 18-25 : {donutData.find((entry) => entry.name === 'usia18_25')?.value}</Text>
</Flex> </Flex>
<Flex gap={"md"} align={"center"}> <Flex gap={"md"} align={"center"}>
<Box bg={'#10A85AFF'} w={20} h={20} /> <Box bg={'#10A85AFF'} w={20} h={20} />
<Text>Usia 26-35 : {donutData.find((entry) => entry.name === 'usia26_35')?.value}</Text> <Text>Usia 26-35 : {donutData.find((entry) => entry.name === 'usia26_35')?.value}
</Text>
</Flex> </Flex>
<Flex gap={"md"} align={"center"}> <Flex gap={"md"} align={"center"}>
<Box bg={'#C07B13FF'} w={20} h={20} /> <Box bg={'#C07B13FF'} w={20} h={20} />
<Text>Usia 36-45 : {donutData.find((entry) => entry.name === 'usia36_45')?.value}</Text> <Text>Usia 36-45 : {donutData.find((entry) => entry.name === 'usia36_45')?.value}
</Text>
</Flex> </Flex>
<Flex gap={"md"} align={"center"}> <Flex gap={"md"} align={"center"}>
<Box bg={'#1094A8FF'} w={20} h={20} /> <Box bg={'#1094A8FF'} w={20} h={20} />
<Text>Usia 46 + : {donutData.find((entry) => entry.name === 'usia46_keatas')?.value}</Text> <Text>Usia 46 + : {donutData.find((entry) => entry.name === 'usia46_keatas')?.value}
</Text>
</Flex> </Flex>
</Box> </Stack>
) : ( </Box>
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text> ) : (
)} <Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
</Stack> )}
</Paper> </Stack>
</Box> </Paper>
</Stack>
{/* Modal Konfirmasi Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleDelete} onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus grafik pengangguran berdasarkan usia kerja ini?' text="Apakah anda yakin ingin menghapus grafik pengangguran berdasarkan usia kerja ini?"
/> />
</Box> </Box>
); );

View File

@@ -1,8 +1,8 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client';
import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran'; import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Select, NumberInput } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -10,52 +10,54 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditDetailDataPengangguran() { function EditDetailDataPengangguran() {
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran) const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran);
const router = useRouter(); const router = useRouter();
const params = useParams() const params = useParams();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
month: stateDetail.update.form.month, month: '',
year: stateDetail.update.form.year, year: new Date().getFullYear(),
totalUnemployment: stateDetail.update.form.totalUnemployment, educatedUnemployment: 0,
educatedUnemployment: stateDetail.update.form.educatedUnemployment, uneducatedUnemployment: 0,
uneducatedUnemployment: stateDetail.update.form.uneducatedUnemployment, totalUnemployment: 0,
percentageChange: stateDetail.update.form.percentageChange || 0, // Ensure it's always a number percentageChange: 0,
}) });
// Hitung total & perubahan otomatis
const calculateTotalAndChange = async () => { const calculateTotalAndChange = async () => {
const total = formData.educatedUnemployment + formData.uneducatedUnemployment; const total = formData.educatedUnemployment + formData.uneducatedUnemployment;
// Ambil data bulan sebelumnya
const monthOrder = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'];
const currentIndex = monthOrder.findIndex(
(m) => m.toLowerCase() === formData.month.toLowerCase()
);
let percentageChange = 0; let percentageChange = 0;
if (currentIndex > 0) { const monthOrder = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'];
const prevMonth = monthOrder[currentIndex - 1]; const currentMonthIndex = monthOrder.indexOf(formData.month);
const prev = await stateDetail.findByMonthYear.load({
month: prevMonth,
year: formData.year,
});
if (prev?.totalUnemployment) { if (currentMonthIndex !== -1) {
percentageChange = Number( let prevMonthIndex = currentMonthIndex - 1;
(((total - prev.totalUnemployment) / prev.totalUnemployment) * 100).toFixed(1) let prevYear = formData.year;
);
if (prevMonthIndex < 0) {
prevMonthIndex = 11;
prevYear--;
}
const prevMonth = monthOrder[prevMonthIndex];
const prevData = await stateDetail.findByMonthYear.load({ month: prevMonth, year: prevYear });
if (prevData && prevData.totalUnemployment > 0) {
const change = ((total - prevData.totalUnemployment) / prevData.totalUnemployment) * 100;
percentageChange = parseFloat(change.toFixed(1));
} }
} }
setFormData({
...formData,
totalUnemployment: total,
percentageChange,
});
return { total, percentageChange }; return { total, percentageChange };
}; };
const updateFormData = async (updates: Partial<typeof formData>) => {
const newData = { ...formData, ...updates };
const { total, percentageChange } = await calculateTotalAndChange();
setFormData({ ...newData, totalUnemployment: total, percentageChange });
};
useEffect(() => { useEffect(() => {
const loadDetail = async () => { const loadDetail = async () => {
const id = params?.id as string; const id = params?.id as string;
@@ -66,8 +68,11 @@ function EditDetailDataPengangguran() {
const data = stateDetail.findUnique.data; const data = stateDetail.findUnique.data;
if (data) { if (data) {
// Convert year from Date to number
const yearValue = data.year instanceof Date ? data.year.getFullYear() : data.year; // Convert year from Date to number if needed
const yearValue = data.year && typeof data.year === 'object' && 'getFullYear' in data.year
? (data.year as Date).getFullYear()
: Number(data.year);
// Set the ID for update // Set the ID for update
stateDetail.update.id = id; stateDetail.update.id = id;
@@ -98,86 +103,72 @@ function EditDetailDataPengangguran() {
loadDetail(); loadDetail();
}, [params?.id]); }, [params?.id]);
const handleSubmit = async () => { const handleSubmit = async () => {
const { total, percentageChange } = await calculateTotalAndChange(); const { total, percentageChange } = await calculateTotalAndChange();
try { try {
stateDetail.update.form = { stateDetail.update.form = { ...formData, totalUnemployment: total, percentageChange };
...formData,
totalUnemployment: total,
percentageChange,
};
const success = await stateDetail.update.submit(); const success = await stateDetail.update.submit();
if (success) { if (success) {
toast.success("Detail data pengangguran berhasil diperbarui!"); toast.success('Detail data pengangguran berhasil diperbarui!');
router.push("/admin/ekonomi/jumlah-pengangguran/detail-data-pengangguran"); router.push('/admin/ekonomi/jumlah-pengangguran');
} }
} catch (error) { } catch (error) {
console.error("Error updating:", error); console.error('Error updating:', error);
toast.error("Terjadi kesalahan saat memperbarui data"); toast.error('Terjadi kesalahan saat memperbarui data');
} }
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Box> <Title order={4} ml="sm">
Edit Detail Data Pengangguran
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p="lg" radius="md" shadow="sm" style={{ border: '1px solid #e0e0e0' }}>
<Title order={4}>Edit Detail Data Pengangguran</Title> <Stack gap="md">
<Stack gap="xs"> <Select
<TextInput
label="Bulan" label="Bulan"
data={['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des']}
value={formData.month} value={formData.month}
placeholder="Contoh: Jan, Feb, Mar" onChange={(val) => updateFormData({ month: val || '' })}
onChange={(val) => (setFormData({
...formData,
month: val.currentTarget.value
}))}
/> />
<TextInput <NumberInput
label="Tahun" label="Tahun"
type="number"
value={formData.year} value={formData.year}
onChange={(val) => (setFormData({ onChange={(val) => updateFormData({ year: Number(val) })}
...formData,
year: Number(val.currentTarget.value)
}))}
/> />
<TextInput <TextInput
label="Pengangguran Terdidik" label="Pengangguran Terdidik"
type="number" type="number"
value={formData.educatedUnemployment} value={formData.educatedUnemployment}
onChange={(val) => (setFormData({ onChange={(val) => updateFormData({ educatedUnemployment: Number(val.currentTarget.value) || 0 })}
...formData,
educatedUnemployment: Number(val.currentTarget.value)
}))}
/> />
<TextInput <TextInput
label="Pengangguran Tidak Terdidik" label="Pengangguran Tidak Terdidik"
type="number" type="number"
value={formData.uneducatedUnemployment} value={formData.uneducatedUnemployment}
onChange={(val) => (setFormData({ onChange={(val) => updateFormData({ uneducatedUnemployment: Number(val.currentTarget.value) || 0 })}
...formData,
uneducatedUnemployment: Number(val.currentTarget.value)
}))}
/> />
<Text fz="sm" fw={500}> <Text fz="sm" fw={500}>Total Otomatis: {formData.totalUnemployment}</Text>
Total Otomatis: {formData.totalUnemployment} <Text fz="sm" fw={500}>Perubahan Otomatis: {formData.percentageChange !== null ? `${formData.percentageChange}%` : '-'}</Text>
</Text>
<Text fz="sm" fw={500}> <Group justify="right">
Perubahan Otomatis:{" "} <Button
{formData.percentageChange !== null onClick={handleSubmit}
? `${formData.percentageChange}%` radius="md"
: '-'} size="md"
</Text> style={{
<Group> background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
<Button bg={colors['blue-button']} mt={10} onClick={handleSubmit}> color: '#fff',
Submit boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
@@ -187,3 +178,4 @@ function EditDetailDataPengangguran() {
} }
export default EditDetailDataPengangguran; export default EditDetailDataPengangguran;

View File

@@ -2,7 +2,7 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran'; import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Flex, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -11,86 +11,125 @@ import { useProxy } from 'valtio/utils';
function DetailJumlahPengangguran() { function DetailJumlahPengangguran() {
const router = useRouter(); const router = useRouter();
const params = useParams() const params = useParams();
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran) const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran);
useShallowEffect(() => { useShallowEffect(() => {
stateDetail.findUnique.load(params?.id as string) stateDetail.findUnique.load(params?.id as string);
}, [params?.id]) }, [params?.id]);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
stateDetail.delete.byId(selectedId) stateDetail.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/ekonomi/jumlah-pengangguran") router.push("/admin/ekonomi/jumlah-pengangguran");
} }
} };
if (!stateDetail.findUnique.data) { if (!stateDetail.findUnique.data) {
return ( return (
<Box> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Box> </Stack>
) );
} }
const data = stateDetail.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> {/* Tombol Kembali */}
<Button variant="subtle" onClick={() => router.back()}> <Button
<IconArrowBack color={colors['blue-button']} size={25} /> variant="subtle"
</Button> onClick={() => router.back()}
</Box> leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}> mb={15}
<Stack> >
<Text fz={"xl"} fw={"bold"}>Detail Data Pengangguran</Text> Kembali
<Paper bg={colors['BG-trans']} p={'md'}> </Button>
<Stack gap={"xs"}>
{/* Paper Detail */}
<Paper
withBorder
w={{ base: "100%", md: "60%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Data Pengangguran
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box> <Box>
<Text fw={"bold"}>Pengangguran Terdidik</Text> <Text fw="bold">Pengangguran Terdidik</Text>
<Text>{stateDetail.findUnique.data?.educatedUnemployment}</Text> <Text c="dimmed">{data.educatedUnemployment || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fw={"bold"}>Pengangguran Tidak Terdidik</Text> <Text fw="bold">Pengangguran Tidak Terdidik</Text>
<Text>{stateDetail.findUnique.data?.uneducatedUnemployment}</Text> <Text c="dimmed">{data.uneducatedUnemployment || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fw={"bold"}>Perubahan</Text> <Text fw="bold">Perubahan</Text>
<Text>{stateDetail.findUnique.data?.percentageChange}</Text> <Text c="dimmed">
{data.percentageChange !== null && data.percentageChange !== undefined
? `${data.percentageChange}%`
: 'Tidak ada data perubahan'}
</Text>
</Box> </Box>
<Box> <Box>
<Text fw={"bold"}>Tahun</Text> <Text fw="bold">Tahun</Text>
<Text>{stateDetail.findUnique.data?.year ? new Date(stateDetail.findUnique.data.year).getFullYear() : ''}</Text> <Text c="dimmed">{data.year || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fw={"bold"}>Bulan</Text> <Text fw="bold">Bulan</Text>
<Text>{stateDetail.findUnique.data?.month}</Text> <Text c="dimmed">{data.month || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fw={"bold"}>Total Pengangguran</Text> <Text fw="bold">Total Pengangguran</Text>
<Text>{stateDetail.findUnique.data?.totalUnemployment}</Text> <Text c="dimmed">{data.totalUnemployment || '-'}</Text>
</Box> </Box>
<Box>
<Flex gap={"xs"}> {/* Tombol Edit & Hapus */}
<Flex gap="sm">
<Tooltip label="Hapus Data Pengangguran" withArrow position="top">
<Button <Button
onClick={() => { onClick={() => {
if (stateDetail.findUnique.data) { setSelectedId(data.id);
setSelectedId(stateDetail.findUnique.data.id);
setModalHapus(true); setModalHapus(true);
} }}
}} color="red"
disabled={stateDetail.delete.loading || !stateDetail.findUnique.data} variant="light"
color={"red"}> radius="md"
size="md"
>
<IconX size={20} /> <IconX size={20} />
</Button> </Button>
<Button onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/${stateDetail.findUnique.data?.id}/edit`)} color="green"> </Tooltip>
<Tooltip label="Edit Data Pengangguran" withArrow position="top">
<Button
onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/${data.id}/edit`)}
color="green"
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Box> </Flex>
</Stack> </Stack>
</Paper> </Paper>
</Stack> </Stack>
@@ -101,7 +140,7 @@ function DetailJumlahPengangguran() {
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus data ini?" text="Apakah Anda yakin ingin menghapus data ini?"
/> />
</Box> </Box>
); );

View File

@@ -3,27 +3,43 @@
'use client' 'use client'
import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran'; import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
Text,
NumberInput,
Title,
Select,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreateJumlahPengangguran() { function CreateJumlahPengangguran() {
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran) const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran);
const [chartData, setChartData] = useState<any[]>([]); const [chartData, setChartData] = useState<any[]>([]);
const router = useRouter(); const router = useRouter();
const monthOptions = [
'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun',
'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'
];
const resetForm = () => { const resetForm = () => {
stateDetail.create.form = { stateDetail.create.form = {
month: "", month: monthOptions[new Date().getMonth()], // default bulan sekarang
year: 0, year: new Date().getFullYear(), // default tahun sekarang
totalUnemployment: 0, totalUnemployment: 0,
educatedUnemployment: 0, educatedUnemployment: 0,
uneducatedUnemployment: 0, uneducatedUnemployment: 0,
percentageChange: 0, percentageChange: 0,
} };
} };
const calculateTotalAndChange = async () => { const calculateTotalAndChange = async () => {
const total = const total =
@@ -32,11 +48,8 @@ function CreateJumlahPengangguran() {
stateDetail.create.form.totalUnemployment = total; stateDetail.create.form.totalUnemployment = total;
// Ambil data bulan sebelumnya // hitung perubahan dibanding bulan sebelumnya
const monthOrder = [ const monthOrder = monthOptions;
'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun',
'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'
];
const currentIndex = monthOrder.findIndex( const currentIndex = monthOrder.findIndex(
(m) => m.toLowerCase() === stateDetail.create.form.month.toLowerCase() (m) => m.toLowerCase() === stateDetail.create.form.month.toLowerCase()
); );
@@ -68,66 +81,117 @@ function CreateJumlahPengangguran() {
setChartData([stateDetail.findUnique.data]); setChartData([stateDetail.findUnique.data]);
} }
resetForm(); resetForm();
router.push('/admin/ekonomi/jumlah-pengangguran/detail-data-pengangguran'); router.push('/admin/ekonomi/jumlah-pengangguran');
} }
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25}/> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button
</Box> variant="subtle"
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> onClick={() => router.back()}
<Title order={4}>Tambah Detail Data Pengangguran</Title> p="xs"
<Stack gap="xs"> radius="md"
<TextInput >
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Data Pengangguran
</Title>
</Group>
{/* Form Card */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Select
label="Bulan" label="Bulan"
placeholder="Pilih bulan"
data={monthOptions}
value={stateDetail.create.form.month} value={stateDetail.create.form.month}
placeholder="Contoh: Jan, Feb, Mar" onChange={(value) => {
onChange={(e) => (stateDetail.create.form.month = e.currentTarget.value)} stateDetail.create.form.month = value || '';
calculateTotalAndChange();
}}
required
/> />
<TextInput
<NumberInput
label="Tahun" label="Tahun"
type="date"
value={stateDetail.create.form.year} value={stateDetail.create.form.year}
onChange={(e) => onChange={(value) => {
(stateDetail.create.form.year = Number(e.currentTarget.value)) stateDetail.create.form.year = Number(value) || new Date().getFullYear();
} calculateTotalAndChange();
}}
min={2000}
max={2100}
required
/> />
<TextInput
<NumberInput
label="Pengangguran Terdidik" label="Pengangguran Terdidik"
type="number"
value={stateDetail.create.form.educatedUnemployment} value={stateDetail.create.form.educatedUnemployment}
onChange={(e) => { onChange={(value) => {
stateDetail.create.form.educatedUnemployment = Number( stateDetail.create.form.educatedUnemployment = Number(value) || 0;
e.currentTarget.value, calculateTotalAndChange();
);
}} }}
min={0}
required
/> />
<TextInput
<NumberInput
label="Pengangguran Tidak Terdidik" label="Pengangguran Tidak Terdidik"
type="number"
value={stateDetail.create.form.uneducatedUnemployment} value={stateDetail.create.form.uneducatedUnemployment}
onChange={(e) => { onChange={(value) => {
stateDetail.create.form.uneducatedUnemployment = Number( stateDetail.create.form.uneducatedUnemployment = Number(value) || 0;
e.currentTarget.value, calculateTotalAndChange();
);
}} }}
min={0}
required
/> />
<Text fz="sm" fw={500}>
Total Otomatis: {stateDetail.create.form.totalUnemployment} <Box>
</Text> <Text fz="sm" fw={500} mb={4}>
<Text fz="sm" fw={500}> Total Otomatis:
Perubahan Otomatis:{" "} </Text>
{stateDetail.create.form.percentageChange !== null <Text fz="sm" c="dimmed">
? `${stateDetail.create.form.percentageChange}%` {stateDetail.create.form.totalUnemployment.toLocaleString()}
: '-'} </Text>
</Text> </Box>
<Group>
<Button bg={colors['blue-button']} mt={10} onClick={handleSubmit}> <Box>
Submit <Text fz="sm" fw={500} mb={4}>
Perubahan Otomatis:
</Text>
<Text fz="sm" c="dimmed">
{stateDetail.create.form.percentageChange.toFixed(1)}%
</Text>
</Box>
{/* Action Button */}
<Group justify="right" mt="md">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
disabled={!stateDetail.create.form.month || !stateDetail.create.form.year}
>
Simpan
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -1,150 +1,181 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import {
Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack,
Table, TableTbody, TableTd, TableTh, TableThead, TableTr,
Text, Title, Tooltip
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { BarChart } from '@mantine/charts'; import { BarChart } from '@mantine/charts';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import jumlahPengangguranState from '../../_state/ekonomi/jumlah-pengangguran'; import jumlahPengangguranState from '../../_state/ekonomi/jumlah-pengangguran';
function DetailDataPengangguran() { function DetailDataPengangguran() {
const [search, setSearch] = useState("") const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
<Stack gap={"xs"}>
<HeaderSearch <HeaderSearch
title='Detail Data Pengangguran' title='Detail Data Pengangguran'
placeholder='pencarian' placeholder='Cari bulan atau tahun...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListDetailDataPengangguran search={search} /> <ListDetailDataPengangguran search={search} />
</Stack>
</Box> </Box>
); );
} }
function ListDetailDataPengangguran({search}: {search: string}) { function ListDetailDataPengangguran({ search }: { search: string }) {
const [chartData, setChartData] = useState<any[]>([]);
type DetailDataPengangguran = { const [mounted, setMounted] = useState(false);
id: string; const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran);
month: string;
year: number;
educatedUnemployment: number;
uneducatedUnemployment: number;
percentageChange: number;
totalUnemployment: number;
}
const [chartData, setChartData] = useState<DetailDataPengangguran[]>([]);
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran)
const router = useRouter(); const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = stateDetail.findMany;
useShallowEffect(() => { useShallowEffect(() => {
setMounted(true) setMounted(true);
stateDetail.findMany.load() load(page, 10, search);
}, []) }, [page, search]);
useEffect(() => { useEffect(() => {
setMounted(true); if (data) {
if (stateDetail.findMany.data) { setChartData(
setChartData(stateDetail.findMany.data.map((item) => ({ data.map((item) => ({
id: item.id, id: item.id,
month: item.month, month: item.month,
year: item.year instanceof Date ? item.year.getFullYear() : Number(item.year), year: Number(item.year),
educatedUnemployment: Number(item.educatedUnemployment), educatedUnemployment: Number(item.educatedUnemployment),
uneducatedUnemployment: Number(item.uneducatedUnemployment), uneducatedUnemployment: Number(item.uneducatedUnemployment),
percentageChange: Number(item.percentageChange), percentageChange: Number(item.percentageChange),
totalUnemployment: Number(item.totalUnemployment), totalUnemployment: Number(item.totalUnemployment),
}))); }))
);
} }
}, [stateDetail.findMany.data]); }, [data]);
const filteredData = (stateDetail.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
// Loading state
if (loading || !data) {
return ( return (
item.month.toLowerCase().includes(keyword) || <Stack py="md">
item.year.toString().toLowerCase().includes(keyword) <Skeleton h={500} radius="md" />
</Stack>
); );
});
if (!stateDetail.findMany.data) {
return (
<Box>
<Skeleton h={500} />
</Box>
)
} }
return ( return (
<Box> <Stack py="md" gap="lg">
<Stack gap={"md"}> {/* Table Section */}
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Detail Data Pengangguran' <Title order={4}>Daftar Detail Data Pengangguran</Title>
href='/admin/ekonomi/jumlah-pengangguran/create' <Tooltip label="Tambah Data Baru" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/jumlah-pengangguran/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover striped withTableBorder withRowBorders>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Bulan</TableTh> <TableTh style={{ width: '25%' }}>Bulan</TableTh>
<TableTh>Terdidik</TableTh> <TableTh style={{ width: '20%' }}>Terdidik</TableTh>
<TableTh>Tidak Terdidik</TableTh> <TableTh style={{ width: '20%' }}>Tidak Terdidik</TableTh>
<TableTh>Detail</TableTh> <TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? (
<TableTr key={item.id}> filteredData.map((item) => (
<TableTd>{item.month}</TableTd> <TableTr key={item.id}>
<TableTd>{item.educatedUnemployment}</TableTd> <TableTd>{item.month} {item.year}</TableTd>
<TableTd>{item.uneducatedUnemployment}</TableTd> <TableTd>{item.educatedUnemployment}</TableTd>
<TableTd> <TableTd>{item.uneducatedUnemployment}</TableTd>
<Button onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/${item.id}`)}> <TableTd>
<IconDeviceImac size={20} /> <Button
</Button> variant="light"
color="blue"
onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/${item.id}`)}
>
<IconDeviceImac size={20} />
<Text ml={5}>Detail</Text>
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text c="dimmed">Tidak ada data yang cocok</Text>
</Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} )}
</TableTbody> </TableTbody>
</Table> </Table>
</Paper> </Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
{/* Chart */} {/* Chart Section */}
{!mounted && !chartData ? ( <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}> <Title order={4} mb="md">
<Paper bg={colors['white-1']} p={'md'}> Data Pengangguran Terdidik & Tidak Terdidik
<Title pb={10} order={3}>Data Pengangguran Terdidik dan Tidak Terdidik</Title> </Title>
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text> {mounted && chartData.length > 0 ? (
</Paper> <Box w={{ base: '100%', md: '70%' }}>
<BarChart
h={450}
data={chartData}
dataKey="month"
series={[
{ name: 'educatedUnemployment', color: 'red.6', label: 'Terdidik' },
{ name: 'uneducatedUnemployment', color: 'orange.6', label: 'Tidak Terdidik' },
]}
/>
</Box> </Box>
) : ( ) : (
<Box style={{ width: '100%', minWidth: 300, height: 550, minHeight: 300 }}> <Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={4}>Data Pengangguran Terdidik dan Tidak Terdidik</Title>
{mounted && chartData.length > 0 && (
<Box w={{ base: '100%', md: '70%' }}>
<BarChart
h={450}
data={chartData}
dataKey="month"
series={[
{ name: 'educatedUnemployment', color: 'red.6', label: 'Terdidik' },
{ name: 'uneducatedUnemployment', color: 'orange.6', label: 'Tidak Terdidik' },
]}
/>
</Box>
)}
</Paper>
</Box>
)} )}
</Stack> </Paper>
</Box> </Stack>
); );
} }

View File

@@ -3,16 +3,25 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import lowonganKerjaState from '@/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja'; import lowonganKerjaState from '@/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditLowonganKerja() { function EditLowonganKerja() {
const lowonganState = useProxy(lowonganKerjaState) const lowonganState = useProxy(lowonganKerjaState);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
@@ -24,7 +33,7 @@ function EditLowonganKerja() {
gaji: lowonganKerjaState.update.form.gaji, gaji: lowonganKerjaState.update.form.gaji,
deskripsi: lowonganKerjaState.update.form.deskripsi, deskripsi: lowonganKerjaState.update.form.deskripsi,
kualifikasi: lowonganKerjaState.update.form.kualifikasi, kualifikasi: lowonganKerjaState.update.form.kualifikasi,
}) });
useEffect(() => { useEffect(() => {
const loadLowongan = async () => { const loadLowongan = async () => {
@@ -32,7 +41,7 @@ function EditLowonganKerja() {
if (!id) return; if (!id) return;
try { try {
const data = await lowonganState.update.load(id); // akses langsung, bukan dari proxy const data = await lowonganState.update.load(id);
if (data) { if (data) {
setFormData({ setFormData({
posisi: data.posisi || '', posisi: data.posisi || '',
@@ -51,20 +60,17 @@ function EditLowonganKerja() {
}; };
loadLowongan(); loadLowongan();
}, [params?.id]) }, [params?.id]);
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
// Set the ID for the update
lowonganState.update.id = params?.id as string; lowonganState.update.id = params?.id as string;
// Update the form state
lowonganState.update.form = { lowonganState.update.form = {
...lowonganState.update.form, ...lowonganState.update.form,
...formData ...formData,
}; };
// Call the update function
await lowonganState.update.update(); await lowonganState.update.update();
toast.success("Lowongan kerja berhasil diperbarui!"); toast.success("Lowongan kerja berhasil diperbarui!");
router.push("/admin/ekonomi/lowongan-kerja-lokal"); router.push("/admin/ekonomi/lowongan-kerja-lokal");
@@ -72,79 +78,105 @@ function EditLowonganKerja() {
console.error("Error updating lowongan kerja:", error); console.error("Error updating lowongan kerja:", error);
toast.error("Terjadi kesalahan saat memperbarui lowongan kerja"); toast.error("Terjadi kesalahan saat memperbarui lowongan kerja");
} }
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header dengan tombol back */}
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Box> <IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Lowongan Kerja Lokal
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> {/* Card Form */}
<Stack gap={"xs"}> <Paper
<Title order={4}>Edit Lowongan Kerja Lokal</Title> w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label="Posisi"
placeholder="Masukkan posisi"
value={formData.posisi} value={formData.posisi}
onChange={(val) => { onChange={(e) => setFormData({ ...formData, posisi: e.target.value })}
setFormData(prev => ({ ...prev, posisi: val.target.value })); required
}}
label={<Text fw={"bold"} fz={"sm"}>Posisi</Text>}
placeholder='Masukkan posisi'
/> />
<TextInput <TextInput
label="Nama Perusahaan"
placeholder="Masukkan nama perusahaan"
value={formData.namaPerusahaan} value={formData.namaPerusahaan}
onChange={(val) => { onChange={(e) => setFormData({ ...formData, namaPerusahaan: e.target.value })}
setFormData(prev => ({ ...prev, namaPerusahaan: val.target.value })); required
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Perusahaan</Text>}
placeholder='Masukkan nama perusahaan'
/> />
<TextInput <TextInput
label="Lokasi"
placeholder="Masukkan lokasi"
value={formData.lokasi} value={formData.lokasi}
onChange={(val) => { onChange={(e) => setFormData({ ...formData, lokasi: e.target.value })}
setFormData(prev => ({ ...prev, lokasi: val.target.value })); required
}}
label={<Text fw={"bold"} fz={"sm"}>Lokasi</Text>}
placeholder='Masukkan lokasi'
/> />
<TextInput <TextInput
label="Tipe Pekerjaan"
placeholder="Masukkan tipe pekerjaan"
value={formData.tipePekerjaan} value={formData.tipePekerjaan}
onChange={(val) => { onChange={(e) => setFormData({ ...formData, tipePekerjaan: e.target.value })}
setFormData(prev => ({ ...prev, tipePekerjaan: val.target.value })); required
}}
label={<Text fw={"bold"} fz={"sm"}>Tipe Pekerjaan</Text>}
placeholder='Masukkan tipe pekerjaan'
/> />
<TextInput <TextInput
label="Gaji (per bulan)"
placeholder="Masukkan gaji"
value={formData.gaji} value={formData.gaji}
onChange={(val) => { onChange={(e) => setFormData({ ...formData, gaji: e.target.value })}
setFormData(prev => ({ ...prev, gaji: val.target.value })); required
}}
label={<Text fw={"bold"} fz={"sm"}>Gaji selama 1 bulan</Text>}
placeholder='Masukkan gaji'
/> />
<Box> <Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Lowongan Kerja</Text> <Text fw="bold" fz="sm" mb={6}>
Deskripsi Lowongan Kerja
</Text>
<EditEditor <EditEditor
value={formData.deskripsi} value={formData.deskripsi}
onChange={(val) => { onChange={(val) => setFormData({ ...formData, deskripsi: val })}
setFormData(prev => ({ ...prev, deskripsi: val }));
}}
/> />
</Box> </Box>
<Box> <Box>
<Text fw={"bold"} fz={"sm"}>Kualifikasi Lowongan Kerja</Text> <Text fw="bold" fz="sm" mb={6}>
Kualifikasi Lowongan Kerja
</Text>
<EditEditor <EditEditor
value={formData.kualifikasi} value={formData.kualifikasi}
onChange={(val) => { onChange={(val) => setFormData({ ...formData, kualifikasi: val })}
setFormData(prev => ({ ...prev, kualifikasi: val }));
}}
/> />
</Box> </Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button> <Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,125 +1,145 @@
'use client' 'use client'
import { useProxy } from 'valtio/utils';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import lowonganKerjaState from '../../../_state/ekonomi/lowongan-kerja'; import lowonganKerjaState from '../../../_state/ekonomi/lowongan-kerja';
function DetailLowonganKerjaLokal() { function DetailLowonganKerjaLokal() {
const lowonganState = useProxy(lowonganKerjaState) const lowonganState = useProxy(lowonganKerjaState);
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams() const params = useParams();
const router = useRouter() const router = useRouter();
useShallowEffect(() => { useShallowEffect(() => {
lowonganState.findUnique.load(params?.id as string) lowonganState.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
lowonganState.delete.byId(selectedId) lowonganState.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/ekonomi/lowongan-kerja-lokal") router.push("/admin/ekonomi/lowongan-kerja-lokal");
} }
} };
if (!lowonganState.findUnique.data) { if (!lowonganState.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={40} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = lowonganState.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> <Button
<Button variant="subtle" onClick={() => router.back()}> variant="subtle"
<IconArrowBack color={colors['blue-button']} size={25} /> onClick={() => router.back()}
</Button> leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
</Box> mb={15}
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}> >
<Stack> Kembali
<Text fz={"xl"} fw={"bold"}>Detail Lowongan Kerja Lokal</Text> </Button>
{lowonganState.findUnique.data ? (
<Paper key={lowonganState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}> <Paper
<Stack gap={"xs"}> withBorder
<Box> w={{ base: "100%", md: "70%" }}
<Text fw={"bold"} fz={"lg"}>Bekerja Sebagai</Text> bg={colors['white-1']}
<Text fz={"lg"}>{lowonganState.findUnique.data?.posisi}</Text> p="lg"
</Box> radius="md"
<Box> shadow="sm"
<Text fw={"bold"} fz={"lg"}>Nama Usaha</Text> >
<Text fz={"lg"}>{lowonganState.findUnique.data?.namaPerusahaan}</Text> <Stack gap="md">
</Box> <Text fz="2xl" fw="bold" c={colors['blue-button']}>
<Box> Detail Lowongan Kerja Lokal
<Text fw={"bold"} fz={"lg"}>Lokasi</Text> </Text>
<Text fz={"lg"}>{lowonganState.findUnique.data?.lokasi}</Text>
</Box> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Box> <Stack gap="sm">
<Text fw={"bold"} fz={"lg"}>Tipe Pekerjaan</Text> <Box>
<Text fz={"lg"}>{lowonganState.findUnique.data?.tipePekerjaan}</Text> <Text fz="lg" fw="bold">Bekerja Sebagai</Text>
</Box> <Text fz="md" c="dimmed">{data.posisi || '-'}</Text>
<Box> </Box>
<Text fw={"bold"} fz={"lg"}>Gaji</Text>
<Text fz={"lg"}>{lowonganState.findUnique.data?.gaji}</Text> <Box>
</Box> <Text fz="lg" fw="bold">Nama Usaha</Text>
<Box> <Text fz="md" c="dimmed">{data.namaPerusahaan || '-'}</Text>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text> </Box>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: lowonganState.findUnique.data?.deskripsi }} />
</Box> <Box>
<Box> <Text fz="lg" fw="bold">Lokasi</Text>
<Text fw={"bold"} fz={"lg"}>Kualifikasi</Text> <Text fz="md" c="dimmed">{data.lokasi || '-'}</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: lowonganState.findUnique.data?.kualifikasi }} /> </Box>
</Box>
<Flex gap={"xs"} mt={10}> <Box>
<Text fz="lg" fw="bold">Tipe Pekerjaan</Text>
<Text fz="md" c="dimmed">{data.tipePekerjaan || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Gaji</Text>
<Text fz="md" c="dimmed">{data.gaji || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }} />
</Box>
<Box>
<Text fz="lg" fw="bold">Kualifikasi</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.kualifikasi || '-' }} />
</Box>
<Group gap="sm" mt="sm">
<Tooltip label="Hapus Lowongan" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (lowonganState.findUnique.data) { setSelectedId(data.id);
setSelectedId(lowonganState.findUnique.data.id); setModalHapus(true);
setModalHapus(true);
}
}} }}
disabled={lowonganState.delete.loading || !lowonganState.findUnique.data} variant="light"
color={"red"} radius="md"
size="md"
> >
<IconX size={20} /> <IconTrash size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Lowongan" withArrow position="top">
<Button <Button
onClick={() => { color="green"
if (lowonganState.findUnique.data) { onClick={() => router.push(`/admin/ekonomi/lowongan-kerja-lokal/${data.id}/edit`)}
router.push(`/admin/ekonomi/lowongan-kerja-lokal/${lowonganState.findUnique.data.id}/edit`); variant="light"
} radius="md"
}} size="md"
disabled={!lowonganState.findUnique.data}
color={"green"}
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Stack> </Group>
</Paper> </Stack>
) : null} </Paper>
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus lowongan kerja ini?' text="Apakah Anda yakin ingin menghapus lowongan kerja ini?"
/> />
</Box> </Box>
); );

View File

@@ -1,88 +1,123 @@
'use client' 'use client';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor'; import CreateEditor from '../../../_com/createEditor';
import lowonganKerjaState from '../../../_state/ekonomi/lowongan-kerja'; import lowonganKerjaState from '../../../_state/ekonomi/lowongan-kerja';
function CreateLowonganKerja() { function CreateLowonganKerja() {
const lowonganState = useProxy(lowonganKerjaState) const lowonganState = useProxy(lowonganKerjaState);
const router = useRouter(); const router = useRouter();
const resetForm = () => { const resetForm = () => {
lowonganState.create.form = { lowonganState.create.form = {
posisi: "", posisi: '',
namaPerusahaan: "", namaPerusahaan: '',
lokasi: "", lokasi: '',
tipePekerjaan: "", tipePekerjaan: '',
gaji: "", gaji: '',
deskripsi: "", deskripsi: '',
kualifikasi: "", kualifikasi: '',
} };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
await lowonganState.create.create() await lowonganState.create.create();
resetForm() resetForm();
router.push("/admin/ekonomi/lowongan-kerja-lokal") router.push('/admin/ekonomi/lowongan-kerja-lokal');
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header dengan tombol kembali */}
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25}/> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button
</Box> variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Lowongan Kerja Lokal
</Title>
</Group>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}> {/* Card Form */}
<Stack gap={"xs"}> <Paper
<Title order={4}>Create Lowongan Kerja Lokal</Title> w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
value={lowonganState.create.form.posisi} value={lowonganState.create.form.posisi}
onChange={(val) => { onChange={(val) =>
lowonganState.create.form.posisi = val.target.value; (lowonganState.create.form.posisi = val.target.value)
}} }
label={<Text fw={"bold"} fz={"sm"}>Posisi</Text>} label="Posisi"
placeholder='Masukkan posisi' placeholder="Masukkan posisi"
required
/> />
<TextInput <TextInput
value={lowonganState.create.form.namaPerusahaan} value={lowonganState.create.form.namaPerusahaan}
onChange={(val) => { onChange={(val) =>
lowonganState.create.form.namaPerusahaan = val.target.value; (lowonganState.create.form.namaPerusahaan = val.target.value)
}} }
label={<Text fw={"bold"} fz={"sm"}>Nama Perusahaan</Text>} label="Nama Perusahaan"
placeholder='Masukkan nama perusahaan' placeholder="Masukkan nama perusahaan"
required
/> />
<TextInput <TextInput
value={lowonganState.create.form.lokasi} value={lowonganState.create.form.lokasi}
onChange={(val) => { onChange={(val) =>
lowonganState.create.form.lokasi = val.target.value; (lowonganState.create.form.lokasi = val.target.value)
}} }
label={<Text fw={"bold"} fz={"sm"}>Lokasi</Text>} label="Lokasi"
placeholder='Masukkan lokasi' placeholder="Masukkan lokasi"
required
/> />
<TextInput <TextInput
value={lowonganState.create.form.tipePekerjaan} value={lowonganState.create.form.tipePekerjaan}
onChange={(val) => { onChange={(val) =>
lowonganState.create.form.tipePekerjaan = val.target.value; (lowonganState.create.form.tipePekerjaan = val.target.value)
}} }
label={<Text fw={"bold"} fz={"sm"}>Tipe Pekerjaan</Text>} label="Tipe Pekerjaan"
placeholder='Masukkan tipe pekerjaan' placeholder="Masukkan tipe pekerjaan"
required
/> />
<TextInput <TextInput
value={lowonganState.create.form.gaji} value={lowonganState.create.form.gaji}
onChange={(val) => { onChange={(val) =>
lowonganState.create.form.gaji = val.target.value; (lowonganState.create.form.gaji = val.target.value)
}} }
label={<Text fw={"bold"} fz={"sm"}>Gaji selama 1 bulan</Text>} label="Gaji (per bulan)"
placeholder='Masukkan gaji' placeholder="Masukkan gaji"
required
/> />
<Box> <Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Lowongan Kerja</Text> <Text fw="bold" fz="sm" mb={6}>
Deskripsi Lowongan Kerja
</Text>
<CreateEditor <CreateEditor
value={lowonganState.create.form.deskripsi} value={lowonganState.create.form.deskripsi}
onChange={(val) => { onChange={(val) => {
@@ -90,8 +125,11 @@ function CreateLowonganKerja() {
}} }}
/> />
</Box> </Box>
<Box> <Box>
<Text fw={"bold"} fz={"sm"}>Kualifikasi Lowongan Kerja</Text> <Text fw="bold" fz="sm" mb={6}>
Kualifikasi Lowongan Kerja
</Text>
<CreateEditor <CreateEditor
value={lowonganState.create.form.kualifikasi} value={lowonganState.create.form.kualifikasi}
onChange={(val) => { onChange={(val) => {
@@ -99,12 +137,25 @@ function CreateLowonganKerja() {
}} }}
/> />
</Box> </Box>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> {/* Tombol Simpan */}
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>
); );
} }

View File

@@ -1,22 +1,40 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import {
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; Box,
import HeaderSearch from '../../_com/header'; Button,
import JudulList from '../../_com/judulList'; Center,
import { useRouter } from 'next/navigation'; Group,
import lowonganKerjaState from '../../_state/ekonomi/lowongan-kerja'; Pagination,
import { useProxy } from 'valtio/utils'; Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import lowonganKerjaState from '../../_state/ekonomi/lowongan-kerja';
function LowonganKerjaLokal() { function LowonganKerjaLokal() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Lowongan Kerja Lokal' title="Lowongan Kerja Lokal"
placeholder='pencarian' placeholder="Cari pekerjaan atau perusahaan..."
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,74 +45,117 @@ function LowonganKerjaLokal() {
} }
function ListLowonganKerjaLokal({ search }: { search: string }) { function ListLowonganKerjaLokal({ search }: { search: string }) {
const lowonganState = useProxy(lowonganKerjaState) const stateLowongan = useProxy(lowonganKerjaState);
const router = useRouter(); const router = useRouter();
const { const { data, page, totalPages, loading, load } = stateLowongan.findMany;
data,
page,
totalPages,
loading,
load,
} = lowonganState.findMany
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search) load(page, 10, search);
}, [page, search]) }, [page, search]);
const filteredData = data || [] const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
) );
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Lowongan Kerja Lokal' <Title order={4}>Daftar Lowongan Kerja Lokal</Title>
href='/admin/ekonomi/lowongan-kerja-lokal/create' <Tooltip label="Tambah Lowongan Kerja" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
<TableThead> color="blue"
<TableTr> variant="light"
<TableTh>Pekerjaan</TableTh> onClick={() =>
<TableTh>Nama Perusahaan</TableTh> router.push('/admin/ekonomi/lowongan-kerja-lokal/create')
<TableTh>Lokasi</TableTh> }
<TableTh>Detail</TableTh> >
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '25%' }}>Pekerjaan</TableTh>
<TableTh style={{ width: '25%' }}>Nama Perusahaan</TableTh>
<TableTh style={{ width: '20%' }}>Lokasi</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? (
<TableTr key={item.id}> filteredData.map((item) => (
<TableTd> <TableTr key={item.id}>
<Text fz={"md"}>{item.posisi}</Text> <TableTd style={{ width: '25%' }}>
</TableTd> <Text fw={500} truncate="end" lineClamp={1}>
<TableTd> {item.posisi}
<Text fz={"md"}>{item.namaPerusahaan}</Text> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd style={{ width: '25%' }}>
<Text fz={"md"}>{item.lokasi}</Text> <Text truncate fz="sm" c="dimmed">
</TableTd> {item.namaPerusahaan}
<TableTd> </Text>
<Button onClick={() => router.push(`/admin/ekonomi/lowongan-kerja-lokal/${item.id}`)}> </TableTd>
<IconDeviceImac size={20} /> <TableTd style={{ width: '20%' }}>
</Button> <Text truncate fz="sm" c="dimmed">
</TableTd> {item.lokasi}
</TableTr> </Text>
))} </TableTd>
</TableTbody> <TableTd style={{ width: '15%' }}>
</Table> <Button
variant="light"
color="blue"
onClick={() =>
router.push(
`/admin/ekonomi/lowongan-kerja-lokal/${item.id}`
)
}
>
<IconDeviceImac size={20} />
<Text ml={5}>Detail</Text>
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">
Tidak ada data lowongan kerja yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => load(newPage)} onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages} total={totalPages}
my="md" mt="md"
mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
</Box> </Box>

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