Compare commits

..

8 Commits

Author SHA1 Message Date
6bdb0246c9 upd: api upload
Deskripsi:
- summary upload file form data

No Issues'
2025-11-25 16:47:47 +08:00
3f68f212cd upd: api jenna ai
Deskripsi:
- api upsert warga pada create pengaduan
- tampilan detail pengaduan jika tidak ada gambar

NO Issues
2025-11-25 16:17:44 +08:00
94e7604afb upd: api jenna ai
Deskripsi:
- tambah pengaduan

NO Issues
2025-11-25 15:01:01 +08:00
a253d40d19 upd: api jenna ai
Deskripsi:
- tambah pengaduan

NO Issues
2025-11-25 14:58:40 +08:00
26c7357ca3 Merge pull request 'amalia/25-nov-25' (#36) from amalia/25-nov-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/36
2025-11-25 14:05:46 +08:00
15c5140902 upd: dashboard admin
Deskripsi:
- nama field pada modal edit dan tambah role user

No Issues
2025-11-25 12:17:03 +08:00
c5b1452955 upd: dashboard admin
Deskripsi:
- tambah role user
- edit role user

No Issues
2025-11-25 12:15:29 +08:00
e1431fafb2 Merge pull request 'amalia/24-nov-25' (#35) from amalia/24-nov-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/35
2025-11-24 17:41:22 +08:00
6 changed files with 195 additions and 175 deletions

View File

@@ -16,112 +16,148 @@ export default function PermissionTree({
selected: string[];
onChange: (val: string[]) => void;
}) {
const [open, setOpen] = useState<Record<string, boolean>>({});
// Ambil semua child dari node
const [openNodes, setOpenNodes] = useState<Record<string, boolean>>({});
const toggle = (key: string) => {
setOpen((prev) => ({ ...prev, [key]: !prev[key] }));
};
function toggleNode(label: string) {
setOpenNodes(prev => ({ ...prev, [label]: !prev[label] }));
}
// Ambil semua key dari node termasuk semua keturunannya
const collectKeys = (n: Node): string[] => {
if (!n.children) return [n.key];
return [n.key, ...n.children.flatMap(collectKeys)];
};
const checkState = (node: Node): { all: boolean; some: boolean } => {
const children = node.children || [];
// Jika tidak ada anak → nilai hanya berdasarkan dirinya sendiri
if (children.length === 0) {
const checked = selected.includes(node.key);
return { all: checked, some: checked };
function getAllChildKeys(node: Node): string[] {
let result: string[] = [];
if (node.children) {
node.children.forEach((c) => {
result.push(c.key);
result = [...result, ...getAllChildKeys(c)];
});
}
// Rekursif ke anak
let all = selected.includes(node.key);
let some = selected.includes(node.key);
for (const c of children) {
const childState = checkState(c);
if (!childState.all) all = false;
if (childState.some) some = true;
return result;
}
// Dapatkan parentKey, jika ada
function getParentKey(key: string) {
const split = key.split(".");
if (split.length <= 1) return null;
split.pop();
return split.join(".");
}
// Update parent ke atas secara rekursif
function updateParent(next: string[], parentKey: string | null): string[] {
if (!parentKey) return next;
const allChildKeys = findAllChildKeysFromKey(parentKey);
const selectedChild = allChildKeys.filter((c) => next.includes(c));
if (selectedChild.length === 0) {
// Semua child uncheck → parent uncheck
next = next.filter((x) => x !== parentKey);
} else if (selectedChild.length === allChildKeys.length) {
// Semua child check → parent check
if (!next.includes(parentKey)) {
next.push(parentKey);
}
} else {
// Sebagian child check → parent intermediate (checked = true, rendered sebagai indeterminate)
if (!next.includes(parentKey)) {
next.push(parentKey);
}
}
return { all, some };
};
// Untuk ordering sesuai urutan JSON
const getOrderedKeys = (nodes: Node[]): string[] =>
nodes.flatMap((n) => [n.key, ...getOrderedKeys(n.children || [])]);
// Rekursif naik ke atas
return updateParent(next, getParentKey(parentKey));
}
// dapatkan child dari string key
function findAllChildKeysFromKey(parentKey: string) {
const list: string[] = [];
const RenderNode = ({ node }: { node: Node }) => {
const children = node.children || [];
function traverse(nodes: Node[]) {
nodes.forEach((n) => {
if (n.key.startsWith(parentKey + ".") && n.key !== parentKey) {
list.push(n.key);
}
if (n.children) traverse(n.children);
});
}
const state = checkState(node); // ← gunakan recursive evaluator
traverse(permissionConfig.menus);
return list;
}
const isChecked = state.all;
const isIndeterminate = !state.all && state.some;
const RenderMenu = ({ menu }: { menu: Node }) => {
const hasChild = menu.children && menu.children.length > 0;
const open = openNodes[menu.label] ?? false;
const childKeys = getAllChildKeys(menu);
const isChecked = selected.includes(menu.key);
const isIndeterminate =
!isChecked &&
selected.some(
(x) =>
typeof x === "string" &&
x.startsWith(menu.key + ".")
);
const showChildren = open[node.key] ?? false;
function handleCheck() {
let next = [...selected];
// Ambil semua key anak + parent
const collectKeys = (n: Node): string[] => {
if (!n.children) return [n.key];
return [n.key, ...n.children.flatMap(collectKeys)];
};
if (childKeys.length > 0) {
// klik parent
if (!isChecked) {
next = [...new Set([...next, menu.key, ...childKeys])];
} else {
next = next.filter((x) => x !== menu.key && !childKeys.includes(x));
}
const allKeys = collectKeys(node);
const toggleCheck = (checked: boolean) => {
let updated = new Set(selected);
if (checked) {
// parent + semua child
allKeys.forEach((k) => updated.add(k));
} else {
// hilangkan parent + semua child
allKeys.forEach((k) => updated.delete(k));
next = updateParent(next, getParentKey(menu.key));
onChange(next);
return;
}
// ⬇⬇⬇ PERBAIKAN PENTING ⬇⬇⬇
//
// Jika node indeterminate → parent harus tetap ada di selected
//
if (isIndeterminate) {
updated.add(node.key);
}
// Jika semua child tercentang → parent harus checked
// klik child
if (isChecked) {
updated.add(node.key);
next = next.filter((x) => x !== menu.key);
} else {
next.push(menu.key);
}
onChange([...updated]);
};
next = updateParent(next, getParentKey(menu.key));
onChange(next);
}
return (
<Stack gap={4} pl="xs">
<Group wrap="nowrap">
{children.length > 0 ? (
<ActionIcon variant="subtle" onClick={() => toggle(node.key)}>
{showChildren ? <IconChevronDown size={16} /> : <IconChevronRight size={16} />}
<Stack gap={4}>
<Group gap="xs">
{menu.children && menu.children.length > 0 ? (
<ActionIcon
variant="subtle"
onClick={() => toggleNode(menu.label)}
>
{openNodes[menu.label] ? (
<IconChevronDown size={16} />
) : (
<IconChevronRight size={16} />
)}
</ActionIcon>
) : (
<div style={{ width: 24 }} />
<div style={{ width: 28 }} />
)}
<Checkbox
label={node.label}
label={menu.label}
checked={isChecked}
indeterminate={isIndeterminate}
onChange={(e) => toggleCheck(e.target.checked)}
onChange={handleCheck}
/>
</Group>
{children.length > 0 && (
<Collapse in={showChildren}>
{menu.children && (
<Collapse in={open}>
<Stack gap={4} pl="md">
{children.map((c) => (
<RenderNode key={c.key} node={c} />
{menu.children.map((child) => (
<RenderMenu key={child.key} menu={child} />
))}
</Stack>
</Collapse>
@@ -130,14 +166,11 @@ export default function PermissionTree({
);
};
return (
<Stack>
<Text size="sm">Hak Akses</Text>
{permissionConfig.menus.map((menu: Node) => (
<RenderNode key={menu.key} node={menu} />
<RenderMenu key={menu.key} menu={menu} />
))}
</Stack>
);

View File

@@ -72,13 +72,13 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
});
notification({
title: "Success",
message: "Your user have been saved",
message: "Your role have been saved",
type: "success",
});
} else {
notification({
title: "Error",
message: "Failed to create user ",
message: "Failed to create role",
type: "error",
});
}
@@ -86,7 +86,7 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
console.error(error);
notification({
title: "Error",
message: "Failed to create user",
message: "Failed to create role",
type: "error",
});
} finally {
@@ -97,19 +97,19 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
async function handleEdit() {
try {
setBtnLoading(true);
const res = await apiFetch.api.pengaduan.category.update.post(dataEdit);
const res = await apiFetch.api.user["role-update"].post(dataEdit as any);
if (res.status === 200) {
mutate();
close();
notification({
title: "Success",
message: "Your category have been saved",
message: "Your role have been saved",
type: "success",
});
} else {
notification({
title: "Error",
message: "Failed to edit category",
message: "Failed to edit role",
type: "error",
});
}
@@ -117,7 +117,7 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
console.error(error);
notification({
title: "Error",
message: "Failed to edit category",
message: "Failed to edit role",
type: "error",
});
} finally {
@@ -156,16 +156,10 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
}
}
function chooseEdit({
data,
}: {
data: {
id: string;
name: string;
permissions: [];
};
}) {
setDataEdit(data);
function chooseEdit({ data }: { data: { id: string; name: string; permissions: []; }; }) {
setDataEdit({
id: data.id, name: data.name, permissions: data.permissions ? data.permissions : []
});
open();
}
@@ -185,7 +179,6 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
}
}
console.log("dataTambah", dataTambah);
useShallowEffect(() => {
if (dataEdit.name.length > 0) {
@@ -200,11 +193,11 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
opened={opened}
onClose={close}
title={"Edit"}
centered
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
size={"lg"}
>
<Stack gap="ld">
<Input.Wrapper label="Edit Kategori">
<Input.Wrapper label="Nama Role">
<Input
value={dataEdit.name}
onChange={(e) =>
@@ -216,6 +209,12 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
}
/>
</Input.Wrapper>
<PermissionTree
selected={dataEdit.permissions}
onChange={(permissions) => {
setDataEdit({ ...dataEdit, permissions: permissions as never[] });
}}
/>
<Group justify="center" grow>
<Button variant="light" onClick={close}>
Batal
@@ -223,7 +222,11 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
<Button
variant="filled"
onClick={handleEdit}
disabled={btnDisable}
disabled={
btnDisable ||
dataEdit.name.length < 1 ||
dataEdit.permissions?.length < 1
}
loading={btnLoading}
>
Simpan
@@ -238,10 +241,11 @@ export default function UserRoleSetting({ permissions }: { permissions: JsonValu
onClose={closeTambah}
title={"Tambah"}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
size={"lg"}
>
<Stack gap="ld">
<Input.Wrapper
label="Nama"
label="Nama Role"
description=""
error={error.name ? "Field is required" : ""}
>

View File

@@ -24,41 +24,41 @@
},
{
"key": "pengaduan.antrian",
"label": "Antrian",
"label": "Detail pengaduan dengan status antrian",
"default": true,
"children": [
{
"key": "pengaduan.antrian.tolak",
"label": "Menolak",
"label": "Menolak pengaduan",
"default": true
},
{
"key": "pengaduan.antrian.terima",
"label": "Menerima",
"label": "Menerima pengaduan",
"default": true
}
]
},
{
"key": "pengaduan.diterima",
"label": "Diterima",
"label": "Detail pengaduan dengan status diterima",
"default": true,
"children": [
{
"key": "pengaduan.diterima.dikerjakan",
"label": "Dikerjakan",
"label": "Menegerjakan pengaduan",
"default": true
}
]
},
{
"key": "pengaduan.dikerjakan",
"label": "Dikerjakan",
"label": "Detail pengaduan dengan status dikerjakan",
"default": true,
"children": [
{
"key": "pengaduan.dikerjakan.selesai",
"label": "Diselesaikan",
"label": "Menyelesaikan pengaduan",
"default": true
}
]
@@ -77,34 +77,34 @@
},
{
"key": "pelayanan.antrian",
"label": "Antrian",
"label": "Detail pelayanan dengan status antrian",
"default": true,
"children": [
{
"key": "pelayanan.antrian.tolak",
"label": "Menolak",
"label": "Menolak pelayanan",
"default": true
},
{
"key": "pelayanan.antrian.terima",
"label": "Menerima",
"label": "Menerima pelayanan",
"default": true
}
]
},
{
"key": "pelayanan.diterima",
"label": "Diterima",
"label": "Detail pelayanan dengan status diterima",
"default": true,
"children": [
{
"key": "pelayanan.diterima.tolak",
"label": "Menolak",
"label": "Menolak pelayanan",
"default": true
},
{
"key": "pelayanan.diterima.setujui",
"label": "Menyetujui",
"label": "Menyetujui pelayanan",
"default": true
}
]
@@ -300,7 +300,7 @@
"default": true,
"children": [
{
"key": "credential.viewØ",
"key": "credential.view",
"label": "View List",
"default": true
}

View File

@@ -263,9 +263,18 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
<IconPhotoScan size={20} />
<Text size="md">Gambar</Text>
</Group>
<Anchor href="#" onClick={() => { }}>
Lihat Gambar
</Anchor>
{
data?.image != null && data?.image != ""
?
<Anchor href="#" onClick={() => { }}>
Lihat Gambar
</Anchor>
:
<Text size="md" c="white">
-
</Text>
}
</Flex>
</Stack>
</Grid.Col>

View File

@@ -107,11 +107,11 @@ const PengaduanRoute = new Elysia({
// --- PENGADUAN ---
.post("/create", async ({ body }) => {
const { judulPengaduan, detailPengaduan, lokasi, namaGambar, kategoriId, wargaId, noTelepon } = body
const { judulPengaduan, detailPengaduan, lokasi, namaGambar, kategoriId, namaWarga, noTelepon } = body
let imageFix = namaGambar
const noPengaduan = await generateNoPengaduan()
let idCategoryFix = kategoriId
let idWargaFix = wargaId
let idWargaFix = ""
if (idCategoryFix) {
const category = await prisma.categoryPengaduan.findUnique({
@@ -138,38 +138,25 @@ const PengaduanRoute = new Elysia({
idCategoryFix = "lainnya"
}
const warga = await prisma.warga.findUnique({
const nomorHP = normalizePhoneNumber({ phone: noTelepon })
const dataWarga = await prisma.warga.upsert({
where: {
id: wargaId,
phone: nomorHP
},
create: {
name: namaWarga,
phone: nomorHP,
},
update: {
name: namaWarga,
},
select: {
id: true
}
})
if (!warga) {
const nomorHP = normalizePhoneNumber({ phone: noTelepon })
const cariWarga = await prisma.warga.findUnique({
where: {
phone: nomorHP,
}
})
idWargaFix = dataWarga.id
if (!cariWarga) {
const wargaCreate = await prisma.warga.create({
data: {
name: wargaId,
phone: nomorHP,
},
select: {
id: true
}
})
idWargaFix = wargaCreate.id
} else {
idWargaFix = cariWarga.id
}
}
const pengaduan = await prisma.pengaduan.create({
data: {
@@ -228,9 +215,9 @@ const PengaduanRoute = new Elysia({
description: "ID atau nama kategori pengaduan (contoh: kebersihan, keamanan, lainnya)"
})),
wargaId: t.Optional(t.String({
namaWarga: t.Optional(t.String({
examples: ["budiman"],
description: "ID unik warga yang melapor (jika sudah terdaftar)"
description: "Nama warga yang melapor"
})),
noTelepon: t.String({
@@ -242,23 +229,7 @@ const PengaduanRoute = new Elysia({
detail: {
summary: "Buat Pengaduan Warga",
description: `
Endpoint ini digunakan untuk membuat data pengaduan (laporan) baru dari warga.
Alur proses:
1. Sistem memvalidasi kategori pengaduan berdasarkan ID.
- Jika ID kategori tidak ditemukan, sistem akan mencari berdasarkan nama kategori.
- Jika tetap tidak ditemukan, kategori akan diset menjadi "lainnya".
2. Sistem memvalidasi data warga berdasarkan ID.
- Jika warga tidak ditemukan, sistem akan mencari berdasarkan nomor telepon.
- Jika tetap tidak ditemukan, data warga baru akan dibuat secara otomatis.
3. Sistem menghasilkan nomor pengaduan unik (noPengaduan).
4. Data pengaduan akan disimpan ke database, termasuk judul, detail, lokasi, gambar (opsional), dan data warga.
5. Sistem juga membuat catatan riwayat awal pengaduan dengan deskripsi "Pengaduan dibuat".
Respon:
- success: true jika pengaduan berhasil dibuat.
- message: berisi pesan sukses dan nomor pengaduan yang dapat digunakan untuk melacak status pengaduan.`,
description: `Endpoint ini digunakan untuk membuat data pengaduan (laporan) baru dari warga`,
tags: ["mcp"]
}
})
@@ -545,8 +516,8 @@ Respon:
folder: t.String(),
}),
detail: {
summary: "Upload File",
description: "Tool untuk upload file ke Seafile",
summary: "Upload File (FormData)",
description: "Tool untuk upload file ke folder tujuan dengan memakai FormData",
tags: ["mcp"],
consumes: ["multipart/form-data"]
},

View File

@@ -159,6 +159,9 @@ const UserRoute = new Elysia({
const data = await prisma.role.findMany({
where: {
isActive: true
},
orderBy: {
name: "asc"
}
})
return data
@@ -193,11 +196,11 @@ const UserRoute = new Elysia({
}
})
.post("role-create", async ({ body }) => {
const { name, permission } = body;
const { name, permissions } = body;
const create = await prisma.role.create({
data: {
name,
permissions: permission
permissions: permissions
}
});
@@ -208,7 +211,7 @@ const UserRoute = new Elysia({
}, {
body: t.Object({
name: t.String({ minLength: 1, error: "name is required" }),
permission: t.Array(t.Any(), { minItems: 1, error: "permission is required" })
permissions: t.Any(),
}),
detail: {
summary: "create-role",
@@ -216,14 +219,14 @@ const UserRoute = new Elysia({
}
})
.post("/role-update", async ({ body }) => {
const { id, name, permission } = body;
const { id, name, permissions } = body;
const update = await prisma.role.update({
where: {
id
},
data: {
name,
permissions: permission
permissions
}
});
@@ -235,7 +238,7 @@ const UserRoute = new Elysia({
body: t.Object({
id: t.String({ minLength: 1, error: "id is required" }),
name: t.String({ minLength: 1, error: "name is required" }),
permission: t.Array(t.String(), { minItems: 1, error: "permission is required" })
permissions: t.Any()
}),
detail: {
summary: "update-role",