Compare commits
2 Commits
nico/19-no
...
nico/20-no
| Author | SHA1 | Date | |
|---|---|---|---|
| 0dff8f3254 | |||
| 78b8aa74cd |
@@ -1,23 +1,26 @@
|
||||
[
|
||||
{
|
||||
"id": "role-1",
|
||||
"name": "ADMIN DESA",
|
||||
"description": "Administrator Desa",
|
||||
"permissions": ["manage_users", "manage_content", "view_reports"],
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"id": "role-2",
|
||||
"name": "ADMIN KESEHATAN",
|
||||
"description": "Administrator Bidang Kesehatan",
|
||||
"permissions": ["manage_health_data", "view_reports"],
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"id": "role-3",
|
||||
"name": "ADMIN SEKOLAH",
|
||||
"description": "Administrator Sekolah",
|
||||
"permissions": ["manage_school_data", "view_reports"],
|
||||
"isActive": true
|
||||
}
|
||||
]
|
||||
{
|
||||
"id": "0",
|
||||
"name": "SUPER ADMIN",
|
||||
"description": "Administrator",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"id": "1",
|
||||
"name": "ADMIN DESA",
|
||||
"description": "Administrator Desa",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "ADMIN KESEHATAN",
|
||||
"description": "Administrator Bidang Kesehatan",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"name": "ADMIN PENDIDIKAN",
|
||||
"description": "Administrator Bidang Pendidikan",
|
||||
"isActive": true
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": "user-1",
|
||||
"nama": "Admin Desa",
|
||||
"nomor": "089647037426",
|
||||
"roleId": "role-1",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"id": "user-2",
|
||||
"nama": "Admin Kesehatan",
|
||||
"nomor": "082339004198",
|
||||
"roleId": "role-2",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"id": "user-3",
|
||||
"nama": "Admin Sekolah",
|
||||
"nomor": "085237157222",
|
||||
"roleId": "role-3",
|
||||
"isActive": true
|
||||
}
|
||||
]
|
||||
@@ -2181,7 +2181,6 @@ model Role {
|
||||
id String @id @default(cuid())
|
||||
name String @unique // ADMIN_DESA, ADMIN_KESEHATAN, ADMIN_SEKOLAH
|
||||
description String?
|
||||
permissions Json // Menyimpan permission dalam format JSON
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@ -2200,17 +2199,6 @@ model KodeOtp {
|
||||
otp Int
|
||||
}
|
||||
|
||||
// Tabel untuk menyimpan permission
|
||||
model Permission {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
description String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("permissions")
|
||||
}
|
||||
|
||||
model UserSession {
|
||||
id String @id @default(cuid())
|
||||
token String
|
||||
|
||||
@@ -54,14 +54,13 @@ import tujuanProgram2 from "./data/pendidikan/pendidikan-non-formal/tujuan-progr
|
||||
import programUnggulan from "./data/pendidikan/program-pendidikan-anak/program-unggulan.json";
|
||||
import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-program.json";
|
||||
import roles from "./data/user/roles.json";
|
||||
import users from "./data/user/users.json";
|
||||
import fileStorage from "./data/file-storage.json";
|
||||
import jenjangPendidikan from "./data/pendidikan/info-sekolah/jenjang-pendidikan.json";
|
||||
import seedAssets from "./seed_assets";
|
||||
import { safeSeedUnique } from "./safeseedUnique";
|
||||
|
||||
(async () => {
|
||||
// =========== USER & ROLE ===========
|
||||
// =========== ROLE ===========
|
||||
// In your seed.ts
|
||||
// =========== ROLES ===========
|
||||
console.log("🔄 Seeding roles...");
|
||||
@@ -69,35 +68,12 @@ import { safeSeedUnique } from "./safeseedUnique";
|
||||
await safeSeedUnique("role", { id: r.id }, {
|
||||
name: r.name,
|
||||
description: r.description,
|
||||
permissions: r.permissions,
|
||||
isActive: r.isActive,
|
||||
});
|
||||
}
|
||||
|
||||
console.log("✅ Roles seeded");
|
||||
|
||||
// =========== USERS ===========
|
||||
console.log("🔄 Seeding users...");
|
||||
for (const u of users) {
|
||||
// First verify the role exists
|
||||
const roleExists = await prisma.role.findUnique({
|
||||
where: { id: u.roleId },
|
||||
});
|
||||
|
||||
if (!roleExists) {
|
||||
console.error(`❌ Role with id ${u.roleId} not found for user ${u.nama}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await safeSeedUnique("user", { id: u.id }, {
|
||||
username: u.nama,
|
||||
nomor: u.nomor,
|
||||
roleId: u.roleId,
|
||||
isActive: u.isActive,
|
||||
});
|
||||
}
|
||||
console.log("✅ Users seeded");
|
||||
|
||||
// =========== FILE STORAGE ===========
|
||||
console.log("🔄 Seeding file storage...");
|
||||
for (const f of fileStorage) {
|
||||
|
||||
@@ -90,27 +90,35 @@ const userState = proxy({
|
||||
}
|
||||
},
|
||||
},
|
||||
updateActive: {
|
||||
update: {
|
||||
loading: false,
|
||||
async submit(id: string, isActive: boolean) {
|
||||
|
||||
async submit(payload: { id: string; isActive?: boolean; roleId?: string }) {
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await fetch(`/api/user/updt`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ id, isActive }),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.status === 200 && data.success) {
|
||||
toast.success(data.message);
|
||||
userState.findMany.load(userState.findMany.page, 10, userState.findMany.search);
|
||||
|
||||
// refresh list
|
||||
userState.findMany.load(
|
||||
userState.findMany.page,
|
||||
10,
|
||||
userState.findMany.search
|
||||
);
|
||||
} else {
|
||||
toast.error(data.message || "Gagal update status user");
|
||||
toast.error(data.message || "Gagal update user");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast.error("Gagal update status user");
|
||||
toast.error("Gagal update user");
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
@@ -120,12 +128,10 @@ const userState = proxy({
|
||||
|
||||
const templateRole = z.object({
|
||||
name: z.string().min(1, "Nama harus diisi"),
|
||||
permissions: z.array(z.string()).min(1, "Permission harus diisi"),
|
||||
});
|
||||
|
||||
const defaultRole = {
|
||||
name: "",
|
||||
permissions: [] as string[],
|
||||
};
|
||||
|
||||
const roleState = proxy({
|
||||
@@ -237,7 +243,7 @@ const roleState = proxy({
|
||||
toast.warn("ID tidak valid");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/role/${id}`, {
|
||||
method: "GET",
|
||||
@@ -245,31 +251,25 @@ const roleState = proxy({
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
|
||||
if (result?.success) {
|
||||
const data = result.data;
|
||||
this.id = data.id;
|
||||
this.form = {
|
||||
|
||||
// langsung set melalui root state, bukan this
|
||||
roleState.update.id = data.id;
|
||||
roleState.update.form = {
|
||||
name: data.name,
|
||||
permissions: data.permissions,
|
||||
};
|
||||
return data; // Return the loaded data
|
||||
} else {
|
||||
throw new Error(result?.message || "Gagal memuat data");
|
||||
|
||||
return data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading role:", error);
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Gagal memuat data"
|
||||
);
|
||||
return null;
|
||||
toast.error("Gagal memuat data");
|
||||
}
|
||||
},
|
||||
},
|
||||
async update() {
|
||||
const cek = templateRole.safeParse(roleState.update.form);
|
||||
if (!cek.success) {
|
||||
@@ -290,7 +290,6 @@ const roleState = proxy({
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: this.form.name,
|
||||
permissions: this.form.permissions,
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Box, Button, Loader, Paper, PinInput, Stack, Text, Title } from '@manti
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { authStore } from '@/store/authStore';
|
||||
|
||||
export default function Validasi() {
|
||||
const router = useRouter();
|
||||
@@ -53,10 +54,17 @@ export default function Validasi() {
|
||||
setLoading(true);
|
||||
const verifyResult = await apiFetchVerifyOtp({ nomor, otp, kodeId });
|
||||
|
||||
if (verifyResult.success) {
|
||||
if (verifyResult.success && verifyResult.user) {
|
||||
// ✅ SET USER KE STORE
|
||||
authStore.setUser({
|
||||
id: verifyResult.user.id,
|
||||
name: verifyResult.user.name,
|
||||
roleId: Number(verifyResult.user.roleId),
|
||||
});
|
||||
|
||||
cleanupStorage();
|
||||
router.push('/admin/landing-page/profil/program-inovasi');
|
||||
return; // ✅ HENTIKAN eksekusi di sini
|
||||
return;
|
||||
}
|
||||
|
||||
// Hanya coba registrasi jika akun tidak ditemukan
|
||||
@@ -76,7 +84,7 @@ export default function Validasi() {
|
||||
const regData = await regRes.json();
|
||||
if (regData.success) {
|
||||
cleanupStorage();
|
||||
router.push('/admin/landing-page/profil/program-inovasi');
|
||||
router.push('/waiting-room'); // ✅
|
||||
} else {
|
||||
toast.error(regData.message || 'Registrasi gagal');
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Loader, Group, MultiSelect, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { Box, Button, Group, Loader, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
@@ -9,6 +10,7 @@ import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import user from '../../../_state/user/user-state';
|
||||
|
||||
|
||||
function EditRole() {
|
||||
const stateRole = useProxy(user.roleState);
|
||||
const router = useRouter();
|
||||
@@ -17,46 +19,37 @@ function EditRole() {
|
||||
// Controlled local state
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
permissions: [] as string[],
|
||||
});
|
||||
|
||||
const [originalData, setOriginalData] = useState({
|
||||
name: '',
|
||||
permissions: [] as string[],
|
||||
});
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// Load role data
|
||||
const loadRole = useCallback(async (id: string) => {
|
||||
try {
|
||||
const data = await stateRole.update.load(id);
|
||||
if (data) {
|
||||
setFormData({
|
||||
name: data.name || '',
|
||||
permissions: data.permissions || [],
|
||||
});
|
||||
setOriginalData({
|
||||
name: data.name || '',
|
||||
permissions: data.permissions || [],
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading role:', error);
|
||||
toast.error(error instanceof Error ? error.message : 'Gagal mengambil data role');
|
||||
const data = await stateRole.update.load(id);
|
||||
|
||||
if (data) {
|
||||
setFormData({
|
||||
name: data.name ?? '',
|
||||
});
|
||||
|
||||
setOriginalData({
|
||||
name: data.name ?? '',
|
||||
});
|
||||
}
|
||||
}, [stateRole.update]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
stateRole.findMany.load(); // Load permissions/options
|
||||
const id = params?.id as string;
|
||||
if (id) loadRole(id);
|
||||
}, [params?.id, loadRole, stateRole.findMany]);
|
||||
stateRole.findMany.load(); // load permission
|
||||
if (params?.id) loadRole(params.id as string);
|
||||
}, [params?.id]);
|
||||
|
||||
const handleResetForm = () => {
|
||||
setFormData({
|
||||
name: originalData.name,
|
||||
permissions: originalData.permissions,
|
||||
});
|
||||
toast.info("Form dikembalikan ke data awal");
|
||||
};
|
||||
@@ -66,10 +59,6 @@ function EditRole() {
|
||||
toast.error('Nama role tidak boleh kosong');
|
||||
return;
|
||||
}
|
||||
if (!formData.permissions.length) {
|
||||
toast.error('Pilih minimal satu permission');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
@@ -77,7 +66,6 @@ function EditRole() {
|
||||
stateRole.update.form = {
|
||||
...stateRole.update.form,
|
||||
name: formData.name,
|
||||
permissions: formData.permissions,
|
||||
};
|
||||
await stateRole.update.update();
|
||||
toast.success('Role berhasil diperbarui!');
|
||||
@@ -116,24 +104,7 @@ function EditRole() {
|
||||
label={<Text fw="bold" fz="sm">Nama Role</Text>}
|
||||
placeholder="Masukkan nama role"
|
||||
/>
|
||||
<MultiSelect
|
||||
value={formData.permissions}
|
||||
onChange={(val) => setFormData({ ...formData, permissions: val })}
|
||||
label={<Text fw="bold" fz="sm">Permission</Text>}
|
||||
placeholder="Pilih permission"
|
||||
data={
|
||||
stateRole.findMany.data?.map((v) => ({
|
||||
value: v.id,
|
||||
label: v.name,
|
||||
})) || []
|
||||
}
|
||||
clearable
|
||||
searchable
|
||||
required
|
||||
error={!formData.permissions.length ? 'Pilih minimal satu permission' : undefined}
|
||||
/>
|
||||
|
||||
<Group justify="right">
|
||||
<Group justify="right">
|
||||
{/* Tombol Batal */}
|
||||
<Button
|
||||
variant="outline"
|
||||
|
||||
@@ -5,9 +5,8 @@ import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
MultiSelect,
|
||||
Paper,
|
||||
Loader,
|
||||
Paper,
|
||||
Stack,
|
||||
TextInput,
|
||||
Title
|
||||
@@ -15,9 +14,9 @@ import {
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import user from '../../../_state/user/user-state';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
|
||||
export default function CreateRole() {
|
||||
@@ -31,8 +30,7 @@ export default function CreateRole() {
|
||||
|
||||
const resetForm = () => {
|
||||
stateRole.create.form = {
|
||||
name: '',
|
||||
permissions: [],
|
||||
name: ''
|
||||
};
|
||||
};
|
||||
|
||||
@@ -80,28 +78,6 @@ export default function CreateRole() {
|
||||
required
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<MultiSelect
|
||||
label="Permission"
|
||||
placeholder="Pilih permission"
|
||||
data={
|
||||
Array.from(
|
||||
new Set(
|
||||
stateRole.findMany.data
|
||||
?.map((item) => item.permissions)
|
||||
.flat()
|
||||
)
|
||||
)
|
||||
.filter((p): p is string => typeof p === 'string')
|
||||
.map((p) => ({ label: p, value: p }))
|
||||
}
|
||||
value={stateRole.create.form.permissions}
|
||||
onChange={(value) => (stateRole.create.form.permissions = value)}
|
||||
required
|
||||
/>
|
||||
|
||||
|
||||
</Box>
|
||||
|
||||
<Group justify="right">
|
||||
<Button
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
|
||||
import { Box, Button, Center, Group, Pagination, Paper, Select, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconCheck, IconSearch, IconX } from '@tabler/icons-react';
|
||||
import { useState } from 'react';
|
||||
@@ -28,6 +28,7 @@ function User() {
|
||||
|
||||
function ListUser({ search }: { search: string }) {
|
||||
const stateUser = useProxy(user.userState)
|
||||
const stateRole = useProxy(user.roleState)
|
||||
const [modalHapus, setModalHapus] = useState(false)
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
|
||||
@@ -51,6 +52,7 @@ function ListUser({ search }: { search: string }) {
|
||||
|
||||
|
||||
useShallowEffect(() => {
|
||||
stateRole.findMany.load()
|
||||
load(page, 10, search)
|
||||
}, [page, search])
|
||||
|
||||
@@ -92,11 +94,31 @@ function ListUser({ search }: { search: string }) {
|
||||
{item.nomor}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '20%', }}>
|
||||
<Text truncate fz="sm" c="dimmed">
|
||||
{item.role.name}
|
||||
</Text>
|
||||
<TableTd style={{ width: '20%' }}>
|
||||
<Select
|
||||
placeholder="Pilih role"
|
||||
data={stateRole.findMany.data.map((r) => ({
|
||||
label: r.name,
|
||||
value: r.id,
|
||||
}))}
|
||||
value={item.roleId} // ⬅ role milik user ini
|
||||
onChange={async (val) => {
|
||||
if (!val) return;
|
||||
|
||||
await stateUser.update.submit({
|
||||
id: item.id,
|
||||
roleId: val, // ⬅ kirim roleId
|
||||
});
|
||||
|
||||
// reload data supaya UI up-to-date
|
||||
stateUser.findMany.load(page, 10, search);
|
||||
}}
|
||||
searchable
|
||||
clearable={false} // role harus ada
|
||||
nothingFoundMessage="Role tidak ditemukan"
|
||||
/>
|
||||
</TableTd>
|
||||
|
||||
<TableTd style={{ width: '15%' }}>
|
||||
<Tooltip
|
||||
label={item.isActive ? "Nonaktifkan user" : "Aktifkan user"}
|
||||
@@ -106,8 +128,12 @@ function ListUser({ search }: { search: string }) {
|
||||
variant="light"
|
||||
color={item.isActive ? "green" : "red"}
|
||||
onClick={async () => {
|
||||
await stateUser.updateActive.submit(item.id, !item.isActive)
|
||||
stateUser.findMany.load(page, 10, search)
|
||||
await stateUser.update.submit({
|
||||
id: item.id,
|
||||
isActive: !item.isActive, // toggle
|
||||
});
|
||||
|
||||
stateUser.findMany.load(page, 10, search);
|
||||
}}
|
||||
>
|
||||
{item.isActive ? <IconCheck size={20} /> : <IconX size={20} />}
|
||||
|
||||
@@ -84,7 +84,6 @@ export const navBar = [
|
||||
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
id: "Desa",
|
||||
name: "Desa",
|
||||
@@ -336,7 +335,415 @@ export const navBar = [
|
||||
path: "/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana"
|
||||
}
|
||||
]
|
||||
}, {
|
||||
},
|
||||
{
|
||||
id: "Pendidikan",
|
||||
name: "Pendidikan",
|
||||
path: "",
|
||||
children: [
|
||||
{
|
||||
id: "Pendidikan_1",
|
||||
name: "Info Sekolah",
|
||||
path: "/admin/pendidikan/info-sekolah/jenjang-pendidikan"
|
||||
},
|
||||
{
|
||||
id: "Pendidikan_2",
|
||||
name: "Beasiswa Desa",
|
||||
path: "/admin/pendidikan/beasiswa-desa/beasiswa-pendaftar"
|
||||
},
|
||||
{
|
||||
id: "Pendidikan_3",
|
||||
name: "Program Pendidikan Anak",
|
||||
path: "/admin/pendidikan/program-pendidikan-anak/program-unggulan"
|
||||
},
|
||||
{
|
||||
id: "Pendidikan_4",
|
||||
name: "Bimbingan Belajar Desa",
|
||||
path: "/admin/pendidikan/bimbingan-belajar-desa/tujuan-program"
|
||||
},
|
||||
{
|
||||
id: "Pendidikan_5",
|
||||
name: "Pendidikan Non Formal",
|
||||
path: "/admin/pendidikan/pendidikan-non-formal/tujuan-program"
|
||||
},
|
||||
{
|
||||
id: "Pendidikan_6",
|
||||
name: "Perpustakaan Digital",
|
||||
path: "/admin/pendidikan/perpustakaan-digital/data-perpustakaan"
|
||||
},
|
||||
{
|
||||
id: "Pendidikan_7",
|
||||
name: "Data Pendidikan",
|
||||
path: "/admin/pendidikan/data-pendidikan"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "User & Role",
|
||||
name: "User & Role",
|
||||
path: "",
|
||||
children: [
|
||||
{
|
||||
id: "User",
|
||||
name: "User",
|
||||
path: "/admin/user&role/user"
|
||||
},
|
||||
{
|
||||
id: "Role",
|
||||
name: "Role",
|
||||
path: "/admin/user&role/role"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
export const role1 = [
|
||||
{
|
||||
id: "Landing Page",
|
||||
name: "Landing Page",
|
||||
path: "",
|
||||
children: [
|
||||
{
|
||||
id: "Landing_Page_1",
|
||||
name: "Profil",
|
||||
path: "/admin/landing-page/profil/program-inovasi"
|
||||
},
|
||||
{
|
||||
id: "Landing_Page_2",
|
||||
name: "Desa Anti Korupsi",
|
||||
path: "/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi"
|
||||
},
|
||||
{
|
||||
id: "Landing_Page_3",
|
||||
name: "Indeks Kepuasan Masyarakat",
|
||||
path: "/admin/landing-page/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat"
|
||||
},
|
||||
{
|
||||
id: "Landing_Page_4",
|
||||
name: "SDGs",
|
||||
path: "/admin/landing-page/SDGs"
|
||||
},
|
||||
{
|
||||
id: "Landing_Page_5",
|
||||
name: "APBDes",
|
||||
path: "/admin/landing-page/apbdes"
|
||||
},
|
||||
{
|
||||
id: "Landing_Page_6",
|
||||
name: "Prestasi Desa",
|
||||
path: "/admin/landing-page/prestasi-desa/list-prestasi-desa"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "PPID",
|
||||
name: "PPID",
|
||||
path: "",
|
||||
children: [
|
||||
{
|
||||
id: "PPID_1",
|
||||
name: "Profil PPID",
|
||||
path: "/admin/ppid/profil-ppid"
|
||||
},
|
||||
{
|
||||
id: "PPID_2",
|
||||
name: "Struktur PPID",
|
||||
path: "/admin/ppid/struktur-ppid/pegawai"
|
||||
},
|
||||
{
|
||||
id: "PPID_3",
|
||||
name: "Visi Misi PPID",
|
||||
path: "/admin/ppid/visi-misi-ppid"
|
||||
},
|
||||
{
|
||||
id: "PPID_4",
|
||||
name: "Dasar Hukum",
|
||||
path: "/admin/ppid/dasar-hukum"
|
||||
},
|
||||
{
|
||||
id: "PPID_5",
|
||||
name: "Permohonan Informasi Publik",
|
||||
path: "/admin/ppid/permohonan-informasi-publik"
|
||||
},
|
||||
{
|
||||
id: "PPID_6",
|
||||
name: "Permohonan Keberatan Informasi Publik",
|
||||
path: "/admin/ppid/permohonan-keberatan-informasi-publik"
|
||||
},
|
||||
{
|
||||
id: "PPID_7",
|
||||
name: "Daftar Informasi Publik",
|
||||
path: "/admin/ppid/daftar-informasi-publik"
|
||||
},
|
||||
{
|
||||
id: "PPID_8",
|
||||
name: "Indeks Kepuasan Masyarakat",
|
||||
path: "/admin/ppid/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat"
|
||||
},
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "Desa",
|
||||
name: "Desa",
|
||||
path: "",
|
||||
children: [
|
||||
{
|
||||
id: "Desa_1",
|
||||
name: "Profile",
|
||||
path: "/admin/desa/profile/profile-desa"
|
||||
},
|
||||
{
|
||||
id: "Desa_2",
|
||||
name: "Potensi",
|
||||
path: "/admin/desa/potensi/list-potensi"
|
||||
},
|
||||
{
|
||||
id: "Desa_3",
|
||||
name: "Berita",
|
||||
path: "/admin/desa/berita/list-berita"
|
||||
},
|
||||
{
|
||||
id: "Desa_4",
|
||||
name: "Pengumuman",
|
||||
path: "/admin/desa/pengumuman/list-pengumuman"
|
||||
},
|
||||
{
|
||||
id: "Desa_5",
|
||||
name: "Gallery",
|
||||
path: "/admin/desa/gallery/foto"
|
||||
},
|
||||
{
|
||||
id: "Desa_6",
|
||||
name: "Layanan",
|
||||
path: "/admin/desa/layanan/pelayanan_surat_keterangan"
|
||||
},
|
||||
{
|
||||
id: "Desa_7",
|
||||
name: "Penghargaan",
|
||||
path: "/admin/desa/penghargaan"
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "Keamanan",
|
||||
name: "Keamanan",
|
||||
path: "",
|
||||
children: [
|
||||
{
|
||||
id: "Keamanan_1",
|
||||
name: "Keamanan Lingkungan (Pecalang/Patwal)",
|
||||
path: "/admin/keamanan/keamanan-lingkungan-pecalang-patwal"
|
||||
},
|
||||
{
|
||||
id: "Keamanan_2",
|
||||
name: "Polsek Terdekat",
|
||||
path: "/admin/keamanan/polsek-terdekat"
|
||||
},
|
||||
{
|
||||
id: "Keamanan_3",
|
||||
name: "Kontak Darurat",
|
||||
path: "/admin/keamanan/kontak-darurat/kontak-darurat-keamanan"
|
||||
},
|
||||
{
|
||||
id: "Keamanan_4",
|
||||
name: "Pencegahan Kriminalitas",
|
||||
path: "/admin/keamanan/pencegahan-kriminalitas"
|
||||
},
|
||||
{
|
||||
id: "Keamanan_5",
|
||||
name: "Laporan Publik",
|
||||
path: "/admin/keamanan/laporan-publik"
|
||||
},
|
||||
{
|
||||
id: "Keamanan_6",
|
||||
name: "Tips Keamanan",
|
||||
path: "/admin/keamanan/tips-keamanan"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "Ekonomi",
|
||||
name: "Ekonomi",
|
||||
path: "",
|
||||
children: [
|
||||
{
|
||||
id: "Ekonomi_1",
|
||||
name: "Pasar Desa",
|
||||
path: "/admin/ekonomi/pasar-desa/produk-pasar-desa"
|
||||
},
|
||||
{
|
||||
id: "Ekonomi_2",
|
||||
name: "Lowongan Kerja Lokal",
|
||||
path: "/admin/ekonomi/lowongan-kerja-lokal"
|
||||
},
|
||||
{
|
||||
id: "Ekonomi_3",
|
||||
name: "Struktur Organisasi Dan Sk Pengurus Bumdesa",
|
||||
path: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai"
|
||||
},
|
||||
{
|
||||
id: "Ekonomi_4",
|
||||
name: "PADesa (Pendapatan Asli Desa)",
|
||||
path: "/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa"
|
||||
},
|
||||
{
|
||||
id: "Ekonomi_5",
|
||||
name: "Jumlah Pengangguran",
|
||||
path: "/admin/ekonomi/jumlah-pengangguran"
|
||||
},
|
||||
{
|
||||
id: "Ekonomi_6",
|
||||
name: "Jumlah penduduk usia kerja yang menganggur",
|
||||
path: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia"
|
||||
},
|
||||
{
|
||||
id: "Ekonomi_7",
|
||||
name: "Jumlah Penduduk Miskin",
|
||||
path: "/admin/ekonomi/jumlah-penduduk-miskin"
|
||||
},
|
||||
{
|
||||
id: "Ekonomi_8",
|
||||
name: "Program Kemiskinan",
|
||||
path: "/admin/ekonomi/program-kemiskinan"
|
||||
},
|
||||
{
|
||||
id: "Ekonomi_9",
|
||||
name: "Sektor Unggulan Desa",
|
||||
path: "/admin/ekonomi/sektor-unggulan-desa"
|
||||
},
|
||||
{
|
||||
id: "Ekonomi_10",
|
||||
name: "Demografi Pekerjaan",
|
||||
path: "/admin/ekonomi/demografi-pekerjaan"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "Inovasi",
|
||||
name: "Inovasi",
|
||||
path: "",
|
||||
children: [
|
||||
{
|
||||
id: "Inovasi_1",
|
||||
name: "Desa Digital/Smart Village",
|
||||
path: "/admin/inovasi/desa-digital-smart-village"
|
||||
},
|
||||
{
|
||||
id: "Inovasi_2",
|
||||
name: "Layanan Online Desa",
|
||||
path: "/admin/inovasi/layanan-online-desa/administrasi-online"
|
||||
},
|
||||
{
|
||||
id: "Inovasi_3",
|
||||
name: "Program Kreatif Desa",
|
||||
path: "/admin/inovasi/program-kreatif-desa"
|
||||
},
|
||||
{
|
||||
id: "Inovasi_4",
|
||||
name: "Kolaborasi Inovasi",
|
||||
path: "/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi"
|
||||
},
|
||||
{
|
||||
id: "Inovasi_5",
|
||||
name: "Info Teknologi Tepat Guna",
|
||||
path: "/admin/inovasi/info-teknologi-tepat-guna"
|
||||
},
|
||||
{
|
||||
id: "Inovasi_6",
|
||||
name: "Ajukan Ide Inovatif",
|
||||
path: "/admin/inovasi/ajukan-ide-inovatif"
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "Lingkungan",
|
||||
name: "Lingkungan",
|
||||
path: "",
|
||||
children: [
|
||||
{
|
||||
id: "Lingkungan_1",
|
||||
name: "Pengelolaan Sampah (Bank Sampah)",
|
||||
path: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah"
|
||||
},
|
||||
{
|
||||
id: "Lingkungan_2",
|
||||
name: "Program Penghijauan",
|
||||
path: "/admin/lingkungan/program-penghijauan"
|
||||
},
|
||||
{
|
||||
id: "Lingkungan_3",
|
||||
name: "Data Lingkungan Desa",
|
||||
path: "/admin/lingkungan/data-lingkungan-desa"
|
||||
},
|
||||
{
|
||||
id: "Lingkungan_4",
|
||||
name: "Gotong Royong",
|
||||
path: "/admin/lingkungan/gotong-royong/kegiatan-desa"
|
||||
},
|
||||
{
|
||||
id: "Lingkungan_5",
|
||||
name: "Edukasi Lingkungan",
|
||||
path: "/admin/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan"
|
||||
},
|
||||
{
|
||||
id: "Lingkungan_6",
|
||||
name: "Konservasi Adat Bali",
|
||||
path: "/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
export const role2 = [
|
||||
{
|
||||
id: "Kesehatan",
|
||||
name: "Kesehatan",
|
||||
path: "",
|
||||
children: [
|
||||
{
|
||||
id: "Kesehatan_1",
|
||||
name: "Posyandu",
|
||||
path: "/admin/kesehatan/posyandu"
|
||||
},
|
||||
{
|
||||
id: "Kesehatan_2",
|
||||
name: "Data Kesehatan Warga",
|
||||
path: "/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian"
|
||||
},
|
||||
{
|
||||
id: "Kesehatan_3",
|
||||
name: "Puskesmas",
|
||||
path: "/admin/kesehatan/puskesmas"
|
||||
},
|
||||
{
|
||||
id: "Kesehatan_4",
|
||||
name: "Program Kesehatan",
|
||||
path: "/admin/kesehatan/program-kesehatan"
|
||||
},
|
||||
{
|
||||
id: "Kesehatan_5",
|
||||
name: "Penanganan Darurat",
|
||||
path: "/admin/kesehatan/penanganan-darurat"
|
||||
},
|
||||
{
|
||||
id: "Kesehatan_6",
|
||||
name: "Kontak Darurat",
|
||||
path: "/admin/kesehatan/kontak-darurat"
|
||||
},
|
||||
{
|
||||
id: "Kesehatan_7",
|
||||
name: "Info Wabah/Penyakit",
|
||||
path: "/admin/kesehatan/info-wabah-penyakit"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
export const role3 = [
|
||||
{
|
||||
id: "Pendidikan",
|
||||
name: "Pendidikan",
|
||||
path: "",
|
||||
|
||||
9
src/app/admin/_com/navigationByRole.ts
Normal file
9
src/app/admin/_com/navigationByRole.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// src/app/(admin)/_com/navigationByRole.ts
|
||||
import { navBar, role1, role2, role3 } from './list_PageAdmin';
|
||||
|
||||
export const navigationByRole = {
|
||||
0: navBar, // SUPERADMIN
|
||||
1: role1, // ADMIN DESA
|
||||
2: role2, // ADMIN KESEHATAN
|
||||
3: role3, // ADMIN PENDIDIKAN
|
||||
} as const;
|
||||
@@ -1,6 +1,269 @@
|
||||
// 'use client'
|
||||
|
||||
// import colors from "@/con/colors";
|
||||
// import {
|
||||
// ActionIcon,
|
||||
// AppShell,
|
||||
// AppShellHeader,
|
||||
// AppShellMain,
|
||||
// AppShellNavbar,
|
||||
// Burger,
|
||||
// Flex,
|
||||
// Group,
|
||||
// Image,
|
||||
// NavLink,
|
||||
// ScrollArea,
|
||||
// Text,
|
||||
// Tooltip,
|
||||
// rem
|
||||
// } from "@mantine/core";
|
||||
// import { useDisclosure } from "@mantine/hooks";
|
||||
// import {
|
||||
// IconChevronLeft,
|
||||
// IconChevronRight,
|
||||
// IconLogout2
|
||||
// } from "@tabler/icons-react";
|
||||
// import _ from "lodash";
|
||||
// import Link from "next/link";
|
||||
// import { useRouter, useSelectedLayoutSegments } from "next/navigation";
|
||||
// import { navBar } from "./_com/list_PageAdmin";
|
||||
|
||||
// export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
// const [opened, { toggle }] = useDisclosure();
|
||||
// const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
||||
// const router = useRouter();
|
||||
// const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s));
|
||||
|
||||
// return (
|
||||
// <AppShell
|
||||
// suppressHydrationWarning
|
||||
// header={{ height: 64 }}
|
||||
// navbar={{
|
||||
// width: { base: 260, sm: 280, lg: 300 },
|
||||
// breakpoint: 'sm',
|
||||
// collapsed: {
|
||||
// mobile: !opened,
|
||||
// desktop: !desktopOpened,
|
||||
// },
|
||||
// }}
|
||||
// padding="md"
|
||||
// >
|
||||
// <AppShellHeader
|
||||
// style={{
|
||||
// background: "linear-gradient(90deg, #ffffff, #f9fbff)",
|
||||
// borderBottom: `1px solid ${colors["blue-button"]}20`,
|
||||
// padding: '0 16px',
|
||||
// }}
|
||||
// px={{ base: 'sm', sm: 'md' }}
|
||||
// py={{ base: 'xs', sm: 'sm' }}
|
||||
// >
|
||||
// <Group w="100%" h="100%" justify="space-between" wrap="nowrap">
|
||||
// <Flex align="center" gap="sm">
|
||||
// <Image
|
||||
// src="/assets/images/darmasaba-icon.png"
|
||||
// alt="Logo Darmasaba"
|
||||
// w={{ base: 32, sm: 40 }}
|
||||
// h={{ base: 32, sm: 40 }}
|
||||
// radius="md"
|
||||
// loading="lazy"
|
||||
// style={{
|
||||
// minWidth: '32px',
|
||||
// height: 'auto',
|
||||
// }}
|
||||
// />
|
||||
// <Text
|
||||
// fw={700}
|
||||
// c={colors["blue-button"]}
|
||||
// fz={{ base: 'md', sm: 'xl' }}
|
||||
// >
|
||||
// Admin Darmasaba
|
||||
// </Text>
|
||||
// </Flex>
|
||||
|
||||
// <Group gap="xs">
|
||||
// {!desktopOpened && (
|
||||
// <Tooltip label="Buka Navigasi" position="bottom" withArrow>
|
||||
// <ActionIcon
|
||||
// variant="light"
|
||||
// radius="xl"
|
||||
// size="lg"
|
||||
// onClick={toggleDesktop}
|
||||
// color={colors["blue-button"]}
|
||||
// >
|
||||
// <IconChevronRight />
|
||||
// </ActionIcon>
|
||||
// </Tooltip>
|
||||
// )}
|
||||
|
||||
// <Burger
|
||||
// opened={opened}
|
||||
// onClick={toggle}
|
||||
// hiddenFrom="sm"
|
||||
// size="md"
|
||||
// color={colors["blue-button"]}
|
||||
// mr="xs"
|
||||
// />
|
||||
|
||||
// <Tooltip label="Kembali ke Website Desa" position="bottom" withArrow>
|
||||
// <ActionIcon
|
||||
// onClick={() => {
|
||||
// router.push("/darmasaba");
|
||||
// }}
|
||||
// color={colors["blue-button"]}
|
||||
// radius="xl"
|
||||
// size="lg"
|
||||
// variant="gradient"
|
||||
// gradient={{ from: colors["blue-button"], to: "#228be6" }}
|
||||
// >
|
||||
// <Image
|
||||
// src="/assets/images/darmasaba-icon.png"
|
||||
// alt="Logo Darmasaba"
|
||||
// w={20}
|
||||
// h={20}
|
||||
// radius="md"
|
||||
// loading="lazy"
|
||||
// style={{
|
||||
// minWidth: '20px',
|
||||
// height: 'auto',
|
||||
// }}
|
||||
// />
|
||||
// </ActionIcon>
|
||||
// </Tooltip>
|
||||
// <Tooltip label="Keluar" position="bottom" withArrow>
|
||||
// <ActionIcon
|
||||
// onClick={() => {
|
||||
// router.push("/darmasaba");
|
||||
// }}
|
||||
// color={colors["blue-button"]}
|
||||
// radius="xl"
|
||||
// size="lg"
|
||||
// variant="gradient"
|
||||
// gradient={{ from: colors["blue-button"], to: "#228be6" }}
|
||||
// >
|
||||
// <IconLogout2 size={22} />
|
||||
// </ActionIcon>
|
||||
// </Tooltip>
|
||||
// </Group>
|
||||
// </Group>
|
||||
// </AppShellHeader>
|
||||
|
||||
// <AppShellNavbar
|
||||
// component={ScrollArea}
|
||||
// style={{
|
||||
// background: "#ffffff",
|
||||
// borderRight: `1px solid ${colors["blue-button"]}20`,
|
||||
// }}
|
||||
// p={{ base: 'xs', sm: 'sm' }}
|
||||
// >
|
||||
// <AppShell.Section p="sm">
|
||||
// {navBar.map((v, k) => {
|
||||
// const isParentActive = segments.includes(_.lowerCase(v.name));
|
||||
|
||||
// return (
|
||||
// <NavLink
|
||||
// key={k}
|
||||
// defaultOpened={isParentActive}
|
||||
// c={isParentActive ? colors["blue-button"] : "gray"}
|
||||
// label={
|
||||
// <Text fw={isParentActive ? 600 : 400} fz="sm">
|
||||
// {v.name}
|
||||
// </Text>
|
||||
// }
|
||||
// style={{
|
||||
// borderRadius: rem(10),
|
||||
// marginBottom: rem(4),
|
||||
// transition: "background 150ms ease",
|
||||
// }}
|
||||
// styles={{
|
||||
// root: {
|
||||
// '&:hover': {
|
||||
// backgroundColor: 'rgba(25, 113, 194, 0.05)',
|
||||
// },
|
||||
// },
|
||||
// }}
|
||||
// variant="light"
|
||||
// active={isParentActive}
|
||||
// >
|
||||
// {v.children.map((child, key) => {
|
||||
// const isChildActive = segments.includes(
|
||||
// _.lowerCase(child.name)
|
||||
// );
|
||||
|
||||
// return (
|
||||
// <NavLink
|
||||
// key={key}
|
||||
// href={child.path}
|
||||
// c={isChildActive ? colors["blue-button"] : "gray"}
|
||||
// label={
|
||||
// <Text fw={isChildActive ? 600 : 400} fz="sm">
|
||||
// {child.name}
|
||||
// </Text>
|
||||
// }
|
||||
// styles={{
|
||||
// root: {
|
||||
// borderRadius: rem(8),
|
||||
// marginBottom: rem(2),
|
||||
// transition: 'background 150ms ease',
|
||||
// padding: '6px 12px',
|
||||
// '&:hover': {
|
||||
// backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)',
|
||||
// },
|
||||
// ...(isChildActive && {
|
||||
// backgroundColor: 'rgba(25, 113, 194, 0.1)',
|
||||
// }),
|
||||
// },
|
||||
// }}
|
||||
// active={isChildActive}
|
||||
// component={Link}
|
||||
// />
|
||||
// );
|
||||
// })}
|
||||
// </NavLink>
|
||||
// );
|
||||
// })}
|
||||
// </AppShell.Section>
|
||||
|
||||
// <AppShell.Section py="md">
|
||||
// <Group justify="end" pr="sm">
|
||||
// <Tooltip
|
||||
// label={desktopOpened ? "Tutup Navigasi" : "Buka Navigasi"}
|
||||
// position="top"
|
||||
// withArrow
|
||||
// >
|
||||
// <ActionIcon
|
||||
// variant="light"
|
||||
// radius="xl"
|
||||
// size="lg"
|
||||
// onClick={toggleDesktop}
|
||||
// color={colors["blue-button"]}
|
||||
// >
|
||||
// <IconChevronLeft />
|
||||
// </ActionIcon>
|
||||
// </Tooltip>
|
||||
// </Group>
|
||||
// </AppShell.Section>
|
||||
// </AppShellNavbar>
|
||||
|
||||
// <AppShellMain
|
||||
// style={{
|
||||
// background: "linear-gradient(180deg, #fdfdfd, #f6f9fc)",
|
||||
// minHeight: "100vh",
|
||||
// }}
|
||||
// >
|
||||
// {children}
|
||||
// </AppShellMain>
|
||||
// </AppShell>
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
'use client'
|
||||
|
||||
import colors from "@/con/colors";
|
||||
import { authStore } from "@/store/authStore";
|
||||
import {
|
||||
ActionIcon,
|
||||
AppShell,
|
||||
@@ -26,7 +289,8 @@ import {
|
||||
import _ from "lodash";
|
||||
import Link from "next/link";
|
||||
import { useRouter, useSelectedLayoutSegments } from "next/navigation";
|
||||
import { navBar } from "./_com/list_PageAdmin";
|
||||
import { useSnapshot } from "valtio";
|
||||
import { navigationByRole } from "./_com/navigationByRole";
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
const [opened, { toggle }] = useDisclosure();
|
||||
@@ -34,6 +298,15 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter();
|
||||
const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s));
|
||||
|
||||
//ambil user dari authStore
|
||||
const {user} = useSnapshot(authStore)
|
||||
console.log("Current User:", user); // 👈 Tambahkan ini
|
||||
|
||||
//ambil navigation berdasarkan role
|
||||
const currentNav = user?.roleId !== undefined
|
||||
? navigationByRole[user.roleId as keyof typeof navigationByRole] || []
|
||||
: [];
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
suppressHydrationWarning
|
||||
@@ -156,7 +429,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
p={{ base: 'xs', sm: 'sm' }}
|
||||
>
|
||||
<AppShell.Section p="sm">
|
||||
{navBar.map((v, k) => {
|
||||
{currentNav.map((v, k) => {
|
||||
const isParentActive = segments.includes(_.lowerCase(v.name));
|
||||
|
||||
return (
|
||||
@@ -255,3 +528,4 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,16 @@ const User = new Elysia({ prefix: "/api/user" })
|
||||
id: t.String(),
|
||||
}),
|
||||
}) // pakai PUT untuk soft delete
|
||||
.put("/updt", userUpdate);
|
||||
.put(
|
||||
"/updt",
|
||||
userUpdate,
|
||||
{
|
||||
body: t.Object({
|
||||
id: t.String(),
|
||||
isActive: t.Optional(t.Boolean()),
|
||||
roleId: t.Optional(t.String()),
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
export default User;
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Context } from "elysia";
|
||||
|
||||
type FormCreate = {
|
||||
name: string;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
export default async function roleCreate(context: Context) {
|
||||
@@ -13,7 +12,6 @@ export default async function roleCreate(context: Context) {
|
||||
const result = await prisma.role.create({
|
||||
data: {
|
||||
name: body.name,
|
||||
permissions: body.permissions,
|
||||
},
|
||||
});
|
||||
return {
|
||||
|
||||
@@ -13,7 +13,6 @@ const Role = new Elysia({
|
||||
.post("/create", roleCreate, {
|
||||
body: t.Object({
|
||||
name: t.String(),
|
||||
permissions: t.Array(t.String()),
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -27,7 +26,6 @@ const Role = new Elysia({
|
||||
.put("/:id", roleUpdate, {
|
||||
body: t.Object({
|
||||
name: t.String(),
|
||||
permissions: t.Array(t.String()),
|
||||
}),
|
||||
})
|
||||
.delete("/del/:id", roleDelete);
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Context } from "elysia";
|
||||
|
||||
type FormUpdate = {
|
||||
name: string;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
export default async function roleUpdate(context: Context) {
|
||||
@@ -15,7 +14,6 @@ export default async function roleUpdate(context: Context) {
|
||||
where: { id },
|
||||
data: {
|
||||
name: body.name,
|
||||
permissions: body.permissions,
|
||||
},
|
||||
});
|
||||
return {
|
||||
|
||||
@@ -4,7 +4,11 @@ import { Context } from "elysia";
|
||||
|
||||
export default async function userUpdate(context: Context) {
|
||||
try {
|
||||
const { id, isActive } = await context.body as { id: string, isActive: boolean };
|
||||
const { id, isActive, roleId } = await context.body as {
|
||||
id: string,
|
||||
isActive?: boolean,
|
||||
roleId?: string
|
||||
};
|
||||
|
||||
if (!id) {
|
||||
return {
|
||||
@@ -13,28 +17,53 @@ export default async function userUpdate(context: Context) {
|
||||
};
|
||||
}
|
||||
|
||||
// Optional: cek apakah roleId valid
|
||||
if (roleId) {
|
||||
const cekRole = await prisma.role.findUnique({
|
||||
where: { id: roleId }
|
||||
});
|
||||
|
||||
if (!cekRole) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Role tidak ditemukan",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id },
|
||||
data: { isActive },
|
||||
data: {
|
||||
...(isActive !== undefined && { isActive }),
|
||||
...(roleId && { roleId }),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
nomor: true,
|
||||
isActive: true,
|
||||
roleId: true,
|
||||
updatedAt: true,
|
||||
role: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `User berhasil ${isActive ? "diaktifkan" : "dinonaktifkan"}`,
|
||||
message: `User berhasil diupdate`,
|
||||
data: updatedUser,
|
||||
};
|
||||
|
||||
} catch (e: any) {
|
||||
console.error("Error update user:", e);
|
||||
return {
|
||||
success: false,
|
||||
message: "Gagal mengupdate status user",
|
||||
message: "Gagal mengupdate user",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export const apiFetchRegister = async ({
|
||||
const cleanPhone = nomor.replace(/\D/g, '');
|
||||
if (cleanPhone.length < 10) throw new Error('Nomor tidak valid');
|
||||
|
||||
const response = await fetch("/api/auth/send-otp-register", {
|
||||
const response = await fetch("/api/auth/register", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username: username.trim(), nomor: cleanPhone }),
|
||||
@@ -86,38 +86,6 @@ export const apiFetchOtpData = async ({ kodeId }: { kodeId: string }) => {
|
||||
return data;
|
||||
};
|
||||
|
||||
// export const apiFetchVerifyOtp = async ({
|
||||
// nomor,
|
||||
// otp,
|
||||
// kodeId
|
||||
// }: {
|
||||
// nomor: string;
|
||||
// otp: string;
|
||||
// kodeId: string;
|
||||
// }) => {
|
||||
// if (!nomor || !otp || !kodeId) {
|
||||
// throw new Error('Data verifikasi tidak lengkap');
|
||||
// }
|
||||
|
||||
// if (!/^\d{4,6}$/.test(otp)) {
|
||||
// throw new Error('Kode OTP harus 4-6 digit angka');
|
||||
// }
|
||||
|
||||
// const response = await fetch('/api/auth/verify-otp', {
|
||||
// method: 'POST',
|
||||
// headers: { 'Content-Type': 'application/json' },
|
||||
// body: JSON.stringify({ nomor, otp, kodeId }),
|
||||
// });
|
||||
|
||||
// const data = await response.json();
|
||||
|
||||
// if (!response.ok) {
|
||||
// throw new Error(data.message || 'Verifikasi OTP gagal');
|
||||
// }
|
||||
|
||||
// return data;
|
||||
// };
|
||||
|
||||
export const apiFetchVerifyOtp = async ({
|
||||
nomor,
|
||||
otp,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// app/api/auth/finalize-registration/route.ts
|
||||
import prisma from "@/lib/prisma";
|
||||
import { cookies } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
import { sessionCreate } from "../_lib/session_create";
|
||||
// import { sessionCreate } from "../_lib/session_create";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
@@ -15,21 +16,28 @@ export async function POST(req: Request) {
|
||||
|
||||
// Buat user
|
||||
const user = await prisma.user.create({
|
||||
data: { username, nomor, isActive: true }
|
||||
data: { username, nomor, isActive: false }
|
||||
});
|
||||
|
||||
// Nonaktifkan OTP
|
||||
await prisma.kodeOtp.update({ where: { id: kodeId }, data: { isActive: false } });
|
||||
|
||||
// Buat session
|
||||
const token = await sessionCreate({
|
||||
sessionKey: process.env.BASE_SESSION_KEY!,
|
||||
jwtSecret: process.env.BASE_TOKEN_KEY!,
|
||||
user: { id: user.id, nomor: user.nomor, username: user.username, roleId: user.roleId, isActive: true },
|
||||
// const token = await sessionCreate({
|
||||
// sessionKey: process.env.BASE_SESSION_KEY!,
|
||||
// jwtSecret: process.env.BASE_TOKEN_KEY!,
|
||||
// user: { id: user.id, nomor: user.nomor, username: user.username, roleId: user.roleId, isActive: true },
|
||||
// });
|
||||
|
||||
(await cookies()).set('desadarmasaba_user_id', user.id, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
path: '/',
|
||||
maxAge: 30 * 24 * 60 * 60, // 30 hari
|
||||
});
|
||||
|
||||
const response = NextResponse.json({ success: true, roleId: user.roleId });
|
||||
response.cookies.set(process.env.BASE_SESSION_KEY!, token, { /* options */ });
|
||||
// response.cookies.set(process.env.BASE_SESSION_KEY!, token, { /* options */ });
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Finalize Registration Error:', error);
|
||||
|
||||
@@ -1,121 +1,50 @@
|
||||
// app/api/auth/register/route.ts
|
||||
import { NextResponse } from 'next/server';
|
||||
import prisma from '@/lib/prisma';
|
||||
import { randomOTP } from '../_lib/randomOTP'; // pastikan ada
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
// Terima langsung properti, bukan { data: { ... } }
|
||||
const { username, nomor } = await req.json();
|
||||
|
||||
// Validasi input
|
||||
if (!username || !nomor) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Data tidak lengkap' },
|
||||
{ status: 400 }
|
||||
);
|
||||
return NextResponse.json({ success: false, message: 'Data tidak lengkap' }, { status: 400 });
|
||||
}
|
||||
|
||||
// // Validasi OTP: pastikan berisi digit saja
|
||||
// const cleanOtp = otp.toString().trim();
|
||||
// if (!/^\d{4,6}$/.test(cleanOtp)) {
|
||||
// return NextResponse.json(
|
||||
// { success: false, message: 'Kode OTP tidak valid' },
|
||||
// { status: 400 }
|
||||
// );
|
||||
// }
|
||||
|
||||
// const receivedOtp = parseInt(cleanOtp, 10);
|
||||
// if (isNaN(receivedOtp)) {
|
||||
// return NextResponse.json(
|
||||
// { success: false, message: 'Kode OTP tidak valid' },
|
||||
// { status: 400 }
|
||||
// );
|
||||
// }
|
||||
|
||||
// // Cari OTP record
|
||||
// const otpRecord = await prisma.kodeOtp.findUnique({
|
||||
// where: { id: kodeId },
|
||||
// });
|
||||
|
||||
// if (!otpRecord) {
|
||||
// return NextResponse.json(
|
||||
// { success: false, message: 'Kode verifikasi tidak valid' },
|
||||
// { status: 400 }
|
||||
// );
|
||||
// }
|
||||
|
||||
// if (!otpRecord.isActive) {
|
||||
// return NextResponse.json(
|
||||
// { success: false, message: 'Kode verifikasi sudah kadaluarsa' },
|
||||
// { status: 400 }
|
||||
// );
|
||||
// }
|
||||
|
||||
// if (otpRecord.otp !== receivedOtp) {
|
||||
// return NextResponse.json(
|
||||
// { success: false, message: 'Kode OTP salah' },
|
||||
// { status: 400 }
|
||||
// );
|
||||
// }
|
||||
|
||||
// if (otpRecord.nomor !== nomor) {
|
||||
// return NextResponse.json(
|
||||
// { success: false, message: 'Nomor tidak sesuai' },
|
||||
// { status: 400 }
|
||||
// );
|
||||
// }
|
||||
|
||||
// Cek duplikat nomor
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { nomor },
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Nomor sudah terdaftar' },
|
||||
{ status: 409 }
|
||||
);
|
||||
// Cek duplikat
|
||||
if (await prisma.user.findUnique({ where: { nomor } })) {
|
||||
return NextResponse.json({ success: false, message: 'Nomor sudah terdaftar' }, { status: 409 });
|
||||
}
|
||||
if (await prisma.user.findUnique({ where: { username } })) {
|
||||
return NextResponse.json({ success: false, message: 'Username sudah digunakan' }, { status: 409 });
|
||||
}
|
||||
|
||||
// Cek username unik (pastikan ada @unique di schema!)
|
||||
const existingByUsername = await prisma.user.findUnique({
|
||||
where: { username },
|
||||
});
|
||||
// ✅ Generate dan kirim OTP
|
||||
const codeOtp = randomOTP();
|
||||
const otpNumber = Number(codeOtp);
|
||||
|
||||
if (existingByUsername) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Username sudah digunakan' },
|
||||
{ status: 409 }
|
||||
);
|
||||
const waMessage = `Website Desa Darmasaba - Kode verifikasi Anda: ${codeOtp}`;
|
||||
const waUrl = `https://wa.wibudev.com/code?nom=${encodeURIComponent(nomor)}&text=${encodeURIComponent(waMessage)}`;
|
||||
const waRes = await fetch(waUrl);
|
||||
const waData = await waRes.json();
|
||||
|
||||
if (waData.status !== "success") {
|
||||
return NextResponse.json({ success: false, message: 'Gagal mengirim OTP via WhatsApp' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Buat user
|
||||
const newUser = await prisma.user.create({
|
||||
data: {
|
||||
username: username.trim(),
|
||||
nomor,
|
||||
isActive: false,
|
||||
// roleId default "1"
|
||||
},
|
||||
// ✅ Simpan OTP ke database
|
||||
const otpRecord = await prisma.kodeOtp.create({
|
||||
data: { nomor, otp: otpNumber, isActive: true }
|
||||
});
|
||||
|
||||
// // Nonaktifkan OTP
|
||||
// await prisma.kodeOtp.update({
|
||||
// where: { id: kodeId },
|
||||
// data: { isActive: false },
|
||||
// });
|
||||
|
||||
// ✅ Kembalikan kodeId (jangan buat user di sini!)
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Pendaftaran berhasil. Menunggu persetujuan admin.',
|
||||
userId: newUser.id,
|
||||
message: 'Kode verifikasi dikirim',
|
||||
kodeId: otpRecord.id,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Terjadi kesalahan saat pendaftaran' },
|
||||
{ status: 500 }
|
||||
);
|
||||
console.error('Register OTP Error:', error);
|
||||
return NextResponse.json({ success: false, message: 'Gagal mengirim OTP' }, { status: 500 });
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
|
||||
@@ -108,11 +108,18 @@ export async function POST(req: Request) {
|
||||
data: { isActive: false },
|
||||
});
|
||||
|
||||
const userData = {
|
||||
id: user.id,
|
||||
name: user.username, // atau user.nama jika ada kolom nama
|
||||
roleId: user.roleId,
|
||||
};
|
||||
|
||||
// Set cookie & respons
|
||||
const response = NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
message: "Berhasil login",
|
||||
user: userData,
|
||||
roleId: user.roleId,
|
||||
},
|
||||
{ status: 200 }
|
||||
|
||||
17
src/store/authStore.ts
Normal file
17
src/store/authStore.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { proxy } from 'valtio';
|
||||
|
||||
export type User = {
|
||||
id: string;
|
||||
name: string;
|
||||
roleId: number; // 0, 1, 2, 3
|
||||
};
|
||||
|
||||
export const authStore = proxy<{
|
||||
user: User | null;
|
||||
setUser: (user: User | null) => void;
|
||||
}>({
|
||||
user: null,
|
||||
setUser(user) {
|
||||
authStore.user = user;
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user