Compare commits
14 Commits
faf78064c7
...
tasks/kegi
| Author | SHA1 | Date | |
|---|---|---|---|
| 23c955597e | |||
| 28a22e8d77 | |||
| 67efe6ce35 | |||
| b39f9b39da | |||
| 6041cdf552 | |||
| 5cd6e3aa99 | |||
| f31ab0eda5 | |||
| 0517d50c8e | |||
| fa7a52a0f3 | |||
| ef237aea2f | |||
| f6107e971d | |||
| 550961d524 | |||
| 34d49fa073 | |||
| 1631e273a4 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "desa-darmasaba",
|
||||
"version": "0.1.35",
|
||||
"version": "0.1.46",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
48
prisma/_seeder_list/desa/seed_kegiatan_desa.ts
Normal file
48
prisma/_seeder_list/desa/seed_kegiatan_desa.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const kategoriKegiatanJson = loadJsonData("desa/kegiatan-desa/kategori-kegiatan.json");
|
||||
const kegiatanDesaJson = loadJsonData("desa/kegiatan-desa/kegiatan-desa.json");
|
||||
|
||||
export async function seedKegiatanDesa() {
|
||||
console.log("🔄 Seeding Kategori Kegiatan Desa...");
|
||||
|
||||
for (const k of kategoriKegiatanJson) {
|
||||
await prisma.kategoriKegiatan.upsert({
|
||||
where: { id: k.id },
|
||||
update: { nama: k.nama },
|
||||
create: { id: k.id, nama: k.nama },
|
||||
});
|
||||
console.log(` ✅ Kategori: ${k.nama}`);
|
||||
}
|
||||
|
||||
console.log("🔄 Seeding Kegiatan Desa...");
|
||||
|
||||
for (const item of kegiatanDesaJson) {
|
||||
await prisma.kegiatanDesa.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
judul: item.judul,
|
||||
deskripsiSingkat: item.deskripsiSingkat,
|
||||
deskripsiLengkap: item.deskripsiLengkap,
|
||||
tanggal: new Date(item.tanggal),
|
||||
lokasi: item.lokasi,
|
||||
partisipan: item.partisipan,
|
||||
kategoriKegiatanId: item.kategoriKegiatanId,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
judul: item.judul,
|
||||
deskripsiSingkat: item.deskripsiSingkat,
|
||||
deskripsiLengkap: item.deskripsiLengkap,
|
||||
tanggal: new Date(item.tanggal),
|
||||
lokasi: item.lokasi,
|
||||
partisipan: item.partisipan,
|
||||
kategoriKegiatanId: item.kategoriKegiatanId,
|
||||
},
|
||||
});
|
||||
console.log(` ✅ Kegiatan: ${item.judul}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Kegiatan Desa seed selesai");
|
||||
}
|
||||
@@ -28,6 +28,7 @@ export async function seedProgramKesehatan() {
|
||||
name: p.name,
|
||||
deskripsiSingkat: p.deskripsiSingkat,
|
||||
deskripsi: p.deskripsi,
|
||||
persentase: p.persentase ?? 0,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
@@ -35,6 +36,7 @@ export async function seedProgramKesehatan() {
|
||||
name: p.name,
|
||||
deskripsiSingkat: p.deskripsiSingkat,
|
||||
deskripsi: p.deskripsi,
|
||||
persentase: p.persentase ?? 0,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
|
||||
24
prisma/_seeder_list/kesehatan/seed_ringkasan_kesehatan.ts
Normal file
24
prisma/_seeder_list/kesehatan/seed_ringkasan_kesehatan.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
const SINGLETON_ID = "ringkasan_kesehatan_desa_001";
|
||||
|
||||
export async function seedRingkasanKesehatan() {
|
||||
console.log("🔄 Seeding Ringkasan Kesehatan Desa...");
|
||||
|
||||
await prisma.ringkasanKesehatanDesa.upsert({
|
||||
where: { id: SINGLETON_ID },
|
||||
update: {
|
||||
ibuHamilAkh: 87,
|
||||
balitaTerdaftar: 342,
|
||||
alertStunting: 12,
|
||||
},
|
||||
create: {
|
||||
id: SINGLETON_ID,
|
||||
ibuHamilAkh: 87,
|
||||
balitaTerdaftar: 342,
|
||||
alertStunting: 12,
|
||||
},
|
||||
});
|
||||
|
||||
console.log("✅ Ringkasan Kesehatan Desa seeded");
|
||||
}
|
||||
22
prisma/_seeder_list/pendidikan/seed_beasiswa_config.ts
Normal file
22
prisma/_seeder_list/pendidikan/seed_beasiswa_config.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
const SINGLETON_ID = "beasiswa_config_desa_001";
|
||||
|
||||
export async function seedBeasiswaConfig() {
|
||||
console.log("🔄 Seeding Beasiswa Config...");
|
||||
|
||||
await prisma.beasiswaConfig.upsert({
|
||||
where: { id: SINGLETON_ID },
|
||||
update: {
|
||||
tahunAjaran: "2025/2026",
|
||||
danaTersalurkan: BigInt(1200000000),
|
||||
},
|
||||
create: {
|
||||
id: SINGLETON_ID,
|
||||
tahunAjaran: "2025/2026",
|
||||
danaTersalurkan: BigInt(1200000000),
|
||||
},
|
||||
});
|
||||
|
||||
console.log("✅ Beasiswa Config seeded");
|
||||
}
|
||||
22
prisma/data/desa/kegiatan-desa/kategori-kegiatan.json
Normal file
22
prisma/data/desa/kegiatan-desa/kategori-kegiatan.json
Normal file
@@ -0,0 +1,22 @@
|
||||
[
|
||||
{
|
||||
"id": "katbudaya000001",
|
||||
"nama": "Budaya"
|
||||
},
|
||||
{
|
||||
"id": "katsosia000001",
|
||||
"nama": "Sosial"
|
||||
},
|
||||
{
|
||||
"id": "katolahraga0001",
|
||||
"nama": "Olahraga"
|
||||
},
|
||||
{
|
||||
"id": "katkeagamaan01",
|
||||
"nama": "Keagamaan"
|
||||
},
|
||||
{
|
||||
"id": "katpemberday01",
|
||||
"nama": "Pemberdayaan Masyarakat"
|
||||
}
|
||||
]
|
||||
52
prisma/data/desa/kegiatan-desa/kegiatan-desa.json
Normal file
52
prisma/data/desa/kegiatan-desa/kegiatan-desa.json
Normal file
@@ -0,0 +1,52 @@
|
||||
[
|
||||
{
|
||||
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567801",
|
||||
"judul": "Hari Kesaktian Pancasila",
|
||||
"deskripsiSingkat": "Peringatan Hari Kesaktian Pancasila di Balai Desa Darmasaba sebagai momentum menguatkan nilai-nilai Pancasila dalam kehidupan bermasyarakat.",
|
||||
"deskripsiLengkap": "Pemerintah Desa Darmasaba menyelenggarakan upacara peringatan Hari Kesaktian Pancasila yang dihadiri oleh perangkat desa, tokoh masyarakat, perwakilan pemuda, dan warga desa. Kegiatan ini sebagai bentuk penghormatan atas perjuangan bangsa dan komitmen untuk terus mengamalkan nilai-nilai Pancasila dalam kehidupan sehari-hari di lingkungan desa.",
|
||||
"tanggal": "2025-10-01T00:00:00.000Z",
|
||||
"lokasi": "Balai Desa",
|
||||
"partisipan": 250,
|
||||
"kategoriKegiatanId": "katbudaya000001"
|
||||
},
|
||||
{
|
||||
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567802",
|
||||
"judul": "Festival Budaya Desa",
|
||||
"deskripsiSingkat": "Festival tahunan menampilkan kesenian dan budaya lokal Bali sebagai upaya pelestarian warisan budaya Desa Darmasaba.",
|
||||
"deskripsiLengkap": "Festival Budaya Desa Darmasaba merupakan agenda tahunan yang menampilkan berbagai pertunjukan seni tradisional Bali seperti tari Kecak, Legong, Barong, serta pameran kerajinan tangan dan kuliner khas desa. Festival ini bertujuan untuk melestarikan warisan budaya leluhur, memperkenalkan kekayaan budaya kepada generasi muda, dan meningkatkan daya tarik wisata budaya Desa Darmasaba.",
|
||||
"tanggal": "2026-05-20T00:00:00.000Z",
|
||||
"lokasi": "Lapangan Desa",
|
||||
"partisipan": 500,
|
||||
"kategoriKegiatanId": "katbudaya000001"
|
||||
},
|
||||
{
|
||||
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567803",
|
||||
"judul": "Perayaan HUT Desa",
|
||||
"deskripsiSingkat": "Perayaan hari ulang tahun Desa Darmasaba dengan berbagai kegiatan seni budaya dan olahraga yang melibatkan seluruh lapisan masyarakat.",
|
||||
"deskripsiLengkap": "Hari Ulang Tahun Desa Darmasaba dirayakan dengan serangkaian kegiatan meriah meliputi upacara adat, pertunjukan seni budaya Bali, lomba olahraga tradisional, dan pameran produk unggulan desa. Perayaan ini menjadi ajang mempererat kebersamaan warga, mengenang sejarah desa, dan merayakan pencapaian pembangunan yang telah diraih bersama.",
|
||||
"tanggal": "2026-08-17T00:00:00.000Z",
|
||||
"lokasi": "Balai Desa",
|
||||
"partisipan": 600,
|
||||
"kategoriKegiatanId": "katbudaya000001"
|
||||
},
|
||||
{
|
||||
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567804",
|
||||
"judul": "Gotong Royong Pembersihan Desa",
|
||||
"deskripsiSingkat": "Kegiatan gotong royong rutin membersihkan lingkungan desa sebagai wujud kebersamaan dan kepedulian masyarakat terhadap kebersihan.",
|
||||
"deskripsiLengkap": "Kegiatan gotong royong pembersihan lingkungan desa dilaksanakan secara rutin setiap bulan dengan melibatkan seluruh warga, kader PKK, karang taruna, dan perangkat desa. Kegiatan ini mencakup pembersihan jalan desa, sungai, tempat ibadah, dan fasilitas umum sebagai bentuk nyata kepedulian masyarakat terhadap kebersihan dan keindahan lingkungan.",
|
||||
"tanggal": "2026-03-15T00:00:00.000Z",
|
||||
"lokasi": "Seluruh Wilayah Desa",
|
||||
"partisipan": 400,
|
||||
"kategoriKegiatanId": "katsosia000001"
|
||||
},
|
||||
{
|
||||
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567805",
|
||||
"judul": "Turnamen Olahraga Antar Banjar",
|
||||
"deskripsiSingkat": "Turnamen olahraga tahunan antar banjar untuk mempererat tali persaudaraan dan mendorong gaya hidup sehat masyarakat desa.",
|
||||
"deskripsiLengkap": "Turnamen olahraga antar banjar di Desa Darmasaba menampilkan berbagai cabang olahraga seperti bola voli, bulu tangkis, tenis meja, dan sepak bola. Kegiatan ini diikuti oleh perwakilan dari setiap banjar di desa dan bertujuan untuk menumbuhkan semangat sportivitas, mempererat silaturahmi antar warga, serta mendorong pola hidup sehat melalui olahraga.",
|
||||
"tanggal": "2026-06-10T00:00:00.000Z",
|
||||
"lokasi": "Lapangan Desa",
|
||||
"partisipan": 350,
|
||||
"kategoriKegiatanId": "katolahraga0001"
|
||||
}
|
||||
]
|
||||
@@ -3,8 +3,57 @@
|
||||
"id": "cmkanjnmx0006vntz1cn7owpb",
|
||||
"name": "Posyandu Pudak Amara",
|
||||
"nomor": "(0361) 8463263",
|
||||
"deskripsi": "<p>Posyandu Pudak Amara merupakan salah satu posyandu aktif di Desa Darmasaba dan pernah berkompetisi dalam lomba kader dan posyandu berprestasi tingkat Provinsi Bali tahun 2025.</p><p>Kegiatan ini melibatkan kader posyandu serta didampingi pihak desa dan puskesmas setempat untuk meningkatkan pelayanan kesehatan ibu dan anak.</p>",
|
||||
"jadwalPelayanan": "<p>Setiap bulan pada satu hari tertentu (mis. minggu ke-2): 08:00 – 12:00 WITA (posyandu balita & ibu hamil)</p>",
|
||||
"deskripsi": "<p>Posyandu Pudak Amara merupakan salah satu posyandu aktif di Desa Darmasaba dan pernah berkompetisi dalam lomba kader dan posyandu berprestasi tingkat Provinsi Bali tahun 2025.</p>",
|
||||
"jadwalPelayanan": "Senin, 10 Feb 2026, 08:00 - 11:00 WITA",
|
||||
"imageName": "TDQReg1lQ73s39crXW0ra-mobile.webp"
|
||||
},
|
||||
{
|
||||
"id": "posyandu_mawar_001",
|
||||
"name": "Posyandu Mawar",
|
||||
"nomor": "(0361) 8463264",
|
||||
"deskripsi": "<p>Posyandu Mawar melayani kesehatan ibu dan anak di wilayah Banjar Mawar, Desa Darmasaba, dengan fokus pada pemantauan tumbuh kembang balita dan kesehatan ibu hamil.</p>",
|
||||
"jadwalPelayanan": "Senin, 15 Feb 2026, 08:00 - 11:00 WITA"
|
||||
},
|
||||
{
|
||||
"id": "posyandu_melati_001",
|
||||
"name": "Posyandu Melati",
|
||||
"nomor": "(0361) 8463265",
|
||||
"deskripsi": "<p>Posyandu Melati berperan aktif dalam pelayanan kesehatan dasar masyarakat di Banjar Melati, meliputi imunisasi, penimbangan balita, dan konsultasi gizi.</p>",
|
||||
"jadwalPelayanan": "Selasa, 16 Feb 2026, 08:00 - 11:00 WITA"
|
||||
},
|
||||
{
|
||||
"id": "posyandu_dahlia_001",
|
||||
"name": "Posyandu Dahlia",
|
||||
"nomor": "(0361) 8463266",
|
||||
"deskripsi": "<p>Posyandu Dahlia aktif melayani masyarakat Banjar Dahlia dengan program unggulan pemantauan stunting dan pemberian makanan tambahan bagi balita berisiko.</p>",
|
||||
"jadwalPelayanan": "Rabu, 17 Feb 2026, 08:00 - 11:00 WITA"
|
||||
},
|
||||
{
|
||||
"id": "posyandu_anggrek_001",
|
||||
"name": "Posyandu Anggrek",
|
||||
"nomor": "(0361) 8463267",
|
||||
"deskripsi": "<p>Posyandu Anggrek melayani ibu hamil, ibu menyusui, dan balita di wilayah Banjar Anggrek dengan dukungan tenaga kesehatan dari Puskesmas Abiansemal 3.</p>",
|
||||
"jadwalPelayanan": "Kamis, 18 Feb 2026, 08:00 - 11:00 WITA"
|
||||
},
|
||||
{
|
||||
"id": "posyandu_kamboja_001",
|
||||
"name": "Posyandu Kamboja",
|
||||
"nomor": "(0361) 8463268",
|
||||
"deskripsi": "<p>Posyandu Kamboja hadir untuk mendukung kesehatan masyarakat Banjar Kamboja melalui layanan pemeriksaan rutin, imunisasi lengkap, dan edukasi gizi keluarga.</p>",
|
||||
"jadwalPelayanan": "Jumat, 19 Feb 2026, 08:00 - 11:00 WITA"
|
||||
},
|
||||
{
|
||||
"id": "posyandu_melur_001",
|
||||
"name": "Posyandu Melur",
|
||||
"nomor": "(0361) 8463269",
|
||||
"deskripsi": "<p>Posyandu Melur aktif memberikan layanan kesehatan preventif bagi ibu dan anak di Banjar Melur, termasuk deteksi dini stunting dan pemantauan gizi balita.</p>",
|
||||
"jadwalPelayanan": "Sabtu, 20 Feb 2026, 08:00 - 11:00 WITA"
|
||||
},
|
||||
{
|
||||
"id": "posyandu_kenanga_001",
|
||||
"name": "Posyandu Kenanga",
|
||||
"nomor": "(0361) 8463270",
|
||||
"deskripsi": "<p>Posyandu Kenanga melayani masyarakat Banjar Kenanga dengan program kesehatan ibu dan anak, pemberian vitamin A, dan konseling laktasi bagi ibu menyusui.</p>",
|
||||
"jadwalPelayanan": "Senin, 23 Feb 2026, 08:00 - 11:00 WITA"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,9 +1,38 @@
|
||||
[
|
||||
{
|
||||
"id": "prog_kes_imunisasi_001",
|
||||
"name": "Imunisasi Lengkap",
|
||||
"deskripsiSingkat": "<p>Persentase balita yang mendapatkan imunisasi lengkap sesuai jadwal di Desa Darmasaba.</p>",
|
||||
"deskripsi": "<p>Program imunisasi lengkap mencakup vaksin BCG, DPT-HB-Hib, Polio, Campak, dan PCV yang diberikan kepada seluruh balita di Desa Darmasaba melalui posyandu dan puskesmas setempat untuk membangun kekebalan komunitas.</p>",
|
||||
"persentase": 92
|
||||
},
|
||||
{
|
||||
"id": "prog_kes_pemeriksaan_001",
|
||||
"name": "Pemeriksaan Rutin",
|
||||
"deskripsiSingkat": "<p>Persentase ibu hamil dan balita yang melakukan pemeriksaan kesehatan rutin secara berkala.</p>",
|
||||
"deskripsi": "<p>Pemeriksaan kesehatan rutin dilakukan setiap bulan di posyandu dan puskesmas, mencakup penimbangan berat badan, pengukuran tinggi badan, pemeriksaan tekanan darah ibu hamil, dan konsultasi gizi untuk memantau perkembangan kesehatan masyarakat.</p>",
|
||||
"persentase": 88
|
||||
},
|
||||
{
|
||||
"id": "prog_kes_gizi_001",
|
||||
"name": "Gizi Baik",
|
||||
"deskripsiSingkat": "<p>Persentase balita dengan status gizi baik berdasarkan hasil pemantauan tumbuh kembang.</p>",
|
||||
"deskripsi": "<p>Program pemantauan gizi balita dilaksanakan melalui posyandu dengan penimbangan bulanan dan pengukuran tinggi badan. Balita dengan status gizi baik menunjukkan berat dan tinggi badan sesuai standar WHO, didukung dengan program pemberian makanan tambahan bagi balita berisiko.</p>",
|
||||
"persentase": 86
|
||||
},
|
||||
{
|
||||
"id": "prog_kes_stunting_001",
|
||||
"name": "Target Stunting",
|
||||
"deskripsiSingkat": "<p>Persentase balita yang teridentifikasi berisiko stunting dan perlu penanganan khusus.</p>",
|
||||
"deskripsi": "<p>Penanganan stunting di Desa Darmasaba dilakukan melalui deteksi dini, intervensi gizi spesifik, dan intervensi gizi sensitif. Program ini melibatkan kader posyandu, bidan desa, dan puskesmas untuk memantau pertumbuhan balita secara berkala dan memberikan penanganan tepat.</p>",
|
||||
"persentase": 14
|
||||
},
|
||||
{
|
||||
"id": "cmkawkji50002vn6yzyrlqhh1",
|
||||
"name": "Gerakan Kulkul PKK dan Posyandu Desa Darmasaba",
|
||||
"deskripsiSingkat": "<p>Kegiatan bersama PKK dan Posyandu untuk meningkatkan pelayanan kesehatan masyarakat.</p>",
|
||||
"deskripsi": "<p>Pada hari Minggu, 11 Januari 2025, Pemerintah Desa Darmasaba melalui TP PKK dan TP Posyandu melaksanakan kegiatan Gerakan Kulkul PKK dan Posyandu yang berlangsung serentak di seluruh wilayah Desa Darmasaba untuk memperkuat pelayanan kesehatan dasar dan peningkatan partisipasi masyarakat dalam program Posyandu.</p>",
|
||||
"persentase": 0,
|
||||
"imageName": "hLeF0GRFZqDUngZnDMAAk-mobile.webp"
|
||||
},
|
||||
{
|
||||
@@ -11,6 +40,7 @@
|
||||
"name": "Pendampingan Kunjungan Rumah oleh Puskesmas Abiansemal 3",
|
||||
"deskripsiSingkat": "<p>Pendataan kesehatan penyandang disabilitas lewat kunjungan rumah di Desa Darmasaba.</p>",
|
||||
"deskripsi": "<p>Pemerintah Desa Darmasaba bersama Kelian Banjar Dinas dan kader kesehatan mendampingi kegiatan kunjungan rumah yang dilaksanakan oleh Puskesmas Abiansemal 3 pada 21 Juli 2025, difokuskan pada pendataan dan pemantauan kondisi kesehatan penyandang disabilitas di Banjar Bersih, Desa Darmasaba.</p>",
|
||||
"persentase": 0,
|
||||
"imageName": "hyyTFi8EApjzFEZ9EvJgB-mobile.webp"
|
||||
},
|
||||
{
|
||||
@@ -18,6 +48,7 @@
|
||||
"name": "Kegiatan Aksi Sosial Tim Penggerak Posyandu Provinsi Bali di Desa Darmasaba",
|
||||
"deskripsiSingkat": "<p>Aksi sosial TP Posyandu Bali untuk memperkuat pelayanan posyandu di desa.</p>",
|
||||
"deskripsi": "<p>Pada 10 Desember 2025, Desa Darmasaba menjadi lokasi pelaksanaan Aksi Sosial Tim Penggerak Posyandu Provinsi Bali yang bertujuan memperkuat pelayanan Posyandu serta meningkatkan kesejahteraan masyarakat, khususnya keluarga dan balita.</p>",
|
||||
"persentase": 0,
|
||||
"imageName": "l4qsUEw2JiclGAkkrXp9g-mobile.webp"
|
||||
},
|
||||
{
|
||||
@@ -25,6 +56,7 @@
|
||||
"name": "Inovasi BAJRA dalam Penanggulangan Rabies",
|
||||
"deskripsiSingkat": "<p>Program BAJRA untuk penanggulangan rabies di Desa Darmasaba.</p>",
|
||||
"deskripsi": "<p>Desa Darmasaba mengembangkan inovasi BAJRA (Bersama Jaga Rabies), sebuah program berbasis komunitas untuk penanggulangan rabies yang mengintegrasikan pelaporan cepat masyarakat, edukasi berkelanjutan dan koordinasi lintas sektor antara kesehatan hewan, manusia, dan pemerintahan desa.</p>",
|
||||
"persentase": 0,
|
||||
"imageName": "Gc79mlIlGuoRQuTqskFj--mobile.webp"
|
||||
},
|
||||
{
|
||||
@@ -32,6 +64,7 @@
|
||||
"name": "Posyandu Pudak Amara Berkompetisi",
|
||||
"deskripsiSingkat": "<p>Partisipasi Posyandu Pudak Amara dalam lomba prestasi Posyandu tingkat provinsi.</p>",
|
||||
"deskripsi": "<p>Kader Posyandu Pudak Amara Br. Cabe mendapat pendampingan dari Perbekel Darmasaba, Dinas Kesehatan Kab. Badung, Puskesmas Abiansemal III, dan Pustu Desa Darmasaba dalam ajang lomba kader dan Posyandu berprestasi tingkat Provinsi Bali tahun 2025.</p>",
|
||||
"persentase": 0,
|
||||
"imageName": "OsMY3AYPyGC_CoN1xUjOn-mobile.webp"
|
||||
},
|
||||
{
|
||||
@@ -39,13 +72,15 @@
|
||||
"name": "Outbound Kader Posyandu Darmasaba",
|
||||
"deskripsiSingkat": "<p>Program pembinaan dan pengembangan kapasitas kader Posyandu.</p>",
|
||||
"deskripsi": "<p>Pemdes Darmasaba melaksanakan kegiatan Outbound Posyandu untuk meningkatkan kapasitas dan wawasan Kader Posyandu se-Desa Darmasaba sebagai bagian dari upaya peningkatan kualitas pelayanan kesehatan dasar di masyarakat.</p>",
|
||||
"persentase": 0,
|
||||
"imageName": "M9QlgVKIEfCdY3g4F_tRZ-mobile.webp"
|
||||
},
|
||||
{
|
||||
"id": "cmkdu8ki10004vn4lpbxm2zqo",
|
||||
"name": "PEMBANGUNAN JAMBAN BAGI MASYARAKAT",
|
||||
"deskripsiSingkat": "<p>Program pengadaan jamban bagi Masyarakat ini diharapkan menjadi stimulus agar masyarakat peduli terhadap lingkungan sehat sehingga Badung Open Defection Free atau terbebas dari buang air besar di tempat terbuka dapat terwujud.</p>",
|
||||
"deskripsi": "<p>Desa Darmasaba sebagai desa yang berkomitmen selalu selaras dengan pembangunan Pemerintah Kabupaten Badung pada tahun anggaran 2023 ini turut ambil bagian dalam menyukseskan program Bupati Badung I Nyoman Giri Prasta, S.Sos dalam bidang kesehatan sanitasi masyarakat. Program pengadaan jamban bagi Masyarakat ini diharapkan menjadi stimulus agar masyarakat peduli terhadap lingkungan sehat sehingga Badung Open Defection Free atau terbebas dari buang air besar di tempat terbuka dapat terwujud.</p><p style=\"text-align: justify\">Pemberian bantuan jamban ini dilaksanakan di 11 banjar dengan menyasar 22 keluarga yang memang belum memiliki jamban yang sumber dananya sepenuhnya dari APBDes Darmasaba T. A. 2023. Pembangunan Jamban bagi Masyarakat ini juga menjadi bukti komitmen Pemerintah Desa Darmasaba dalam melaksanakan salah satu visi mewujudkan masyarakat yang sejahtera dan berbudaya untuk menjaga lingkungan yang bersih dan sehat.</p>",
|
||||
"deskripsi": "<p>Desa Darmasaba sebagai desa yang berkomitmen selalu selaras dengan pembangunan Pemerintah Kabupaten Badung pada tahun anggaran 2023 ini turut ambil bagian dalam menyukseskan program Bupati Badung I Nyoman Giri Prasta, S.Sos dalam bidang kesehatan sanitasi masyarakat.</p>",
|
||||
"persentase": 0,
|
||||
"imageName": "6DQbAvn0St-xHdPGW3vpY-mobile.webp"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Migrate PasarDesa.kategoriProdukId FK from KategoriProduk to KategoriProdukUmkm
|
||||
ALTER TABLE "PasarDesa" DROP CONSTRAINT "PasarDesa_kategoriProdukId_fkey";
|
||||
|
||||
ALTER TABLE "PasarDesa" ADD CONSTRAINT "PasarDesa_kategoriProdukId_fkey" FOREIGN KEY ("kategoriProdukId") REFERENCES "KategoriProdukUmkm"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,12 @@
|
||||
-- Seed KategoriProdukUmkm with any KategoriProduk entries referenced by PasarDesa
|
||||
-- that are not yet in KategoriProdukUmkm
|
||||
INSERT INTO "KategoriProdukUmkm" ("id", "nama", "createdAt", "updatedAt", "deletedAt", "isActive")
|
||||
SELECT DISTINCT kp.id, kp.nama, kp."createdAt", kp."updatedAt", kp."deletedAt", kp."isActive"
|
||||
FROM "KategoriProduk" kp
|
||||
WHERE kp.id IN (
|
||||
SELECT DISTINCT "kategoriProdukId" FROM "PasarDesa"
|
||||
WHERE "kategoriProdukId" IS NOT NULL
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM "KategoriProdukUmkm" WHERE id = kp.id
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
-- Step 1: Seed KategoriProdukUmkm from KategoriProduk for any PasarDesa-referenced entries not yet present
|
||||
INSERT INTO "KategoriProdukUmkm" ("id", "nama", "createdAt", "updatedAt", "deletedAt", "isActive")
|
||||
SELECT DISTINCT kp.id, kp.nama, kp."createdAt", kp."updatedAt", kp."deletedAt", kp."isActive"
|
||||
FROM "KategoriProduk" kp
|
||||
WHERE kp.id IN (
|
||||
SELECT DISTINCT "kategoriProdukId" FROM "PasarDesa"
|
||||
WHERE "kategoriProdukId" IS NOT NULL
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM "KategoriProdukUmkm" WHERE id = kp.id
|
||||
);
|
||||
|
||||
-- Step 2: Make kategoriProdukId nullable to handle orphaned/legacy data
|
||||
ALTER TABLE "PasarDesa" ALTER COLUMN "kategoriProdukId" DROP NOT NULL;
|
||||
|
||||
-- Step 3: Null out any remaining orphaned references (not in KategoriProdukUmkm)
|
||||
UPDATE "PasarDesa"
|
||||
SET "kategoriProdukId" = NULL
|
||||
WHERE "kategoriProdukId" IS NOT NULL
|
||||
AND "kategoriProdukId" NOT IN (SELECT id FROM "KategoriProdukUmkm");
|
||||
@@ -0,0 +1,27 @@
|
||||
-- Add persentase field to ProgramKesehatan (untuk Statistik Kesehatan bar chart)
|
||||
ALTER TABLE "ProgramKesehatan" ADD COLUMN "persentase" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
-- Create BeasiswaConfig (untuk dana tersalurkan + tahun ajaran beasiswa desa)
|
||||
CREATE TABLE "BeasiswaConfig" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tahunAjaran" TEXT NOT NULL,
|
||||
"danaTersalurkan" BIGINT NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
|
||||
CONSTRAINT "BeasiswaConfig_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- Create RingkasanKesehatanDesa (untuk stat cards: ibu hamil, balita, stunting)
|
||||
CREATE TABLE "RingkasanKesehatanDesa" (
|
||||
"id" TEXT NOT NULL,
|
||||
"ibuHamilAkh" INTEGER NOT NULL DEFAULT 0,
|
||||
"balitaTerdaftar" INTEGER NOT NULL DEFAULT 0,
|
||||
"alertStunting" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
|
||||
CONSTRAINT "RingkasanKesehatanDesa_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
@@ -1213,6 +1213,7 @@ model ProgramKesehatan {
|
||||
name String
|
||||
deskripsiSingkat String
|
||||
deskripsi String
|
||||
persentase Int @default(0)
|
||||
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||
imageId String?
|
||||
createdAt DateTime @default(now())
|
||||
@@ -1445,8 +1446,8 @@ model PasarDesa {
|
||||
deletedAt DateTime?
|
||||
isActive Boolean @default(true)
|
||||
|
||||
kategoriProduk KategoriProduk @relation(fields: [kategoriProdukId], references: [id])
|
||||
kategoriProdukId String
|
||||
kategoriProduk KategoriProdukUmkm? @relation(fields: [kategoriProdukId], references: [id])
|
||||
kategoriProdukId String?
|
||||
KategoriToPasar KategoriToPasar[]
|
||||
}
|
||||
|
||||
@@ -1458,7 +1459,6 @@ model KategoriProduk {
|
||||
deletedAt DateTime?
|
||||
isActive Boolean @default(true)
|
||||
KategoriToPasar KategoriToPasar[]
|
||||
PasarDesa PasarDesa[]
|
||||
}
|
||||
|
||||
model KategoriToPasar {
|
||||
@@ -2441,13 +2441,14 @@ model Umkm {
|
||||
}
|
||||
|
||||
model KategoriProdukUmkm {
|
||||
id String @id @default(cuid())
|
||||
id String @id @default(cuid())
|
||||
nama String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime?
|
||||
isActive Boolean @default(true)
|
||||
isActive Boolean @default(true)
|
||||
Umkm Umkm[]
|
||||
PasarDesa PasarDesa[]
|
||||
}
|
||||
|
||||
|
||||
@@ -2470,3 +2471,24 @@ model PenjualanProduk {
|
||||
@@index([tanggal])
|
||||
}
|
||||
|
||||
// ========================================= BEASISWA CONFIG ========================================= //
|
||||
model BeasiswaConfig {
|
||||
id String @id @default(cuid())
|
||||
tahunAjaran String
|
||||
danaTersalurkan BigInt @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
// ========================================= RINGKASAN KESEHATAN DESA ========================================= //
|
||||
model RingkasanKesehatanDesa {
|
||||
id String @id @default(cuid())
|
||||
ibuHamilAkh Int @default(0)
|
||||
balitaTerdaftar Int @default(0)
|
||||
alertStunting Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { seedBerita } from "./_seeder_list/desa/berita/seed_berita";
|
||||
import { seedKegiatanDesa } from "./_seeder_list/desa/seed_kegiatan_desa";
|
||||
import { seedFoto } from "./_seeder_list/desa/gallery/foto/seed_foto";
|
||||
import { seedVideo } from "./_seeder_list/desa/gallery/video/seed_video";
|
||||
import { seedLayanan } from "./_seeder_list/desa/layanan/seed_layanan";
|
||||
@@ -46,6 +47,7 @@ import { seedProgramKesehatan } from "./_seeder_list/kesehatan/program-kesehatan
|
||||
import { seedPuskesmas } from "./_seeder_list/kesehatan/puskesmas/seed_puskesmas";
|
||||
import { seedGrafikKepuasan } from "./_seeder_list/kesehatan/seed_grafik_kepuasan";
|
||||
import { seedKelahiranKematian } from "./_seeder_list/kesehatan/seed_kelahiran_kematian";
|
||||
import { seedRingkasanKesehatan } from "./_seeder_list/kesehatan/seed_ringkasan_kesehatan";
|
||||
import { seedDesaAntiKorupsi } from "./_seeder_list/landing-page/desa-anti-korupsi/seed_desa_anti_korupsi";
|
||||
import { seedPrestasiDesa } from "./_seeder_list/landing-page/prestasi-desa/seed_prestasi_desa";
|
||||
import { seedMediaSosial } from "./_seeder_list/landing-page/profil_landing_page/seed_media_sosial";
|
||||
@@ -59,6 +61,7 @@ import { seedKonservasiAdatBali } from "./_seeder_list/lingkungan/seed_konservas
|
||||
import { seedPengelolaanSampah } from "./_seeder_list/lingkungan/seed_pengelolaan_sampah";
|
||||
import { seedProgramPenghijauan } from "./_seeder_list/lingkungan/seed_program_penghijauan";
|
||||
import { seedBeasiswaPendaftar } from "./_seeder_list/pendidikan/seed_beasiswa_pendaftar";
|
||||
import { seedBeasiswaConfig } from "./_seeder_list/pendidikan/seed_beasiswa_config";
|
||||
import { seedBimbinganBelajar } from "./_seeder_list/pendidikan/seed_bimbingan_belajar";
|
||||
import { seedDataPendidikan } from "./_seeder_list/pendidikan/seed_data_pendidikan";
|
||||
import { seedDataPerpustakaan } from "./_seeder_list/pendidikan/seed_data_perpustakaan";
|
||||
@@ -378,6 +381,11 @@ import seedAssets from "./seed_assets";
|
||||
// ===== PENDIDIKAN =====
|
||||
await seedKeunggulanProgram();
|
||||
await seedBeasiswaPendaftar();
|
||||
await seedBeasiswaConfig();
|
||||
|
||||
// ===== SOSIAL DASHBOARD =====
|
||||
await seedRingkasanKesehatan();
|
||||
await seedKegiatanDesa();
|
||||
|
||||
// ===== DESA =====
|
||||
await seedMusikDesa();
|
||||
|
||||
@@ -493,23 +493,31 @@ export const umkmState = proxy({
|
||||
summary: { data: null as any, loading: false },
|
||||
topProduk: { data: [] as any[], loading: false },
|
||||
detail: { data: [] as any[], loading: false },
|
||||
async loadAll(periode = "") {
|
||||
mode: "month" as "week" | "month",
|
||||
async loadAll(periode = "", mode?: "week" | "month", kategoriId = "", umkmId = "") {
|
||||
const p = periode || `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
|
||||
const m = mode ?? this.mode;
|
||||
const modeParam = m === "week" ? "&mode=week" : "";
|
||||
const detailFilter = [
|
||||
kategoriId ? `&kategoriId=${kategoriId}` : "",
|
||||
umkmId ? `&umkmId=${umkmId}` : "",
|
||||
].join("");
|
||||
this.kpi.loading = true;
|
||||
this.summary.loading = true;
|
||||
this.topProduk.loading = true;
|
||||
this.detail.loading = true;
|
||||
try {
|
||||
const [kpi, sum, top, det] = await Promise.all([
|
||||
fetch(`/api/ekonomi/umkm/dashboard/kpi?periode=${p}`).then(r => r.json()),
|
||||
fetch(`/api/ekonomi/umkm/dashboard/ringkasan-penjualan?periode=${p}`).then(r => r.json()),
|
||||
fetch(`/api/ekonomi/umkm/dashboard/top-produk?periode=${p}`).then(r => r.json()),
|
||||
fetch(`/api/ekonomi/umkm/dashboard/detail-penjualan?periode=${p}`).then(r => r.json())
|
||||
fetch(`/api/ekonomi/umkm/dashboard/kpi?periode=${p}${modeParam}`).then(r => r.json()),
|
||||
fetch(`/api/ekonomi/umkm/dashboard/ringkasan-penjualan?periode=${p}${modeParam}`).then(r => r.json()),
|
||||
fetch(`/api/ekonomi/umkm/dashboard/top-produk?periode=${p}${modeParam}`).then(r => r.json()),
|
||||
fetch(`/api/ekonomi/umkm/dashboard/detail-penjualan?periode=${p}${modeParam}${detailFilter}`).then(r => r.json()),
|
||||
]);
|
||||
if (kpi.success) this.kpi.data = kpi.data;
|
||||
if (sum.success) this.summary.data = sum.data;
|
||||
if (top.success) this.topProduk.data = top.data;
|
||||
if (det.success) this.detail.data = det.data;
|
||||
this.mode = m;
|
||||
} catch (e) { console.error(e); } finally {
|
||||
this.kpi.loading = false;
|
||||
this.summary.loading = false;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Card,
|
||||
Grid,
|
||||
Group,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Skeleton,
|
||||
Stack,
|
||||
@@ -16,10 +18,11 @@ import {
|
||||
TableTr,
|
||||
Text,
|
||||
Title,
|
||||
Badge
|
||||
SegmentedControl,
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowUpRight, IconArrowDownRight, IconMinus } from '@tabler/icons-react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import { Bar, BarChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
import umkmState from '../../../_state/ekonomi/umkm/umkm';
|
||||
@@ -27,10 +30,25 @@ import umkmState from '../../../_state/ekonomi/umkm/umkm';
|
||||
function UmkmDashboard() {
|
||||
const state = useProxy(umkmState.dashboard);
|
||||
|
||||
const [kategoriId, setKategoriId] = useState("");
|
||||
const [umkmId, setUmkmId] = useState("");
|
||||
const [kategoriList, setKategoriList] = useState<{ value: string; label: string }[]>([]);
|
||||
const [umkmList, setUmkmList] = useState<{ value: string; label: string }[]>([]);
|
||||
|
||||
useShallowEffect(() => {
|
||||
state.loadAll();
|
||||
fetch('/api/ekonomi/kategoriproduk/find-many-all').then(r => r.json()).then(res => {
|
||||
if (res.success) setKategoriList(res.data.map((k: any) => ({ value: k.id, label: k.nama })));
|
||||
});
|
||||
fetch('/api/ekonomi/umkm/find-many-all').then(r => r.json()).then(res => {
|
||||
if (res.success) setUmkmList(res.data.map((u: any) => ({ value: u.id, label: u.nama })));
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.kpi.data) state.loadAll("", state.mode, kategoriId, umkmId);
|
||||
}, [kategoriId, umkmId]);
|
||||
|
||||
if (state.kpi.loading || !state.kpi.data) {
|
||||
return <Skeleton height={400} radius="md" />;
|
||||
}
|
||||
@@ -42,15 +60,31 @@ function UmkmDashboard() {
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<Group justify="space-between">
|
||||
<Title order={4}>Update Penjualan Produk</Title>
|
||||
<SegmentedControl
|
||||
value={state.mode}
|
||||
onChange={(v) => state.loadAll("", v as "week" | "month", kategoriId, umkmId)}
|
||||
data={[
|
||||
{ label: 'Minggu ini', value: 'week' },
|
||||
{ label: 'Bulan ini', value: 'month' },
|
||||
]}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 4 }}>
|
||||
<KpiCard title="UMKM Aktif" value={kpi.umkmAktif} subValue={`Total: ${kpi.totalUmkm}`} />
|
||||
<KpiCard
|
||||
title="Omzet Bulan Ini"
|
||||
value={`Rp ${kpi.omzetBulanan.toLocaleString()}`}
|
||||
trend={summary?.persentasePerubahan}
|
||||
<KpiCard
|
||||
title="Omzet Bulan Ini"
|
||||
value={`Rp ${kpi.omzetBulanan.toLocaleString()}`}
|
||||
trend={summary?.persentasePerubahan}
|
||||
/>
|
||||
<KpiCard title="Kategori Aktif" value={summary?.kategoriAktif || 0} subValue="kategori" />
|
||||
<KpiCard
|
||||
title="UMKM Terbanyak"
|
||||
value={kpi.jumlahKategoriTerbanyak || 0}
|
||||
subValue={kpi.kategoriTerbanyak}
|
||||
/>
|
||||
<KpiCard title="Produk Aktif" value={summary?.produkAktif || 0} />
|
||||
<KpiCard title="Kategori Populer" value={kpi.kategoriTerbanyak} />
|
||||
</SimpleGrid>
|
||||
|
||||
<Grid>
|
||||
@@ -59,7 +93,7 @@ function UmkmDashboard() {
|
||||
<Title order={4} mb="md">Grafik Penjualan per Produk</Title>
|
||||
<Box style={{ height: 350 }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={detail.map(item => ({
|
||||
<BarChart data={detail.map((item: any) => ({
|
||||
name: item.namaProduk,
|
||||
penjualan: item.penjualanBulanIni
|
||||
}))}>
|
||||
@@ -76,16 +110,21 @@ function UmkmDashboard() {
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={{ base: 12, md: 4 }}>
|
||||
<Card h={"100%"} withBorder radius="md" p="lg" shadow="sm">
|
||||
<Title order={4} mb="md">Top 3 Produk</Title>
|
||||
<Card h="100%" withBorder radius="md" p="lg" shadow="sm">
|
||||
<Title order={4} mb="md">Top 3 Produk Terlaris</Title>
|
||||
<Stack gap="sm">
|
||||
{topProduk.map((item, i) => (
|
||||
<Group key={i} justify="space-between">
|
||||
{topProduk.map((item: any, i: number) => (
|
||||
<Group key={i} justify="space-between" wrap="nowrap">
|
||||
<Box>
|
||||
<Text fw={500}>{item.namaProduk}</Text>
|
||||
<Text fw={500} size="sm">{item.namaProduk}</Text>
|
||||
<Text size="xs" c="dimmed">{item.namaUmkm}</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
Rp {item.totalPenjualan.toLocaleString()} · {item.jumlahTerjual} terjual
|
||||
</Text>
|
||||
</Box>
|
||||
<Text fw={600} c="blue">Rp {item.totalPenjualan.toLocaleString()}</Text>
|
||||
<Badge color={item.growth >= 0 ? 'teal' : 'red'} variant="light" size="sm">
|
||||
{item.growth >= 0 ? '+' : ''}{item.growth}%
|
||||
</Badge>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
@@ -94,24 +133,62 @@ function UmkmDashboard() {
|
||||
|
||||
<Grid.Col span={{ base: 12, md: 8 }}>
|
||||
<Card withBorder radius="md" p="lg" shadow="sm">
|
||||
<Title order={4} mb="md">Detail Penjualan & Stok</Title>
|
||||
<Group justify="space-between" mb="md" wrap="wrap" gap="sm">
|
||||
<Title order={4}>Detail Penjualan Produk</Title>
|
||||
<Group gap="sm">
|
||||
<Select
|
||||
placeholder="Semua Kategori"
|
||||
data={kategoriList}
|
||||
value={kategoriId || null}
|
||||
onChange={(v) => setKategoriId(v || "")}
|
||||
clearable
|
||||
size="xs"
|
||||
style={{ minWidth: 140 }}
|
||||
/>
|
||||
<Select
|
||||
placeholder="Semua UMKM"
|
||||
data={umkmList}
|
||||
value={umkmId || null}
|
||||
onChange={(v) => setUmkmId(v || "")}
|
||||
clearable
|
||||
size="xs"
|
||||
style={{ minWidth: 140 }}
|
||||
/>
|
||||
</Group>
|
||||
</Group>
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Produk</TableTh>
|
||||
<TableTh>Penjualan</TableTh>
|
||||
<TableTh>Penjualan Bulan Ini</TableTh>
|
||||
<TableTh>Bulan Lalu</TableTh>
|
||||
<TableTh>Trend</TableTh>
|
||||
<TableTh>Volume</TableTh>
|
||||
<TableTh>Stok</TableTh>
|
||||
<TableTh>Status</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{detail.map((item, i) => (
|
||||
{detail.map((item: any, i: number) => (
|
||||
<TableTr key={i}>
|
||||
<TableTd>{item.namaProduk}</TableTd>
|
||||
<TableTd>Rp {item.penjualanBulanIni.toLocaleString()}</TableTd>
|
||||
<TableTd>{renderTrend(item.trend)}</TableTd>
|
||||
<TableTd>Rp {item.penjualanBulanLalu.toLocaleString()}</TableTd>
|
||||
<TableTd>
|
||||
<Group gap={4} wrap="nowrap">
|
||||
{renderTrend(item.trend)}
|
||||
{item.trendPersen !== 0 && (
|
||||
<Text
|
||||
size="xs"
|
||||
c={item.trend === 'up' ? 'green' : item.trend === 'down' ? 'red' : 'dimmed'}
|
||||
>
|
||||
{item.trendPersen > 0 ? '+' : ''}{item.trendPersen}%
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
</TableTd>
|
||||
<TableTd>{item.volume}</TableTd>
|
||||
<TableTd>{item.stok}</TableTd>
|
||||
<TableTd>
|
||||
<Badge color={getStatusColor(item.statusStok)}>
|
||||
|
||||
@@ -13,6 +13,8 @@ import KategoriPengumuman from "./pengumuman/kategori-pengumuman";
|
||||
import MantanPerbekel from "./profile/profile-mantan-perbekel";
|
||||
import AjukanPermohonan from "./layanan/ajukan_permohonan";
|
||||
import Musik from "./musik";
|
||||
import KegiatanDesa from "./kegiatan-desa";
|
||||
import KategoriKegiatan from "./kegiatan-desa/kategori-kegiatan";
|
||||
|
||||
|
||||
const Desa = new Elysia({ prefix: "/desa", tags: ["Desa"] })
|
||||
@@ -30,6 +32,8 @@ const Desa = new Elysia({ prefix: "/desa", tags: ["Desa"] })
|
||||
.use(KategoriPengumuman)
|
||||
.use(AjukanPermohonan)
|
||||
.use(Musik)
|
||||
.use(KegiatanDesa)
|
||||
.use(KategoriKegiatan)
|
||||
|
||||
|
||||
export default Desa;
|
||||
|
||||
30
src/app/api/[[...slugs]]/_lib/desa/kegiatan-desa/create.ts
Normal file
30
src/app/api/[[...slugs]]/_lib/desa/kegiatan-desa/create.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
async function kegiatanDesaCreate(context: Context) {
|
||||
const body = context.body as any;
|
||||
|
||||
try {
|
||||
const data = await prisma.kegiatanDesa.create({
|
||||
data: {
|
||||
judul: body.judul,
|
||||
deskripsiSingkat: body.deskripsiSingkat,
|
||||
deskripsiLengkap: body.deskripsiLengkap,
|
||||
tanggal: new Date(body.tanggal),
|
||||
lokasi: body.lokasi,
|
||||
partisipan: Number(body.partisipan) || 0,
|
||||
kategoriKegiatanId: body.kategoriKegiatanId,
|
||||
imageId: body.imageId || null,
|
||||
},
|
||||
include: { kategoriKegiatan: true, image: true },
|
||||
});
|
||||
|
||||
return { success: true, message: "Kegiatan desa berhasil dibuat", data };
|
||||
} catch (e) {
|
||||
console.error("Error di kegiatanDesaCreate:", e);
|
||||
return { success: false, message: "Gagal membuat kegiatan desa" };
|
||||
}
|
||||
}
|
||||
|
||||
export default kegiatanDesaCreate;
|
||||
19
src/app/api/[[...slugs]]/_lib/desa/kegiatan-desa/del.ts
Normal file
19
src/app/api/[[...slugs]]/_lib/desa/kegiatan-desa/del.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
async function kegiatanDesaDelete(context: Context) {
|
||||
const { id } = context.params as { id: string };
|
||||
|
||||
try {
|
||||
await prisma.kegiatanDesa.update({
|
||||
where: { id },
|
||||
data: { isActive: false },
|
||||
});
|
||||
return { success: true, message: "Kegiatan desa berhasil dihapus" };
|
||||
} catch (e) {
|
||||
console.error("Error di kegiatanDesaDelete:", e);
|
||||
return { success: false, message: "Gagal menghapus kegiatan desa" };
|
||||
}
|
||||
}
|
||||
|
||||
export default kegiatanDesaDelete;
|
||||
@@ -0,0 +1,57 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
async function kegiatanDesaFindMany(context: Context) {
|
||||
const page = Number(context.query.page) || 1;
|
||||
const limit = Number(context.query.limit) || 10;
|
||||
const search = (context.query.search as string) || '';
|
||||
const kategori = (context.query.kategori as string) || '';
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const where: any = { isActive: true };
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ judul: { contains: search, mode: 'insensitive' } },
|
||||
{ lokasi: { contains: search, mode: 'insensitive' } },
|
||||
];
|
||||
}
|
||||
|
||||
if (kategori) {
|
||||
where.kategoriKegiatan = {
|
||||
nama: { contains: kategori, mode: 'insensitive' },
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.kegiatanDesa.findMany({
|
||||
where,
|
||||
include: {
|
||||
kategoriKegiatan: true,
|
||||
image: true,
|
||||
},
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { tanggal: 'asc' },
|
||||
}),
|
||||
prisma.kegiatanDesa.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Berhasil ambil kegiatan desa",
|
||||
data,
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Error di kegiatanDesaFindMany:", e);
|
||||
return { success: false, message: "Gagal mengambil data kegiatan desa" };
|
||||
}
|
||||
}
|
||||
|
||||
export default kegiatanDesaFindMany;
|
||||
35
src/app/api/[[...slugs]]/_lib/desa/kegiatan-desa/index.ts
Normal file
35
src/app/api/[[...slugs]]/_lib/desa/kegiatan-desa/index.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
import kegiatanDesaFindMany from "./find-many";
|
||||
import kegiatanDesaCreate from "./create";
|
||||
import kegiatanDesaDelete from "./del";
|
||||
import kegiatanDesaUpdate from "./updt";
|
||||
|
||||
const KegiatanDesa = new Elysia({ prefix: "/kegiatandesa", tags: ["Desa/Kegiatan Desa"] })
|
||||
.get("/find-many", kegiatanDesaFindMany)
|
||||
.post("/create", kegiatanDesaCreate, {
|
||||
body: t.Object({
|
||||
judul: t.String(),
|
||||
deskripsiSingkat: t.String(),
|
||||
deskripsiLengkap: t.String(),
|
||||
tanggal: t.String(),
|
||||
lokasi: t.String(),
|
||||
partisipan: t.Optional(t.Number()),
|
||||
kategoriKegiatanId: t.String(),
|
||||
imageId: t.Optional(t.String()),
|
||||
}),
|
||||
})
|
||||
.put("/:id", kegiatanDesaUpdate, {
|
||||
body: t.Object({
|
||||
judul: t.String(),
|
||||
deskripsiSingkat: t.String(),
|
||||
deskripsiLengkap: t.String(),
|
||||
tanggal: t.String(),
|
||||
lokasi: t.String(),
|
||||
partisipan: t.Optional(t.Number()),
|
||||
kategoriKegiatanId: t.String(),
|
||||
imageId: t.Optional(t.String()),
|
||||
}),
|
||||
})
|
||||
.delete("/del/:id", kegiatanDesaDelete);
|
||||
|
||||
export default KegiatanDesa;
|
||||
@@ -0,0 +1,26 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
type FormCreate = {
|
||||
nama: string;
|
||||
}
|
||||
|
||||
export default async function kategoriKegiatanCreate(context: Context) {
|
||||
const body = (await context.body) as FormCreate;
|
||||
|
||||
try {
|
||||
const result = await prisma.kategoriKegiatan.create({
|
||||
data: {
|
||||
nama: body.nama,
|
||||
},
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: "Berhasil membuat kategori kegiatan",
|
||||
data: result,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error creating kategori kegiatan:", error);
|
||||
throw new Error("Gagal membuat kategori kegiatan: " + (error as Error).message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function kategoriKegiatanDelete(context: Context) {
|
||||
try {
|
||||
const id = context.params?.id as string;
|
||||
|
||||
if (!id) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
message: "ID tidak boleh kosong",
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// ✅ Cek apakah kategori masih digunakan oleh kegiatan
|
||||
const kegiatanCount = await prisma.kegiatanDesa.count({
|
||||
where: {
|
||||
kategoriKegiatanId: id,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (kegiatanCount > 0) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
message: `Kategori tidak dapat dihapus karena masih digunakan oleh ${kegiatanCount} kegiatan`,
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// ✅ Soft delete (bukan hard delete)
|
||||
await prisma.kategoriKegiatan.update({
|
||||
where: { id },
|
||||
data: {
|
||||
deletedAt: new Date(),
|
||||
isActive: false,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Kategori kegiatan berhasil dihapus",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Delete kategori error:", error);
|
||||
return Response.json({
|
||||
success: false,
|
||||
message: "Gagal menghapus kategori: " + (error instanceof Error ? error.message : 'Unknown error'),
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
async function kategoriKegiatanFindMany(context: Context) {
|
||||
// Ambil parameter dari query
|
||||
const page = Number(context.query.page) || 1;
|
||||
const limit = Number(context.query.limit) || 10;
|
||||
const search = (context.query.search as string) || '';
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Buat where clause
|
||||
const where: any = { isActive: true };
|
||||
|
||||
// Tambahkan pencarian (jika ada)
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ nama: { contains: search, mode: 'insensitive' } },
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
// Ambil data dan total count secara paralel
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.kategoriKegiatan.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { createdAt: 'asc' },
|
||||
}),
|
||||
prisma.kategoriKegiatan.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Berhasil ambil kategori kegiatan dengan pagination",
|
||||
data,
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Error di findMany paginated:", e);
|
||||
return {
|
||||
success: false,
|
||||
message: "Gagal mengambil data kategori kegiatan",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default kategoriKegiatanFindMany;
|
||||
@@ -0,0 +1,46 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export default async function kategoriKegiatanFindUnique(request: Request) {
|
||||
const url = new URL(request.url);
|
||||
const pathSegments = url.pathname.split('/');
|
||||
const id = pathSegments[pathSegments.length - 1];
|
||||
|
||||
if (!id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "ID is required",
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof id !== 'string') {
|
||||
return {
|
||||
success: false,
|
||||
message: "ID is required",
|
||||
}
|
||||
}
|
||||
|
||||
const data = await prisma.kategoriKegiatan.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Data not found",
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success get kategori kegiatan",
|
||||
data,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Find by ID error:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Gagal mengambil data: " + (error instanceof Error ? error.message : 'Unknown error'),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
import kategoriKegiatanCreate from "./create";
|
||||
import kategoriKegiatanDelete from "./del";
|
||||
import kategoriKegiatanFindMany from "./findMany";
|
||||
import kategoriKegiatanFindUnique from "./findUnique";
|
||||
import kategoriKegiatanUpdate from "./updt";
|
||||
|
||||
const KategoriKegiatan = new Elysia({
|
||||
prefix: "/kategorikegiatan",
|
||||
tags: ["Desa / Kegiatan Desa / Kategori Kegiatan"],
|
||||
})
|
||||
|
||||
.post("/create", kategoriKegiatanCreate, {
|
||||
body: t.Object({
|
||||
nama: t.String(),
|
||||
}),
|
||||
})
|
||||
|
||||
.get("/findMany", kategoriKegiatanFindMany)
|
||||
.get("/:id", async (context) => {
|
||||
const response = await kategoriKegiatanFindUnique(
|
||||
new Request(context.request)
|
||||
);
|
||||
return response;
|
||||
})
|
||||
.put("/:id", kategoriKegiatanUpdate, {
|
||||
body: t.Object({
|
||||
nama: t.String(),
|
||||
}),
|
||||
})
|
||||
.delete("/del/:id", kategoriKegiatanDelete);
|
||||
|
||||
export default KategoriKegiatan;
|
||||
@@ -0,0 +1,28 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
type FormUpdate = {
|
||||
nama: string;
|
||||
}
|
||||
|
||||
export default async function kategoriKegiatanUpdate(context: Context) {
|
||||
const body = (await context.body) as FormUpdate;
|
||||
const id = context.params.id as string;
|
||||
|
||||
try {
|
||||
const result = await prisma.kategoriKegiatan.update({
|
||||
where: { id },
|
||||
data: {
|
||||
nama: body.nama,
|
||||
},
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: "Berhasil mengupdate kategori kegiatan",
|
||||
data: result,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error updating kategori kegiatan:", error);
|
||||
throw new Error("Gagal mengupdate kategori kegiatan: " + (error as Error).message);
|
||||
}
|
||||
}
|
||||
32
src/app/api/[[...slugs]]/_lib/desa/kegiatan-desa/updt.ts
Normal file
32
src/app/api/[[...slugs]]/_lib/desa/kegiatan-desa/updt.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
async function kegiatanDesaUpdate(context: Context) {
|
||||
const { id } = context.params as { id: string };
|
||||
const body = context.body as any;
|
||||
|
||||
try {
|
||||
const data = await prisma.kegiatanDesa.update({
|
||||
where: { id },
|
||||
data: {
|
||||
judul: body.judul,
|
||||
deskripsiSingkat: body.deskripsiSingkat,
|
||||
deskripsiLengkap: body.deskripsiLengkap,
|
||||
tanggal: new Date(body.tanggal),
|
||||
lokasi: body.lokasi,
|
||||
partisipan: Number(body.partisipan) || 0,
|
||||
kategoriKegiatanId: body.kategoriKegiatanId,
|
||||
imageId: body.imageId || null,
|
||||
},
|
||||
include: { kategoriKegiatan: true, image: true },
|
||||
});
|
||||
|
||||
return { success: true, message: "Kegiatan desa berhasil diupdate", data };
|
||||
} catch (e) {
|
||||
console.error("Error di kegiatanDesaUpdate:", e);
|
||||
return { success: false, message: "Gagal mengupdate kegiatan desa" };
|
||||
}
|
||||
}
|
||||
|
||||
export default kegiatanDesaUpdate;
|
||||
@@ -1,49 +1,86 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
function getWeekRange(offsetWeeks = 0) {
|
||||
const now = new Date();
|
||||
const day = now.getDay();
|
||||
const diffToMonday = day === 0 ? -6 : 1 - day;
|
||||
|
||||
const monday = new Date(now);
|
||||
monday.setDate(now.getDate() + diffToMonday + offsetWeeks * 7);
|
||||
monday.setHours(0, 0, 0, 0);
|
||||
|
||||
const sunday = new Date(monday);
|
||||
sunday.setDate(monday.getDate() + 6);
|
||||
sunday.setHours(23, 59, 59, 999);
|
||||
|
||||
return { start: monday, end: sunday };
|
||||
}
|
||||
|
||||
async function umkmDashboardDetailPenjualan(context: Context) {
|
||||
const periode = (context.query.periode as string) ||
|
||||
`${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
|
||||
const mode = context.query.mode as string | undefined;
|
||||
const isWeek = mode === "week";
|
||||
|
||||
const periode =
|
||||
(context.query.periode as string) ||
|
||||
`${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, "0")}`;
|
||||
|
||||
const date = new Date(periode + "-01");
|
||||
date.setMonth(date.getMonth() - 1);
|
||||
const periodeLalu = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
||||
const periodeLalu = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
||||
|
||||
const kategoriId = context.query.kategoriId as string | undefined;
|
||||
const umkmId = context.query.umkmId as string | undefined;
|
||||
|
||||
const whereSkrg = isWeek
|
||||
? { createdAt: { gte: getWeekRange(0).start, lte: getWeekRange(0).end }, deletedAt: null }
|
||||
: { periode, deletedAt: null };
|
||||
|
||||
const whereLalu = isWeek
|
||||
? { createdAt: { gte: getWeekRange(-1).start, lte: getWeekRange(-1).end }, deletedAt: null }
|
||||
: { periode: periodeLalu, deletedAt: null };
|
||||
|
||||
// Filter produk berdasarkan kategori dan/atau UMKM
|
||||
const produkFilter: Record<string, unknown> = { deletedAt: null };
|
||||
if (kategoriId) produkFilter.kategoriProdukId = kategoriId;
|
||||
if (umkmId) produkFilter.umkmId = umkmId;
|
||||
|
||||
try {
|
||||
// Ambil semua produk yang punya penjualan bulan ini atau bulan lalu
|
||||
const [produkSkrg, produkLalu, allProduks] = await Promise.all([
|
||||
prisma.penjualanProduk.groupBy({
|
||||
by: ['produkId'],
|
||||
where: { periode, deletedAt: null },
|
||||
_sum: { totalNilai: true, jumlah: true }
|
||||
by: ["produkId"],
|
||||
where: whereSkrg,
|
||||
_sum: { totalNilai: true, jumlah: true },
|
||||
}),
|
||||
prisma.penjualanProduk.groupBy({
|
||||
by: ['produkId'],
|
||||
where: { periode: periodeLalu, deletedAt: null },
|
||||
_sum: { totalNilai: true }
|
||||
by: ["produkId"],
|
||||
where: whereLalu,
|
||||
_sum: { totalNilai: true },
|
||||
}),
|
||||
// Use PasarDesa
|
||||
prisma.pasarDesa.findMany({
|
||||
where: { deletedAt: null },
|
||||
select: { id: true, nama: true, stok: true }
|
||||
})
|
||||
where: produkFilter,
|
||||
select: { id: true, nama: true, stok: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
const data = allProduks.map(p => {
|
||||
const skrgRaw = produkSkrg.find(s => s.produkId === p.id)?._sum || { totalNilai: 0, jumlah: 0 };
|
||||
const laluRaw = produkLalu.find(l => l.produkId === p.id)?._sum || { totalNilai: 0 };
|
||||
|
||||
const skrg = {
|
||||
totalNilai: (skrgRaw as any).totalNilai || 0,
|
||||
jumlah: (skrgRaw as any).jumlah || 0
|
||||
};
|
||||
const lalu = {
|
||||
totalNilai: (laluRaw as any).totalNilai || 0
|
||||
};
|
||||
|
||||
const data = allProduks.map((p) => {
|
||||
const skrgRaw = produkSkrg.find((s) => s.produkId === p.id)?._sum || {};
|
||||
const laluRaw = produkLalu.find((l) => l.produkId === p.id)?._sum || {};
|
||||
|
||||
const nilaiSkrg = (skrgRaw as any).totalNilai || 0;
|
||||
const nilaiLalu = (laluRaw as any).totalNilai || 0;
|
||||
const jumlah = (skrgRaw as any).jumlah || 0;
|
||||
|
||||
let trend = "stable";
|
||||
if (skrg.totalNilai > lalu.totalNilai) trend = "up";
|
||||
if (skrg.totalNilai < lalu.totalNilai) trend = "down";
|
||||
if (nilaiSkrg > nilaiLalu) trend = "up";
|
||||
if (nilaiSkrg < nilaiLalu) trend = "down";
|
||||
|
||||
let trendPersen = 0;
|
||||
if (nilaiLalu > 0) {
|
||||
trendPersen = Math.round(((nilaiSkrg - nilaiLalu) / nilaiLalu) * 10000) / 100;
|
||||
} else if (nilaiSkrg > 0) {
|
||||
trendPersen = 100;
|
||||
}
|
||||
|
||||
let statusStok = "Aman";
|
||||
if (p.stok < 5) statusStok = "Rendah";
|
||||
@@ -51,19 +88,17 @@ async function umkmDashboardDetailPenjualan(context: Context) {
|
||||
|
||||
return {
|
||||
namaProduk: p.nama,
|
||||
penjualanBulanIni: skrg.totalNilai,
|
||||
penjualanBulanLalu: lalu.totalNilai,
|
||||
penjualanBulanIni: nilaiSkrg,
|
||||
penjualanBulanLalu: nilaiLalu,
|
||||
trend,
|
||||
volume: skrg.jumlah,
|
||||
trendPersen,
|
||||
volume: jumlah,
|
||||
stok: p.stok,
|
||||
statusStok
|
||||
statusStok,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data
|
||||
};
|
||||
return { success: true, data };
|
||||
} catch (e) {
|
||||
console.error("Error di umkmDashboardDetailPenjualan:", e);
|
||||
return { success: false, message: "Gagal mengambil detail penjualan dashboard" };
|
||||
|
||||
@@ -1,44 +1,55 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
function getWeekRange(offsetWeeks = 0) {
|
||||
const now = new Date();
|
||||
const day = now.getDay();
|
||||
const diffToMonday = day === 0 ? -6 : 1 - day;
|
||||
|
||||
const monday = new Date(now);
|
||||
monday.setDate(now.getDate() + diffToMonday + offsetWeeks * 7);
|
||||
monday.setHours(0, 0, 0, 0);
|
||||
|
||||
const sunday = new Date(monday);
|
||||
sunday.setDate(monday.getDate() + 6);
|
||||
sunday.setHours(23, 59, 59, 999);
|
||||
|
||||
return { start: monday, end: sunday };
|
||||
}
|
||||
|
||||
async function umkmDashboardKpi(context: Context) {
|
||||
const mode = context.query.mode as string | undefined;
|
||||
const isWeek = mode === "week";
|
||||
|
||||
const periode =
|
||||
(context.query.periode as string) ||
|
||||
`${new Date().getFullYear()}-${String(
|
||||
new Date().getMonth() + 1
|
||||
).padStart(2, "0")}`;
|
||||
`${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, "0")}`;
|
||||
|
||||
const whereClause = isWeek
|
||||
? { createdAt: { gte: getWeekRange(0).start, lte: getWeekRange(0).end }, deletedAt: null }
|
||||
: { periode, deletedAt: null };
|
||||
|
||||
try {
|
||||
// KPI utama
|
||||
const [umkmAktif, totalUmkm, omzetBulanan] = await Promise.all([
|
||||
prisma.umkm.count({
|
||||
where: { isActive: true, deletedAt: null },
|
||||
}),
|
||||
prisma.umkm.count({
|
||||
where: { deletedAt: null },
|
||||
}),
|
||||
prisma.umkm.count({ where: { isActive: true, deletedAt: null } }),
|
||||
prisma.umkm.count({ where: { deletedAt: null } }),
|
||||
prisma.penjualanProduk.aggregate({
|
||||
where: { periode, deletedAt: null },
|
||||
where: whereClause,
|
||||
_sum: { totalNilai: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
// =========================
|
||||
// 1. Cari kategori dari penjualan
|
||||
// =========================
|
||||
// Cari kategori dengan penjualan terbanyak
|
||||
const salesByCategory = await prisma.penjualanProduk.findMany({
|
||||
where: { periode, deletedAt: null },
|
||||
where: whereClause,
|
||||
select: {
|
||||
jumlah: true,
|
||||
produk: {
|
||||
select: {
|
||||
kategoriProdukId: true,
|
||||
},
|
||||
},
|
||||
produk: { select: { kategoriProdukId: true } },
|
||||
},
|
||||
});
|
||||
|
||||
let kategoriNama = "-";
|
||||
let topCategoryId: string | null = null;
|
||||
|
||||
if (salesByCategory.length > 0) {
|
||||
const categoryCounts: Record<string, number> = {};
|
||||
@@ -46,15 +57,10 @@ async function umkmDashboardKpi(context: Context) {
|
||||
for (const sale of salesByCategory) {
|
||||
const catId = sale.produk.kategoriProdukId;
|
||||
if (!catId) continue;
|
||||
|
||||
categoryCounts[catId] =
|
||||
(categoryCounts[catId] || 0) + sale.jumlah;
|
||||
categoryCounts[catId] = (categoryCounts[catId] || 0) + sale.jumlah;
|
||||
}
|
||||
|
||||
// cari kategori dengan penjualan tertinggi
|
||||
let topCategoryId: string | null = null;
|
||||
let maxSales = 0;
|
||||
|
||||
for (const [id, count] of Object.entries(categoryCounts)) {
|
||||
if (count > maxSales) {
|
||||
maxSales = count;
|
||||
@@ -63,38 +69,40 @@ async function umkmDashboardKpi(context: Context) {
|
||||
}
|
||||
|
||||
if (topCategoryId) {
|
||||
const kategori = await prisma.kategoriProduk.findUnique({
|
||||
const kategori = await prisma.kategoriProdukUmkm.findUnique({
|
||||
where: { id: topCategoryId },
|
||||
select: { nama: true },
|
||||
});
|
||||
|
||||
kategoriNama = kategori?.nama || "-";
|
||||
}
|
||||
}
|
||||
|
||||
// =========================
|
||||
// 2. Fallback (kalau tidak ada penjualan)
|
||||
// =========================
|
||||
// Fallback: kategori dari UMKM terbanyak
|
||||
if (kategoriNama === "-") {
|
||||
const kategoriTerbanyakUmkm = await prisma.umkm.groupBy({
|
||||
by: ["kategoriId"],
|
||||
_count: { _all: true },
|
||||
orderBy: {
|
||||
_count: { kategoriId: "desc" },
|
||||
},
|
||||
orderBy: { _count: { kategoriId: "desc" } },
|
||||
take: 1,
|
||||
});
|
||||
|
||||
if (kategoriTerbanyakUmkm.length > 0) {
|
||||
topCategoryId = kategoriTerbanyakUmkm[0].kategoriId;
|
||||
const kategori = await prisma.kategoriProdukUmkm.findUnique({
|
||||
where: { id: kategoriTerbanyakUmkm[0].kategoriId },
|
||||
where: { id: topCategoryId },
|
||||
select: { nama: true },
|
||||
});
|
||||
|
||||
kategoriNama = kategori?.nama || "-";
|
||||
}
|
||||
}
|
||||
|
||||
// Hitung jumlah produk dalam kategori terbanyak
|
||||
const jumlahKategoriTerbanyak = topCategoryId
|
||||
? await prisma.pasarDesa.count({
|
||||
where: { kategoriProdukId: topCategoryId, deletedAt: null },
|
||||
})
|
||||
: 0;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
@@ -102,15 +110,13 @@ async function umkmDashboardKpi(context: Context) {
|
||||
totalUmkm,
|
||||
omzetBulanan: omzetBulanan._sum.totalNilai || 0,
|
||||
kategoriTerbanyak: kategoriNama,
|
||||
jumlahKategoriTerbanyak,
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Error di umkmDashboardKpi:", e);
|
||||
return {
|
||||
success: false,
|
||||
message: "Gagal mengambil data KPI dashboard",
|
||||
};
|
||||
return { success: false, message: "Gagal mengambil data KPI dashboard" };
|
||||
}
|
||||
}
|
||||
|
||||
export default umkmDashboardKpi;
|
||||
export default umkmDashboardKpi;
|
||||
|
||||
@@ -1,35 +1,63 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
function getWeekRange(offsetWeeks = 0) {
|
||||
const now = new Date();
|
||||
const day = now.getDay();
|
||||
const diffToMonday = day === 0 ? -6 : 1 - day;
|
||||
|
||||
const monday = new Date(now);
|
||||
monday.setDate(now.getDate() + diffToMonday + offsetWeeks * 7);
|
||||
monday.setHours(0, 0, 0, 0);
|
||||
|
||||
const sunday = new Date(monday);
|
||||
sunday.setDate(monday.getDate() + 6);
|
||||
sunday.setHours(23, 59, 59, 999);
|
||||
|
||||
return { start: monday, end: sunday };
|
||||
}
|
||||
|
||||
async function umkmDashboardRingSummary(context: Context) {
|
||||
const periode = (context.query.periode as string) ||
|
||||
`${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
|
||||
const mode = context.query.mode as string | undefined;
|
||||
const isWeek = mode === "week";
|
||||
|
||||
const periode =
|
||||
(context.query.periode as string) ||
|
||||
`${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, "0")}`;
|
||||
|
||||
// Hitung periode bulan lalu
|
||||
const date = new Date(periode + "-01");
|
||||
date.setMonth(date.getMonth() - 1);
|
||||
const periodeLalu = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
||||
const periodeLalu = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
||||
|
||||
const whereSkrg = isWeek
|
||||
? { createdAt: { gte: getWeekRange(0).start, lte: getWeekRange(0).end }, deletedAt: null }
|
||||
: { periode, deletedAt: null };
|
||||
|
||||
const whereLalu = isWeek
|
||||
? { createdAt: { gte: getWeekRange(-1).start, lte: getWeekRange(-1).end }, deletedAt: null }
|
||||
: { periode: periodeLalu, deletedAt: null };
|
||||
|
||||
try {
|
||||
const [penjualanSkrg, penjualanLalu, produkAktif, totalTransaksi] = await Promise.all([
|
||||
const [penjualanSkrg, penjualanLalu, kategoriAktif, totalTransaksi] = await Promise.all([
|
||||
prisma.penjualanProduk.aggregate({
|
||||
where: { periode, deletedAt: null },
|
||||
_sum: { totalNilai: true }
|
||||
where: whereSkrg,
|
||||
_sum: { totalNilai: true },
|
||||
}),
|
||||
prisma.penjualanProduk.aggregate({
|
||||
where: { periode: periodeLalu, deletedAt: null },
|
||||
_sum: { totalNilai: true }
|
||||
where: whereLalu,
|
||||
_sum: { totalNilai: true },
|
||||
}),
|
||||
// Count from PasarDesa
|
||||
prisma.pasarDesa.count({
|
||||
where: { isActive: true, deletedAt: null }
|
||||
// Hitung jumlah kategori aktif
|
||||
prisma.kategoriProdukUmkm.count({
|
||||
where: { isActive: true, deletedAt: null },
|
||||
}),
|
||||
prisma.penjualanProduk.count({ where: { periode, deletedAt: null } })
|
||||
prisma.penjualanProduk.count({ where: whereSkrg }),
|
||||
]);
|
||||
|
||||
const skrg = penjualanSkrg._sum.totalNilai || 0;
|
||||
const lalu = penjualanLalu._sum.totalNilai || 0;
|
||||
|
||||
|
||||
let persentasePerubahan = 0;
|
||||
if (lalu > 0) {
|
||||
persentasePerubahan = ((skrg - lalu) / lalu) * 100;
|
||||
@@ -42,9 +70,9 @@ async function umkmDashboardRingSummary(context: Context) {
|
||||
data: {
|
||||
totalPenjualan: skrg,
|
||||
persentasePerubahan: Math.round(persentasePerubahan * 100) / 100,
|
||||
produkAktif,
|
||||
totalTransaksi
|
||||
}
|
||||
kategoriAktif,
|
||||
totalTransaksi,
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Error di umkmDashboardRingSummary:", e);
|
||||
|
||||
@@ -1,39 +1,91 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
function getWeekRange(offsetWeeks = 0) {
|
||||
const now = new Date();
|
||||
const day = now.getDay();
|
||||
const diffToMonday = day === 0 ? -6 : 1 - day;
|
||||
|
||||
const monday = new Date(now);
|
||||
monday.setDate(now.getDate() + diffToMonday + offsetWeeks * 7);
|
||||
monday.setHours(0, 0, 0, 0);
|
||||
|
||||
const sunday = new Date(monday);
|
||||
sunday.setDate(monday.getDate() + 6);
|
||||
sunday.setHours(23, 59, 59, 999);
|
||||
|
||||
return { start: monday, end: sunday };
|
||||
}
|
||||
|
||||
async function umkmDashboardTopProduk(context: Context) {
|
||||
const periode = (context.query.periode as string) ||
|
||||
`${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
|
||||
const mode = context.query.mode as string | undefined;
|
||||
const isWeek = mode === "week";
|
||||
|
||||
const periode =
|
||||
(context.query.periode as string) ||
|
||||
`${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, "0")}`;
|
||||
|
||||
const date = new Date(periode + "-01");
|
||||
date.setMonth(date.getMonth() - 1);
|
||||
const periodeLalu = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
||||
|
||||
const whereSkrg = isWeek
|
||||
? { createdAt: { gte: getWeekRange(0).start, lte: getWeekRange(0).end }, deletedAt: null }
|
||||
: { periode, deletedAt: null };
|
||||
|
||||
const whereLalu = isWeek
|
||||
? { createdAt: { gte: getWeekRange(-1).start, lte: getWeekRange(-1).end }, deletedAt: null }
|
||||
: { periode: periodeLalu, deletedAt: null };
|
||||
|
||||
try {
|
||||
const topPenjualan = await prisma.penjualanProduk.groupBy({
|
||||
by: ['produkId'],
|
||||
where: { periode, deletedAt: null },
|
||||
by: ["produkId"],
|
||||
where: whereSkrg,
|
||||
_sum: { totalNilai: true, jumlah: true },
|
||||
orderBy: { _sum: { totalNilai: 'desc' } },
|
||||
take: 3
|
||||
orderBy: { _sum: { totalNilai: "desc" } },
|
||||
take: 3,
|
||||
});
|
||||
|
||||
const data = await Promise.all(topPenjualan.map(async (item) => {
|
||||
// Find from PasarDesa now
|
||||
const produk = await prisma.pasarDesa.findUnique({
|
||||
where: { id: item.produkId },
|
||||
include: { umkm: true }
|
||||
});
|
||||
// Ambil penjualan periode lalu untuk produk yang sama
|
||||
const produkIds = topPenjualan.map((p) => p.produkId);
|
||||
const penjualanLalu = await prisma.penjualanProduk.groupBy({
|
||||
by: ["produkId"],
|
||||
where: { ...whereLalu, produkId: { in: produkIds } },
|
||||
_sum: { totalNilai: true },
|
||||
});
|
||||
|
||||
return {
|
||||
namaProduk: produk?.nama || "Unknown",
|
||||
namaUmkm: produk?.umkm?.nama || "Unknown",
|
||||
totalPenjualan: item._sum.totalNilai || 0,
|
||||
jumlahTerjual: item._sum.jumlah || 0,
|
||||
growth: 0
|
||||
};
|
||||
}));
|
||||
const laluMap = new Map(
|
||||
penjualanLalu.map((p) => [p.produkId, p._sum.totalNilai || 0])
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data
|
||||
};
|
||||
const data = await Promise.all(
|
||||
topPenjualan.map(async (item) => {
|
||||
const produk = await prisma.pasarDesa.findUnique({
|
||||
where: { id: item.produkId },
|
||||
include: { umkm: true },
|
||||
});
|
||||
|
||||
const totalSkrg = item._sum.totalNilai || 0;
|
||||
const totalLalu = laluMap.get(item.produkId) || 0;
|
||||
|
||||
let growth = 0;
|
||||
if (totalLalu > 0) {
|
||||
growth = Math.round(((totalSkrg - totalLalu) / totalLalu) * 10000) / 100;
|
||||
} else if (totalSkrg > 0) {
|
||||
growth = 100;
|
||||
}
|
||||
|
||||
return {
|
||||
namaProduk: produk?.nama || "Unknown",
|
||||
namaUmkm: produk?.umkm?.nama || "Unknown",
|
||||
totalPenjualan: totalSkrg,
|
||||
jumlahTerjual: item._sum.jumlah || 0,
|
||||
growth,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return { success: true, data };
|
||||
} catch (e) {
|
||||
console.error("Error di umkmDashboardTopProduk:", e);
|
||||
return { success: false, message: "Gagal mengambil top produk" };
|
||||
|
||||
@@ -21,6 +21,7 @@ import Kematian from "./data_kesehatan_warga/persentase_kelahiran_kematian/kemat
|
||||
import DokterTenagaMedis from "./data_kesehatan_warga/fasilitas_kesehatan/dokter-tenaga-medis";
|
||||
import PendaftaranJadwalKegiatan from "./data_kesehatan_warga/jadwal_kegiatan/pendaftaran";
|
||||
import TarifLayanan from "./data_kesehatan_warga/fasilitas_kesehatan/tarif-layanan";
|
||||
import RingkasanKesehatan from "./ringkasan-kesehatan";
|
||||
|
||||
|
||||
const Kesehatan = new Elysia({
|
||||
@@ -49,4 +50,5 @@ const Kesehatan = new Elysia({
|
||||
.use(DokterTenagaMedis)
|
||||
.use(TarifLayanan)
|
||||
.use(PendaftaranJadwalKegiatan)
|
||||
.use(RingkasanKesehatan)
|
||||
export default Kesehatan;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
async function ringkasanKesehatanFindUnique() {
|
||||
try {
|
||||
const data = await prisma.ringkasanKesehatanDesa.findFirst({
|
||||
where: { isActive: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
return { success: true, data };
|
||||
} catch (e) {
|
||||
console.error("Error di ringkasanKesehatanFindUnique:", e);
|
||||
return { success: false, message: "Gagal mengambil ringkasan kesehatan" };
|
||||
}
|
||||
}
|
||||
|
||||
export default ringkasanKesehatanFindUnique;
|
||||
@@ -0,0 +1,15 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
import ringkasanKesehatanFindUnique from "./findUnique";
|
||||
import ringkasanKesehatanUpdate from "./updt";
|
||||
|
||||
const RingkasanKesehatan = new Elysia({ prefix: "/ringkasankesehatan", tags: ["Kesehatan/Ringkasan"] })
|
||||
.get("/find", ringkasanKesehatanFindUnique)
|
||||
.put("/update", ringkasanKesehatanUpdate, {
|
||||
body: t.Object({
|
||||
ibuHamilAkh: t.Number(),
|
||||
balitaTerdaftar: t.Number(),
|
||||
alertStunting: t.Number(),
|
||||
}),
|
||||
});
|
||||
|
||||
export default RingkasanKesehatan;
|
||||
@@ -0,0 +1,38 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
async function ringkasanKesehatanUpdate(context: Context) {
|
||||
const body = context.body as any;
|
||||
|
||||
try {
|
||||
const existing = await prisma.ringkasanKesehatanDesa.findFirst({
|
||||
where: { isActive: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
const data = existing
|
||||
? await prisma.ringkasanKesehatanDesa.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
ibuHamilAkh: Number(body.ibuHamilAkh),
|
||||
balitaTerdaftar: Number(body.balitaTerdaftar),
|
||||
alertStunting: Number(body.alertStunting),
|
||||
},
|
||||
})
|
||||
: await prisma.ringkasanKesehatanDesa.create({
|
||||
data: {
|
||||
ibuHamilAkh: Number(body.ibuHamilAkh),
|
||||
balitaTerdaftar: Number(body.balitaTerdaftar),
|
||||
alertStunting: Number(body.alertStunting),
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true, message: "Ringkasan kesehatan berhasil disimpan", data };
|
||||
} catch (e) {
|
||||
console.error("Error di ringkasanKesehatanUpdate:", e);
|
||||
return { success: false, message: "Gagal menyimpan ringkasan kesehatan" };
|
||||
}
|
||||
}
|
||||
|
||||
export default ringkasanKesehatanUpdate;
|
||||
@@ -0,0 +1,17 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
async function beasiswaConfigFindUnique() {
|
||||
try {
|
||||
const data = await prisma.beasiswaConfig.findFirst({
|
||||
where: { isActive: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
return { success: true, data };
|
||||
} catch (e) {
|
||||
console.error("Error di beasiswaConfigFindUnique:", e);
|
||||
return { success: false, message: "Gagal mengambil konfigurasi beasiswa" };
|
||||
}
|
||||
}
|
||||
|
||||
export default beasiswaConfigFindUnique;
|
||||
@@ -0,0 +1,14 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
import beasiswaConfigFindUnique from "./findUnique";
|
||||
import beasiswaConfigUpdate from "./updt";
|
||||
|
||||
const BeasiswaConfig = new Elysia({ prefix: "/beasiswaconfig", tags: ["Pendidikan/Beasiswa Desa/Config"] })
|
||||
.get("/find", beasiswaConfigFindUnique)
|
||||
.put("/update", beasiswaConfigUpdate, {
|
||||
body: t.Object({
|
||||
tahunAjaran: t.String(),
|
||||
danaTersalurkan: t.String(),
|
||||
}),
|
||||
});
|
||||
|
||||
export default BeasiswaConfig;
|
||||
@@ -0,0 +1,36 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
async function beasiswaConfigUpdate(context: Context) {
|
||||
const body = context.body as any;
|
||||
|
||||
try {
|
||||
const existing = await prisma.beasiswaConfig.findFirst({
|
||||
where: { isActive: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
const data = existing
|
||||
? await prisma.beasiswaConfig.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
tahunAjaran: body.tahunAjaran,
|
||||
danaTersalurkan: BigInt(body.danaTersalurkan),
|
||||
},
|
||||
})
|
||||
: await prisma.beasiswaConfig.create({
|
||||
data: {
|
||||
tahunAjaran: body.tahunAjaran,
|
||||
danaTersalurkan: BigInt(body.danaTersalurkan),
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true, message: "Konfigurasi beasiswa berhasil disimpan", data: { ...data, danaTersalurkan: data.danaTersalurkan.toString() } };
|
||||
} catch (e) {
|
||||
console.error("Error di beasiswaConfigUpdate:", e);
|
||||
return { success: false, message: "Gagal menyimpan konfigurasi beasiswa" };
|
||||
}
|
||||
}
|
||||
|
||||
export default beasiswaConfigUpdate;
|
||||
@@ -1,6 +1,7 @@
|
||||
import Elysia from "elysia";
|
||||
import BeasiswaPendaftar from "./beasiswa-pendaftar";
|
||||
import KeunggulanProgram from "./keunggulan-program";
|
||||
import BeasiswaConfig from "./beasiswa-config";
|
||||
|
||||
const Beasiswa = new Elysia({
|
||||
prefix: "/beasiswa",
|
||||
@@ -8,5 +9,6 @@ const Beasiswa = new Elysia({
|
||||
})
|
||||
.use(BeasiswaPendaftar)
|
||||
.use(KeunggulanProgram)
|
||||
.use(BeasiswaConfig)
|
||||
|
||||
export default Beasiswa
|
||||
@@ -63,17 +63,19 @@ const Utils = new Elysia({
|
||||
});
|
||||
|
||||
const ApiServer = new Elysia()
|
||||
.use(swagger({ path: "/api/docs" }))
|
||||
.use(cors(corsConfig))
|
||||
.use(
|
||||
swagger({
|
||||
path: "/docs",
|
||||
path: "/api/docs",
|
||||
documentation: {
|
||||
info: {
|
||||
title: "Desa Darmasaba API Documentation",
|
||||
version: "1.0.0",
|
||||
},
|
||||
},
|
||||
scalarConfig: {
|
||||
spec: { url: "/api/docs/json" },
|
||||
},
|
||||
}),
|
||||
)
|
||||
.onError(({ code }) => {
|
||||
|
||||
@@ -174,7 +174,10 @@ function DetailProdukPasarUser() {
|
||||
{data.stok > 0 ? `Stok: ${data.stok}` : 'Stok Habis'}
|
||||
</Badge>
|
||||
|
||||
<Text>{data.deskripsi}</Text>
|
||||
<Text
|
||||
style={{ wordBreak: 'break-word' }}
|
||||
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
|
||||
/>
|
||||
|
||||
<Button
|
||||
leftSection={<IconShoppingCart />}
|
||||
|
||||
Reference in New Issue
Block a user