Compare commits

..

6 Commits

Author SHA1 Message Date
89e83d806e upd: dashboard
Deskripsi:
- menu dashboard
- tampilan pengaduan list

No Issues
2025-11-06 17:37:21 +08:00
df7f93c794 upd: configurasi desa
Deskripsi:
- update table database
- seeder configurasi desa

NO Issues
2025-11-06 12:23:25 +08:00
de594acbf6 upd: api pelayanan surat 2025-11-06 11:29:40 +08:00
84d2388eb8 upd: coba upload file
Deskripsi:
- seafile upload
- coba api upload file api

No Issues
2025-11-06 10:44:40 +08:00
25f92e3686 Merge pull request 'upd: upload gambar' (#9) from amalia/05-nov-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/9
2025-11-05 17:24:31 +08:00
169b2b0e3e Merge pull request 'upd: api' (#8) from amalia/04-nov-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/8
2025-11-04 15:33:42 +08:00
11 changed files with 331 additions and 51 deletions

View File

@@ -193,7 +193,7 @@ model SuratPelayanan {
model Configuration {
id String @id @default(cuid())
category String
name String
value String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

View File

@@ -1,4 +1,5 @@
import { categoryPelayananSurat } from "@/lib/categoryPelayananSurat";
import { confDesa } from "@/lib/configurationDesa";
import { prisma } from "@/server/lib/prisma";
const category = [
@@ -51,7 +52,6 @@ const user = [
(async () => {
for (const r of role) {
console.log(`Seeding role ${r.name}`)
await prisma.role.upsert({
where: { id: r.id },
create: r,
@@ -81,7 +81,7 @@ const user = [
console.log(`✅ Category ${c.name} seeded successfully`)
}
for (const cp of categoryPelayananSurat){
for (const cp of categoryPelayananSurat) {
await prisma.categoryPelayanan.upsert({
where: { id: cp.id },
create: cp,
@@ -91,6 +91,15 @@ const user = [
console.log(`✅ Category Pelayanan ${cp.name} seeded successfully`)
}
for (const c of confDesa) {
await prisma.configuration.upsert({
where: { id: c.id },
create: c,
update: c
})
console.log(`✅ Configuration ${c.name} seeded successfully`)
}

View File

@@ -1,27 +1,28 @@
// ⚡ Auto-generated by generateRoutes.ts — DO NOT EDIT MANUALLY
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Login from "./pages/Login";
import DarmasabaLayout from "./pages/darmasaba/darmasaba_layout";
import FormSuratKeteranganUsaha from "./pages/darmasaba/form_surat_keterangan_usaha";
import FormSuratKeteranganTidakMampu from "./pages/darmasaba/form_surat_keterangan_tidak_mampu";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import DarmasabaHome from "./pages/darmasaba/darmasaba_home";
import FormKartuTandaPenduduk from "./pages/darmasaba/form_kartu_tanda_penduduk";
import DarmasabaLayout from "./pages/darmasaba/darmasaba_layout";
import FormKartuKeluarga from "./pages/darmasaba/form_kartu_keluarga";
import FormLaporanSampah from "./pages/darmasaba/form_laporan_sampah";
import FormSuratKeteranganPenghasilan from "./pages/darmasaba/form_surat_keterangan_penghasilan";
import FormSuratKeteranganDomisiliOrganisasi from "./pages/darmasaba/form_surat_keterangan_domisili_organisasi";
import FormSuratKeteranganBelumKawin from "./pages/darmasaba/form_surat_keterangan_belum_kawin";
import FormKartuTandaPenduduk from "./pages/darmasaba/form_kartu_tanda_penduduk";
import FormKeteranganKelahiran from "./pages/darmasaba/form_keterangan_kelahiran";
import FormSuratKeteranganTempatUsaha from "./pages/darmasaba/form_surat_keterangan_tempat_usaha";
import FormLaporanSampah from "./pages/darmasaba/form_laporan_sampah";
import FormSuratKeteranganBelumKawin from "./pages/darmasaba/form_surat_keterangan_belum_kawin";
import FormSuratKeteranganDomisiliOrganisasi from "./pages/darmasaba/form_surat_keterangan_domisili_organisasi";
import FormSuratKeteranganKelakuanBaik from "./pages/darmasaba/form_surat_keterangan_kelakuan_baik";
import FormSuratKeteranganPenghasilan from "./pages/darmasaba/form_surat_keterangan_penghasilan";
import FormSuratKeteranganTempatUsaha from "./pages/darmasaba/form_surat_keterangan_tempat_usaha";
import FormSuratKeteranganTidakMampu from "./pages/darmasaba/form_surat_keterangan_tidak_mampu";
import FormSuratKeteranganUsaha from "./pages/darmasaba/form_surat_keterangan_usaha";
import DirPage from "./pages/dir/dir_page";
import Home from "./pages/Home";
import Login from "./pages/Login";
import NotFound from "./pages/NotFound";
import ApikeyPage from "./pages/scr/dashboard/apikey/apikey_page";
import CredentialPage from "./pages/scr/dashboard/credential/credential_page";
import DashboardHome from "./pages/scr/dashboard/dashboard_home";
import ApikeyPage from "./pages/scr/dashboard/apikey/apikey_page";
import DashboardLayout from "./pages/scr/dashboard/dashboard_layout";
import ListPage from "./pages/scr/dashboard/pengaduan/list_page";
import ScrLayout from "./pages/scr/scr_layout";
import DirPage from "./pages/dir/dir_page";
import NotFound from "./pages/NotFound";
export default function AppRoutes() {
return (
@@ -92,6 +93,10 @@ export default function AppRoutes() {
path="/scr/dashboard/dashboard-home"
element={<DashboardHome />}
/>
<Route
path="/scr/dashboard/pengaduan/list"
element={<ListPage />}
/>
<Route
path="/scr/dashboard/apikey/apikey"
element={<ApikeyPage />}

View File

@@ -19,6 +19,7 @@ const clientRoutes = {
"/scr/dashboard": "/scr/dashboard",
"/scr/dashboard/credential/credential": "/scr/dashboard/credential/credential",
"/scr/dashboard/dashboard-home": "/scr/dashboard/dashboard-home",
"/scr/dashboard/pengaduan/list": "/scr/dashboard/pengaduan/list",
"/scr/dashboard/apikey/apikey": "/scr/dashboard/apikey/apikey",
"/dir/dir": "/dir/dir",
"/*": "/*"

View File

@@ -0,0 +1,57 @@
export const confDesa = [
{
id: "desaNama",
name: "Nama Desa",
value: "Darmasaba"
},
{
id: "desaKabupaten",
name: "Kabupaten",
value: "Badung"
},
{
id: "desaKecamatan",
name: "Kecamatan",
value: "Abiansemal"
},
{
id: "desaAlamat",
name: "Alamat Kantor Desa",
value: "Jl. Raya Darmasaba No.22, Darmasaba"
},
{
id: "desaPos",
name: "Kode Pos",
value: "80352"
},
{
id: "desaTelepon",
name: "Telepon",
value: "081239580000"
},
{
id: "desaEmail",
name: "Email",
value: "desadarmasaba@badungkab.go.id"
},
{
id: "perbekelNama",
name: "Nama Perbekel",
value: "Ida Bagus Surya Prabhawa Manuaba, S.H., M.H., N.L.P."
},
{
id: "perbekelJabatan",
name: "Jabatan",
value: "Perbekel"
},
{
id: "perbekelNIP",
name: "NIP",
value: ""
},
{
id: "perbekelTTD",
name: "TTD",
value: ""
},
];

View File

@@ -2,16 +2,16 @@ import { Tree } from "@mantine/core";
// ✅ Valid data, all values are unique
const data = [
{
value: 'src',
label: 'src',
children: [
{ value: 'src/components', label: 'components' },
{ value: 'src/hooks', label: 'hooks' },
],
},
{ value: 'package.json', label: 'package.json' },
];
{
value: "src",
label: "src",
children: [
{ value: "src/components", label: "components" },
{ value: "src/hooks", label: "hooks" },
],
},
{ value: "package.json", label: "package.json" },
];
export default function DirPage() {
return (

View File

@@ -1,4 +1,8 @@
import { useEffect, useState } from "react";
import {
default as clientRoute,
default as clientRoutes,
} from "@/clientRoutes";
import apiFetch from "@/lib/apiFetch";
import {
ActionIcon,
AppShell,
@@ -22,17 +26,17 @@ import {
IconChevronLeft,
IconChevronRight,
IconDashboard,
IconFileCertificate,
IconKey,
IconLock,
IconMessageReport,
IconSettings,
IconUser,
IconUsersGroup,
} from "@tabler/icons-react";
import type { User } from "generated/prisma";
import { useEffect, useState } from "react";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
import {
default as clientRoute,
default as clientRoutes,
} from "@/clientRoutes";
import apiFetch from "@/lib/apiFetch";
function Logout() {
return (
@@ -98,7 +102,7 @@ export default function DashboardLayout() {
size="lg"
style={{
backgroundColor: "rgba(255,255,255,0.05)",
boxShadow: "0 0 6px rgba(0,255,200,0.2)",
boxShadow: "0 0 6px hsla(167, 100%, 50%, 0.20), 0.20)",
}}
>
{opened ? <IconChevronLeft /> : <IconChevronRight />}
@@ -186,7 +190,7 @@ function HostView() {
{host.name}
</Text>
<Text size="sm" c="dimmed">
{host.email}
{host.roleId}
</Text>
</Stack>
</Flex>
@@ -219,6 +223,31 @@ function NavigationDashboard() {
label: "Dashboard Overview",
description: "Quick summary and insights",
},
{
path: "/scr/dashboard/pengaduan/list",
icon: <IconMessageReport size={20} />,
label: "Pengaduan Warga",
description: "Manage pengaduan warga",
},
{
path: "/scr/dashboard/pelayanan",
icon: <IconFileCertificate size={20} />,
label: "Pelayanan Surat",
description: "Manage pelayanan surat",
},
{
path: "/scr/dashboard/user",
icon: <IconUsersGroup size={20} />,
label: "User",
description: "Manage user",
},
{
path: "/scr/dashboard/setting",
icon: <IconSettings size={20} />,
label: "Setting",
description:
"Manage setting (category pengaduan dan pelayanan surat, desa, etc)",
},
{
path: "/scr/dashboard/apikey/apikey",
icon: <IconKey size={20} />,

View File

@@ -0,0 +1,186 @@
import apiFetch from "@/lib/apiFetch";
import {
Badge,
Card,
Container,
Divider,
Flex,
Group,
Stack,
Tabs,
Text,
Title
} from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { showNotification } from "@mantine/notifications";
import { IconAlignJustified, IconClockHour3, IconMapPin } from "@tabler/icons-react";
import { useLocation, useNavigate } from "react-router-dom";
import useSwr from "swr";
import { proxy, subscribe } from "valtio";
const state = proxy({ reload: "" });
function reloadState() {
state.reload = Math.random().toString();
}
export default function PengaduanListPage() {
const { search } = useLocation();
const query = new URLSearchParams(search);
const status = query.get("status");
console.log(status, "status");
return (
<Container
size="xl"
py="xl"
w={"100%"}
>
<Stack gap="xl">
<TabListPengaduan status={status || "all"} />
<ListPengaduan />
</Stack>
</Container>
);
}
function TabListPengaduan({ status }: { status: string }) {
const navigate = useNavigate();
return (
<Tabs defaultValue={status || "all"} color="teal">
<Tabs.List grow>
<Tabs.Tab value="all" onClick={() => { navigate("?status=all") }}>Semua</Tabs.Tab>
<Tabs.Tab value="antrian" onClick={() => { navigate("?status=antrian") }}>Antrian</Tabs.Tab>
<Tabs.Tab value="diterima" onClick={() => { navigate("?status=diterima") }}>Diterima</Tabs.Tab>
<Tabs.Tab value="dikerjakan" onClick={() => { navigate("?status=dikerjakan") }}>Dikerjakan</Tabs.Tab>
<Tabs.Tab value="ditolak" onClick={() => { navigate("?status=ditolak") }}>Ditolak</Tabs.Tab>
<Tabs.Tab value="selesai" onClick={() => { navigate("?status=selesai") }}>Selesai</Tabs.Tab>
</Tabs.List>
</Tabs>
);
}
function ListPengaduan() {
const { data, mutate, isLoading } = useSwr("/", () =>
apiFetch.api.credential.list.get(),
);
useShallowEffect(() => {
const unsubscribe = subscribe(state, () => mutate());
return () => unsubscribe();
}, []);
async function handleRemove(id: string) {
try {
await apiFetch.api.credential.rm.delete({ id });
showNotification({
color: "teal",
title: "Credential Deleted",
message: "The credential was successfully removed.",
});
reloadState();
} catch {
showNotification({
color: "red",
title: "Error",
message: "Failed to delete credential. Please try again.",
});
}
}
if (isLoading)
return (
<Card
radius="lg"
p="xl"
withBorder
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
}}
>
<Text size="sm" c="dimmed">
Loading credentials...
</Text>
</Card>
);
const list = data?.data?.list || [];
return (
<Card
radius="lg"
p="xl"
withBorder
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
}}
>
<Stack gap="md">
<Flex align="center" justify="space-between">
<Flex direction={"column"}>
<Title order={3} c="gray.2">
Dompet Hilang
</Title>
<Group>
<Title order={6} c="gray.5">
#PGD-061125-001
</Title>
<Text size="sm" c="dimmed">
updated 2 minutes ago
</Text>
</Group>
</Flex>
<Badge
size="xl"
variant="light"
radius="sm"
color="gray"
style={{ textTransform: "none" }}
>
Antrian
</Badge>
</Flex>
<Divider my={0} />
<Stack gap="sm">
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconClockHour3 size={20} color="white" />
<Text size="md" c="white">
Tanggal Aduan
</Text>
</Group>
<Text size="md">
05 November 2025
</Text>
</Flex>
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconMapPin size={20} color="white" />
<Text size="md" c="white">
Lokasi
</Text>
</Group>
<Text size="md">
Jalan Darmasaba Raya no 77
</Text>
</Flex>
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconAlignJustified size={20} color="white" />
<Text size="md" c="white">
Detail
</Text>
</Group>
<Text size="md">
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Quis, obcaecati. Sint natus culpa temporibus neque quasi expedita ratione, facere optio incidunt quibusdam suscipit nam nemo delectus beatae similique velit obcaecati?
</Text>
</Flex>
</Stack>
</Stack>
</Card>
);
}

View File

@@ -158,7 +158,6 @@ export async function uploadFile(config: Config, file: File): Promise<string> {
});
const text = await res.text();
console.log(text);
if (!res.ok) throw new Error(`Upload failed: ${text}`);
return `✅ Uploaded ${file.name} successfully`;

View File

@@ -2,6 +2,7 @@ import Elysia, { StatusMap, t } from "elysia"
import { generateNoPengajuanSurat } from "../lib/no-pengajuan-surat"
import { prisma } from "../lib/prisma"
import type { StatusPengaduan } from "generated/prisma"
import { normalizePhoneNumber } from "../lib/normalizePhone"
const PelayananRoute = new Elysia({
prefix: "pelayanan",
@@ -22,7 +23,7 @@ const PelayananRoute = new Elysia({
}, {
detail: {
summary: "List Kategori Pelayanan Surat",
description: `tool untuk mendapatkan list kategori pelayanan surat`,
description: `tool untuk mendapatkan list kategori pelayanan surat beserta syaratnya untuk memenuhi syarat dokumen sesuai kategori yg dipilih saat melakukan pengajuan surat`,
tags: ["mcp"]
}
})
@@ -175,9 +176,10 @@ const PelayananRoute = new Elysia({
})
if (!warga) {
const nomorHP = normalizePhoneNumber({ phone })
const cariWarga = await prisma.warga.findFirst({
where: {
phone,
phone: nomorHP,
}
})
@@ -185,7 +187,7 @@ const PelayananRoute = new Elysia({
const wargaCreate = await prisma.warga.create({
data: {
name: idWarga,
phone,
phone: nomorHP,
},
select: {
id: true
@@ -210,7 +212,7 @@ const PelayananRoute = new Elysia({
})
if (!pengaduan.id) {
throw new Error("gagal membuat pengaduan")
throw new Error("gagal membuat pengajuan surat")
}
let dataInsertSyaratDokumen = []
@@ -270,7 +272,7 @@ const PelayananRoute = new Elysia({
}),
detail: {
summary: "Create Pengajuan Pelayanan Surat",
description: `tool untuk membuat pengajuan pelayanan surat`,
description: `tool untuk membuat pengajuan pelayanan surat dengan syarat dokumen serta data text sesuai kategori pelayanan surat yang dipilih`,
tags: ["mcp"]
}
})

View File

@@ -1,10 +1,9 @@
import { swagger } from "@elysiajs/swagger"
import Elysia, { t } from "elysia"
import type { StatusPengaduan } from "generated/prisma"
import { generateNoPengaduan } from "../lib/no-pengaduan"
import { prisma } from "../lib/prisma"
import { defaultConfigSF, testConnection, uploadFile } from "../lib/seafile"
import { normalizePhoneNumber } from "../lib/normalizePhone"
import { prisma } from "../lib/prisma"
import { defaultConfigSF, uploadFile } from "../lib/seafile"
const PengaduanRoute = new Elysia({
prefix: "pengaduan",
@@ -432,7 +431,6 @@ const PengaduanRoute = new Elysia({
tags: ["mcp"]
}
})
.use(swagger())
.post("/upload",
async ({ body }) => {
const { file } = body;
@@ -442,16 +440,10 @@ const PengaduanRoute = new Elysia({
return { success: false, message: "File tidak ditemukan" };
}
// Contoh: cek koneksi ke Seafile
const coba = await testConnection(defaultConfigSF);
console.log("Seafile Connection:", coba);
// Upload ke Seafile (pastikan uploadFile menerima Blob atau ArrayBuffer)
// const buffer = await file.arrayBuffer();
const result = await uploadFile(defaultConfigSF, file);
console.log("Upload result:", result);
return {
success: true,
message: "Upload berhasil",