upd: form surat

Deskripsi:
- api detail categori list
- form awal buat surat

No Issues
This commit is contained in:
2025-12-16 17:38:13 +08:00
parent a13e51a724
commit 7c6e4ac9eb
38 changed files with 4236 additions and 2941 deletions

View File

@@ -1,177 +1,183 @@
import permissionConfig from "@/lib/listPermission.json";
import { ActionIcon, Checkbox, Collapse, Group, Stack, Text } from "@mantine/core";
import {
ActionIcon,
Checkbox,
Collapse,
Group,
Stack,
Text,
} from "@mantine/core";
import { IconChevronDown, IconChevronRight } from "@tabler/icons-react";
import { useState } from "react";
interface Node {
label: string;
key: string;
children?: Node[];
label: string;
key: string;
children?: Node[];
}
export default function PermissionTree({
selected,
onChange,
selected,
onChange,
}: {
selected: string[];
onChange: (val: string[]) => void;
selected: string[];
onChange: (val: string[]) => void;
}) {
// Ambil semua child dari node
const [openNodes, setOpenNodes] = useState<Record<string, boolean>>({});
// Ambil semua child dari node
const [openNodes, setOpenNodes] = useState<Record<string, boolean>>({});
function toggleNode(label: string) {
setOpenNodes(prev => ({ ...prev, [label]: !prev[label] }));
}
function toggleNode(label: string) {
setOpenNodes((prev) => ({ ...prev, [label]: !prev[label] }));
}
function getAllChildKeys(node: Node): string[] {
let result: string[] = [];
if (node.children) {
node.children.forEach((c) => {
result.push(c.key);
result = [...result, ...getAllChildKeys(c)];
});
function getAllChildKeys(node: Node): string[] {
let result: string[] = [];
if (node.children) {
node.children.forEach((c) => {
result.push(c.key);
result = [...result, ...getAllChildKeys(c)];
});
}
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);
}
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);
}
} else {
// Sebagian child check → parent intermediate (checked = true, rendered sebagai indeterminate)
if (!next.includes(parentKey)) {
next.push(parentKey);
}
}
// Rekursif naik ke atas
return updateParent(next, getParentKey(parentKey));
}
// Rekursif naik ke atas
return updateParent(next, getParentKey(parentKey));
}
// dapatkan child dari string key
function findAllChildKeysFromKey(parentKey: string) {
const list: string[] = [];
// dapatkan child dari string key
function findAllChildKeysFromKey(parentKey: string) {
const list: string[] = [];
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);
});
}
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);
});
}
traverse(permissionConfig.menus);
return list;
}
traverse(permissionConfig.menus);
return list;
}
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 + ".")
);
function handleCheck() {
let next = [...selected];
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));
}
next = updateParent(next, getParentKey(menu.key));
onChange(next);
return;
}
// klik child
if (isChecked) {
next = next.filter((x) => x !== menu.key);
} else {
next.push(menu.key);
}
next = updateParent(next, getParentKey(menu.key));
onChange(next);
}
return (
<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: 28 }} />
)}
<Checkbox
label={menu.label}
checked={isChecked}
indeterminate={isIndeterminate}
onChange={handleCheck}
/>
</Group>
{menu.children && (
<Collapse in={open}>
<Stack gap={4} pl="md">
{menu.children.map((child) => (
<RenderMenu key={child.key} menu={child} />
))}
</Stack>
</Collapse>
)}
</Stack>
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 + "."),
);
};
return (
<Stack>
<Text size="sm">Hak Akses</Text>
{permissionConfig.menus.filter((menu: Node) => !menu.key.startsWith("api") && !menu.key.startsWith("credential")).map((menu: Node) => (
<RenderMenu key={menu.key} menu={menu} />
))}
function handleCheck() {
let next = [...selected];
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));
}
next = updateParent(next, getParentKey(menu.key));
onChange(next);
return;
}
// klik child
if (isChecked) {
next = next.filter((x) => x !== menu.key);
} else {
next.push(menu.key);
}
next = updateParent(next, getParentKey(menu.key));
onChange(next);
}
return (
<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: 28 }} />
)}
<Checkbox
label={menu.label}
checked={isChecked}
indeterminate={isIndeterminate}
onChange={handleCheck}
/>
</Group>
{menu.children && (
<Collapse in={open}>
<Stack gap={4} pl="md">
{menu.children.map((child) => (
<RenderMenu key={child.key} menu={child} />
))}
</Stack>
</Collapse>
)}
</Stack>
);
);
};
return (
<Stack>
<Text size="sm">Hak Akses</Text>
{permissionConfig.menus
.filter(
(menu: Node) =>
!menu.key.startsWith("api") && !menu.key.startsWith("credential"),
)
.map((menu: Node) => (
<RenderMenu key={menu.key} menu={menu} />
))}
</Stack>
);
}