From 24fcc1ee76223ee437fa510b76db454d111c30ec Mon Sep 17 00:00:00 2001 From: amal Date: Tue, 14 Apr 2026 16:41:03 +0800 Subject: [PATCH] upd: user staff Deskripsi: - connected to database pada halaman user - tambah user - delete user - update user No Issues --- src/app.ts | 95 ++++++++++++- src/frontend/config/api.ts | 3 + src/frontend/routes/users.tsx | 258 +++++++++++++++++++++++++++++++++- 3 files changed, 350 insertions(+), 6 deletions(-) diff --git a/src/app.ts b/src/app.ts index 3802ce9..b1a38ef 100644 --- a/src/app.ts +++ b/src/app.ts @@ -285,7 +285,7 @@ export function createApp() { .get('/api/operators/stats', async () => { const [totalStaff, activeNow, rolesGroup] = await Promise.all([ - prisma.user.count(), + prisma.user.count({where: {active: true}}), prisma.session.count({ where: { expiresAt: { gte: new Date() } }, }), @@ -302,6 +302,99 @@ export function createApp() { } }) + .post('/api/operators', async ({ request, set }) => { + const cookie = request.headers.get('cookie') ?? '' + const token = cookie.match(/session=([^;]+)/)?.[1] + let userId: string | undefined + if (token) { + const session = await prisma.session.findUnique({ where: { token } }) + if (session && session.expiresAt > new Date()) userId = session.userId + } + + const body = (await request.json()) as { name: string; email: string; password: string; role: string } + + const existing = await prisma.user.findUnique({ where: { email: body.email } }) + if (existing) { + set.status = 400 + return { error: 'Email sudah terdaftar' } + } + + const hashedPassword = await Bun.password.hash(body.password) + const user = await prisma.user.create({ + data: { + name: body.name, + email: body.email, + password: hashedPassword, + role: body.role as any, + }, + }) + + if (userId) { + await createSystemLog(userId, 'CREATE', `Created new user: ${body.name} (${body.email})`) + } + + return { id: user.id, name: user.name, email: user.email, role: user.role } + }) + + .patch('/api/operators/:id', async ({ params: { id }, request, set }) => { + const cookie = request.headers.get('cookie') ?? '' + const token = cookie.match(/session=([^;]+)/)?.[1] + let userId: string | undefined + if (token) { + const session = await prisma.session.findUnique({ where: { token } }) + if (session && session.expiresAt > new Date()) userId = session.userId + } + + const body = (await request.json()) as { name?: string; email?: string; role?: string; active?: boolean } + + const user = await prisma.user.update({ + where: { id }, + data: { + ...(body.name !== undefined && { name: body.name }), + ...(body.email !== undefined && { email: body.email }), + ...(body.role !== undefined && { role: body.role as any }), + ...(body.active !== undefined && { active: body.active }), + }, + }) + + if (userId) { + await createSystemLog(userId, 'UPDATE', `Updated user: ${user.name} (${user.email})`) + } + + return { id: user.id, name: user.name, email: user.email, role: user.role, active: user.active } + }) + + .delete('/api/operators/:id', async ({ params: { id }, request, set }) => { + const cookie = request.headers.get('cookie') ?? '' + const token = cookie.match(/session=([^;]+)/)?.[1] + let userId: string | undefined + if (token) { + const session = await prisma.session.findUnique({ where: { token } }) + if (session && session.expiresAt > new Date()) userId = session.userId + } + + const user = await prisma.user.findUnique({ where: { id } }) + if (!user) { + set.status = 404 + return { error: 'User not found' } + } + + // Prevent deleting self + if (userId === id) { + set.status = 400 + return { error: 'Cannot delete your own account' } + } + + await prisma.session.deleteMany({ where: { userId: id } }) + await prisma.user.update({ where: { id }, data: { active: false } }) + + if (userId) { + await createSystemLog(userId, 'DELETE', `Deactivated user: ${user.name} (${user.email})`) + } + + return { ok: true } + }) + .get('/api/logs/operators', async () => { return await prisma.user.findMany({ select: { id: true, name: true, image: true }, diff --git a/src/frontend/config/api.ts b/src/frontend/config/api.ts index dde9bee..6c10800 100644 --- a/src/frontend/config/api.ts +++ b/src/frontend/config/api.ts @@ -31,6 +31,9 @@ export const API_URLS = { getOperators: (page: number, search: string) => `/api/operators?page=${page}&search=${encodeURIComponent(search)}`, getOperatorStats: () => `/api/operators/stats`, + createOperator: () => `/api/operators`, + editOperator: (id: string) => `/api/operators/${id}`, + deleteOperator: (id: string) => `/api/operators/${id}`, getBugs: (page: number, search: string, app: string, status: string) => `/api/bugs?page=${page}&search=${encodeURIComponent(search)}&app=${app}&status=${status}`, createBug: () => `/api/bugs`, diff --git a/src/frontend/routes/users.tsx b/src/frontend/routes/users.tsx index ec0a12b..52c21ef 100644 --- a/src/frontend/routes/users.tsx +++ b/src/frontend/routes/users.tsx @@ -18,9 +18,14 @@ import { List, Divider, Pagination, + Modal, + Select, + PasswordInput, } from '@mantine/core' import { createFileRoute } from '@tanstack/react-router' import { useState, useEffect } from 'react' +import { useDisclosure } from '@mantine/hooks' +import { notifications } from '@mantine/notifications' import { TbPlus, TbSearch, @@ -30,6 +35,7 @@ import { TbShieldCheck, TbAccessPoint, TbCircleCheck, + TbCircleX, TbClock, TbApps, } from 'react-icons/tb' @@ -80,14 +86,131 @@ function UsersPage() { return () => clearTimeout(timer) }, [search]) - const { data: stats } = useSWR(API_URLS.getOperatorStats(), fetcher) - const { data: response, isLoading } = useSWR( + const { data: stats, mutate: mutateStats } = useSWR(API_URLS.getOperatorStats(), fetcher) + const { data: response, isLoading, mutate: mutateOperators } = useSWR( API_URLS.getOperators(page, debouncedSearch), fetcher ) const operators = response?.data || [] + // ── Create User Modal ── + const [createOpened, { open: openCreate, close: closeCreate }] = useDisclosure(false) + const [isCreating, setIsCreating] = useState(false) + const [createForm, setCreateForm] = useState({ + name: '', + email: '', + password: '', + role: 'USER', + }) + + const handleCreateUser = async () => { + if (!createForm.name || !createForm.email || !createForm.password) { + notifications.show({ title: 'Validation Error', message: 'Please fill in all required fields.', color: 'red' }) + return + } + + setIsCreating(true) + try { + const res = await fetch(API_URLS.createOperator(), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(createForm), + }) + + if (res.ok) { + notifications.show({ title: 'Success', message: 'User has been created.', color: 'teal', icon: }) + mutateOperators() + mutateStats() + closeCreate() + setCreateForm({ name: '', email: '', password: '', role: 'USER' }) + } else { + const err = await res.json() + throw new Error(err.error || 'Failed to create user') + } + } catch (e: any) { + notifications.show({ title: 'Error', message: e.message || 'Something went wrong.', color: 'red', icon: }) + } finally { + setIsCreating(false) + } + } + + // ── Edit User Modal ── + const [editOpened, { open: openEdit, close: closeEdit }] = useDisclosure(false) + const [isEditing, setIsEditing] = useState(false) + const [editingUserId, setEditingUserId] = useState(null) + const [editForm, setEditForm] = useState({ + name: '', + email: '', + role: '', + }) + + const handleOpenEdit = (user: any) => { + setEditingUserId(user.id) + setEditForm({ name: user.name, email: user.email, role: user.role }) + openEdit() + } + + const handleEditUser = async () => { + if (!editingUserId || !editForm.name || !editForm.email) return + + setIsEditing(true) + try { + const res = await fetch(API_URLS.editOperator(editingUserId), { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(editForm), + }) + + if (res.ok) { + notifications.show({ title: 'Success', message: 'User has been updated.', color: 'teal', icon: }) + mutateOperators() + closeEdit() + } else { + throw new Error('Failed to update user') + } + } catch (e) { + notifications.show({ title: 'Error', message: 'Something went wrong.', color: 'red', icon: }) + } finally { + setIsEditing(false) + } + } + + // ── Delete User ── + const [deleteOpened, { open: openDelete, close: closeDelete }] = useDisclosure(false) + const [isDeleting, setIsDeleting] = useState(false) + const [deletingUser, setDeletingUser] = useState(null) + + const handleOpenDelete = (user: any) => { + setDeletingUser(user) + openDelete() + } + + const handleDeleteUser = async () => { + if (!deletingUser) return + + setIsDeleting(true) + try { + const res = await fetch(API_URLS.deleteOperator(deletingUser.id), { + method: 'DELETE', + }) + + if (res.ok) { + notifications.show({ title: 'Success', message: 'User has been deleted.', color: 'teal', icon: }) + mutateOperators() + mutateStats() + closeDelete() + } else { + const err = await res.json() + throw new Error(err.error || 'Failed to delete user') + } + } catch (e: any) { + notifications.show({ title: 'Error', message: e.message || 'Something went wrong.', color: 'red', icon: }) + } finally { + setIsDeleting(false) + } + } + return ( @@ -131,6 +254,7 @@ function UsersPage() { gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }} leftSection={} radius="md" + onClick={openCreate} > Add New User @@ -183,10 +307,10 @@ function UsersPage() { - + handleOpenEdit(user)}> - + handleOpenDelete(user)}> @@ -256,7 +380,131 @@ function UsersPage() { + + {/* Create User Modal */} + Add New User} + radius="xl" + overlayProps={{ backgroundOpacity: 0.55, blur: 3 }} + > + + setCreateForm({ ...createForm, name: e.target.value })} + /> + setCreateForm({ ...createForm, email: e.target.value })} + /> + setCreateForm({ ...createForm, password: e.target.value })} + /> + setEditForm({ ...editForm, role: val || 'USER' })} + /> + + + + + {/* Delete Confirmation Modal */} + Delete User} + radius="xl" + size="sm" + overlayProps={{ backgroundOpacity: 0.55, blur: 3 }} + > + + + Are you sure you want to delete {deletingUser?.name}? This action cannot be undone. + + + + + + + ) } -