feat(kesehatan): posyandu banjar relation, redesign halaman publik, fix tips keamanan image

- Tambah model Banjar + relasi ke Posyandu (migration + seeder)
- Update API posyandu (create/update/find) untuk support banjarId
- Tambah endpoint banjar di kesehatan API
- Redesign halaman publik posyandu dengan tabs: ringkasan, data posyandu, balita, ibu hamil
- Update halaman admin posyandu list/create/edit/detail untuk banjar
- Fix image ketukar pada seed tips keamanan
- Hapus seeder core yang sudah tidak dipakai

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-28 15:51:32 +08:00
parent 9de32e5f12
commit 97d08734c5
38 changed files with 1126 additions and 454 deletions

View File

@@ -28,7 +28,15 @@ export default async function balitaFindMany(context: Context) {
const [data, total] = await Promise.all([
prisma.balita.findMany({
where,
include: { posyandu: { select: { id: true, name: true } } },
include: {
posyandu: {
select: {
id: true,
name: true,
banjar: { select: { id: true, name: true } },
},
},
},
skip,
take: limit,
orderBy: { createdAt: "desc" },

View File

@@ -0,0 +1,19 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function banjarFindMany(context: Context) {
try {
const data = await prisma.banjar.findMany({
where: { isActive: true },
select: { id: true, name: true },
orderBy: { name: "asc" },
});
return { success: true, data };
} catch (e) {
console.error("Error di banjarFindMany:", e);
return { success: false, message: "Gagal mengambil data banjar" };
}
}
export default banjarFindMany;

View File

@@ -0,0 +1,9 @@
import Elysia from "elysia";
import banjarFindMany from "./find-many";
const Banjar = new Elysia({
prefix: "/banjar",
tags: ["Kesehatan/Banjar"],
}).get("/find-many", banjarFindMany);
export default Banjar;

View File

@@ -27,7 +27,15 @@ export default async function ibuHamilFindMany(context: Context) {
const [data, total] = await Promise.all([
prisma.ibuHamil.findMany({
where,
include: { posyandu: { select: { id: true, name: true } } },
include: {
posyandu: {
select: {
id: true,
name: true,
banjar: { select: { id: true, name: true } },
},
},
},
skip,
take: limit,
orderBy: { createdAt: "desc" },

View File

@@ -24,6 +24,7 @@ import TarifLayanan from "./data_kesehatan_warga/fasilitas_kesehatan/tarif-layan
import RingkasanKesehatan from "./ringkasan-kesehatan";
import IbuHamil from "./ibu-hamil";
import Balita from "./balita";
import Banjar from "./banjar";
const Kesehatan = new Elysia({
@@ -55,4 +56,5 @@ const Kesehatan = new Elysia({
.use(RingkasanKesehatan)
.use(IbuHamil)
.use(Balita)
.use(Banjar)
export default Kesehatan;

View File

@@ -2,15 +2,14 @@ import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
type FormCreate = Prisma.PosyanduGetPayload<{
select: {
name: true;
nomor: true;
deskripsi: true;
imageId: true;
jadwalPelayanan: true;
};
}>;
type FormCreate = {
name: string;
nomor: string;
deskripsi: string;
imageId: string;
jadwalPelayanan: string;
banjarId?: string;
};
export default async function posyanduCreate(context: Context) {
const body = context.body as FormCreate;
@@ -21,6 +20,7 @@ export default async function posyanduCreate(context: Context) {
deskripsi: body.deskripsi,
imageId: body.imageId,
jadwalPelayanan: body.jadwalPelayanan,
banjarId: body.banjarId || null,
}
})
return {

View File

@@ -23,7 +23,8 @@ export default async function findPosyanduById(request: Request) {
const data = await prisma.posyandu.findUnique({
where: {id},
include: {
image: true
image: true,
banjar: { select: { id: true, name: true } },
}
})

View File

@@ -30,6 +30,7 @@ async function posyanduFindMany(context: Context) {
where,
include: {
image: true,
banjar: { select: { id: true, name: true } },
},
skip,
take: limit,

View File

@@ -16,6 +16,7 @@ const Posyandu = new Elysia({
deskripsi: t.String(),
imageId: t.String(),
jadwalPelayanan: t.String(),
banjarId: t.Optional(t.String()),
})
})
.get("/find-many", posyanduFindMany)
@@ -37,6 +38,7 @@ const Posyandu = new Elysia({
deskripsi: t.String(),
imageId: t.String(),
jadwalPelayanan: t.String(),
banjarId: t.Optional(t.String()),
})
}
)

View File

@@ -1,23 +1,20 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
import minio, { MINIO_BUCKET } from "@/lib/minio";
type FormUpdate = Prisma.PosyanduGetPayload<{
select: {
id: true;
name: true;
nomor: true;
deskripsi: true;
imageId: true;
jadwalPelayanan: true;
}
}>
type FormUpdate = {
name: string;
nomor: string;
deskripsi: string;
imageId: string;
jadwalPelayanan: string;
banjarId?: string;
};
export default async function posyanduUpdate(context: Context) {
try {
const id = context.params?.id as string;
const body = (await context.body) as Omit<FormUpdate, "id">;
const body = (await context.body) as FormUpdate;
const {
name,
@@ -25,6 +22,7 @@ export default async function posyanduUpdate(context: Context) {
deskripsi,
imageId,
jadwalPelayanan,
banjarId,
} = body;
if(!id) {
@@ -80,6 +78,7 @@ export default async function posyanduUpdate(context: Context) {
deskripsi,
imageId,
jadwalPelayanan,
banjarId: banjarId || null,
}
})

View File

@@ -5,7 +5,10 @@ import ringkasanKesehatanStats from "./stats";
const RingkasanKesehatan = new Elysia({ prefix: "/ringkasankesehatan", tags: ["Kesehatan/Ringkasan"] })
.get("/find", ringkasanKesehatanFindUnique)
.get("/stats", ringkasanKesehatanStats)
.get("/stats", (context) => {
const banjarId = (context.query.banjarId as string) || undefined;
return ringkasanKesehatanStats(banjarId);
})
.put("/update", ringkasanKesehatanUpdate, {
body: t.Object({
targetStuntingPct: t.Number({ minimum: 0, maximum: 100 }),

View File

@@ -10,8 +10,12 @@ type StatsResult = {
targetStuntingPct: number;
};
export default async function ringkasanKesehatanStats(): Promise<{ success: boolean; data?: StatsResult; message?: string }> {
export default async function ringkasanKesehatanStats(
banjarId?: string
): Promise<{ success: boolean; data?: StatsResult; message?: string }> {
try {
const posyanduFilter = banjarId ? { posyandu: { banjarId } } : {};
const [
ibuHamilAktif,
balitaTotal,
@@ -21,12 +25,12 @@ export default async function ringkasanKesehatanStats(): Promise<{ success: bool
giziBaik,
config,
] = await Promise.all([
prisma.ibuHamil.count({ where: { status: "AKTIF", isActive: true } }),
prisma.balita.count({ where: { isActive: true } }),
prisma.balita.count({ where: { isActive: true, statusStunting: { in: ["ALERT", "STUNTING"] } } }),
prisma.balita.count({ where: { isActive: true, imunisasiLengkap: true } }),
prisma.balita.count({ where: { isActive: true, pemeriksaanRutin: true } }),
prisma.balita.count({ where: { isActive: true, giziBaik: true } }),
prisma.ibuHamil.count({ where: { status: "AKTIF", isActive: true, ...posyanduFilter } }),
prisma.balita.count({ where: { isActive: true, ...posyanduFilter } }),
prisma.balita.count({ where: { isActive: true, statusStunting: { in: ["ALERT", "STUNTING"] }, ...posyanduFilter } }),
prisma.balita.count({ where: { isActive: true, imunisasiLengkap: true, ...posyanduFilter } }),
prisma.balita.count({ where: { isActive: true, pemeriksaanRutin: true, ...posyanduFilter } }),
prisma.balita.count({ where: { isActive: true, giziBaik: true, ...posyanduFilter } }),
prisma.ringkasanKesehatanDesa.findFirst({ where: { isActive: true }, orderBy: { createdAt: "desc" } }),
]);