Pull Request branch dev-clone to main #1

Merged
gitea merged 429 commits from dev-clone into main 2024-12-23 09:31:34 +00:00
6 changed files with 109 additions and 119 deletions
Showing only changes of commit 33c0f73257 - Show all commits

View File

@ -8,7 +8,7 @@ const sidebarMenus: SidebarMenu[] = [
link: "/dashboard",
},
{
label: "Users",
label: "Manajemen Pengguna",
icon: { tb: "TbUsers" },
allowedPermissions: ["permissions.read"],
link: "/users",

View File

@ -13,37 +13,20 @@ import requestValidator from "../../utils/requestValidator";
import authInfo from "../../middlewares/authInfo";
import checkPermission from "../../middlewares/checkPermission";
import { respondents } from "../../drizzle/schema/respondents";
import { notFound } from "../../errors/DashboardError";
import { forbidden, notFound } from "../../errors/DashboardError";
export const userFormSchema = z.object({
name: z.string().min(1).max(255),
username: z.string().min(1).max(255),
email: z.string().email().optional().or(z.literal("")),
password: z.string().min(6),
companyName: z.string().min(1).max(255),
position: z.string().min(1).max(255),
workExperience: z.string().min(1).max(255),
address: z.string().min(1),
phoneNumber: z.string().min(1).max(13),
name: z.string().min(1, "Name is required").max(255),
username: z.string().min(1, "Username is required").max(255),
email: z.string().min(1, "Email is required").email().optional().or(z.literal("")),
password: z.string().min(6, "Password is required"),
companyName: z.string().min(1, "Company name is required").max(255),
position: z.string().min(1, "Position is required").max(255),
workExperience: z.string().min(1, "Work experience is required").max(255),
address: z.string().min(1, "Address is required"),
phoneNumber: z.string().min(1, "Phone number is required").max(13),
isEnabled: z.string().default("false"),
// roles: z
// .string()
// .refine(
// (data) => {
// console.log(data);
// try {
// const parsed = JSON.parse(data);
// return Array.isArray(parsed);
// } catch {
// return false;
// }
// },
// {
// message: "Roles must be an array",
// }
// )
// .optional(),
roles: z.array(z.string()).optional(),
roles: z.array(z.string().min(1, "Role is required")),
});
export const userUpdateSchema = userFormSchema.extend({
@ -75,7 +58,7 @@ const usersRoute = new Hono<HonoEnv>()
.optional()
.transform((v) => v?.toLowerCase() === "true"),
page: z.coerce.number().int().min(0).default(0),
limit: z.coerce.number().int().min(1).max(1000).default(100),
limit: z.coerce.number().int().min(1).max(1000).default(1),
q: z.string().default(""),
})
),
@ -225,9 +208,9 @@ const usersRoute = new Hono<HonoEnv>()
);
if (!queryResult.length)
throw new HTTPException(404, {
message: "The user does not exists",
});
throw notFound({
message : "The user does not exists",
})
const roles = queryResult.reduce((prev, curr) => {
if (!curr.role) return prev;
@ -244,6 +227,7 @@ const usersRoute = new Hono<HonoEnv>()
return c.json(userData);
}
)
//create user
.post(
"/",
@ -275,15 +259,15 @@ const usersRoute = new Hono<HonoEnv>()
.where(eq(respondents.phoneNumber, userData.phoneNumber));
if (existingUser.length > 0) {
throw new HTTPException(400, {
throw notFound({
message: "Email or username has been registered",
});
})
}
if (existingRespondent.length > 0) {
throw new HTTPException(400, {
throw forbidden({
message: "Phone number has been registered",
});
})
}
// Hash the password
@ -303,7 +287,9 @@ const usersRoute = new Hono<HonoEnv>()
})
.returning()
.catch(() => {
throw new HTTPException(500, { message: "Error creating user" });
throw forbidden({
message: "Error creating user",
})
});
// Create respondent
@ -325,7 +311,7 @@ const usersRoute = new Hono<HonoEnv>()
});
// Add other roles if provided
if (userData.roles) {
if (userData.roles && userData.roles.length > 0) {
const roles = userData.roles;
for (let roleId of roles) {
@ -348,6 +334,10 @@ const usersRoute = new Hono<HonoEnv>()
});
}
}
} else {
throw forbidden({
message: "Harap pilih minimal satu role",
});
}
return newUser;
@ -387,9 +377,9 @@ const usersRoute = new Hono<HonoEnv>()
);
if (existingUser.length > 0) {
throw new HTTPException(400, {
throw forbidden({
message: "Email or username has been registered by another user",
});
})
}
}
@ -431,7 +421,7 @@ const usersRoute = new Hono<HonoEnv>()
}
// Update roles if provided
if (userData.roles) {
if (userData.roles && userData.roles.length > 0) {
const roles = userData.roles;
// Remove existing roles for the user
@ -458,7 +448,11 @@ const usersRoute = new Hono<HonoEnv>()
});
}
}
}
} else {
throw forbidden({
message: "Harap pilih minimal satu role",
});
}
});
return c.json({
@ -497,13 +491,13 @@ const usersRoute = new Hono<HonoEnv>()
// Throw error if the user does not exist
if (!user[0])
throw new HTTPException(404, {
throw notFound ({
message: "The user is not found",
});
// Throw error if the user is trying to delete themselves
if (user[0].id === currentUserId) {
throw new HTTPException(400, {
throw forbidden ({
message: "You cannot delete yourself",
});
}
@ -541,7 +535,7 @@ const usersRoute = new Hono<HonoEnv>()
// Throw error if the user is not deleted
if (!user.deletedAt) {
throw new HTTPException(400, {
throw forbidden({
message: "The user is not deleted",
});
}

View File

@ -63,14 +63,14 @@ export default function UserDeleteModal() {
<Modal
opened={isModalOpen}
onClose={() => navigate({ search: {} })}
title={`Delete confirmation`}
title={`Konfirmasi Hapus`}
>
<Text size="sm">
Are you sure you want to delete user{" "}
Apakah Anda yakin ingin menghapus pengguna{" "}
<Text span fw={700}>
{userQuery.data?.name}
</Text>
? This action is irreversible.
? Tindakan ini tidak dapat diubah.
</Text>
{/* {errorMessage && <Alert color="red">{errorMessage}</Alert>} */}
@ -81,7 +81,7 @@ export default function UserDeleteModal() {
onClick={() => navigate({ search: {} })}
disabled={mutation.isPending}
>
Cancel
Batal
</Button>
<Button
variant="subtle"
@ -91,7 +91,7 @@ export default function UserDeleteModal() {
loading={mutation.isPending}
onClick={() => mutation.mutate({ id: userId })}
>
Delete User
Hapus Pengguna
</Button>
</Flex>
</Modal>

View File

@ -42,7 +42,7 @@ export default function UserFormModal() {
const detailId = searchParams.detail;
const editId = searchParams.edit;
const formType = detailId ? "detail" : editId ? "edit" : "create";
const formType = detailId ? "detail" : editId ? "ubah" : "tambah";
/**
* CHANGE FOLLOWING:
@ -51,7 +51,7 @@ export default function UserFormModal() {
const userQuery = useQuery(getUserByIdQueryOptions(dataId));
const modalTitle =
formType.charAt(0).toUpperCase() + formType.slice(1) + " User";
formType.charAt(0).toUpperCase() + formType.slice(1) + " Pengguna";
const form = useForm({
initialValues: {
@ -101,11 +101,11 @@ export default function UserFormModal() {
mutationKey: ["usersMutation"],
mutationFn: async (
options:
| { action: "edit"; data: Parameters<typeof updateUser>[0] }
| { action: "create"; data: Parameters<typeof createUser>[0] }
| { action: "ubah"; data: Parameters<typeof updateUser>[0] }
| { action: "tambah"; data: Parameters<typeof createUser>[0] }
) => {
console.log("called");
return options.action === "edit"
return options.action === "ubah"
? await updateUser(options.data)
: await createUser(options.data);
},
@ -130,7 +130,7 @@ export default function UserFormModal() {
if (formType === "detail") return;
//TODO: OPtimize this code
if (formType === "create") {
if (formType === "tambah") {
await mutation.mutateAsync({
action: formType,
data: {
@ -168,7 +168,7 @@ export default function UserFormModal() {
}
queryClient.invalidateQueries({ queryKey: ["users"] });
notifications.show({
message: `The ser is ${formType === "create" ? "created" : "edited"}`,
message: `The ser is ${formType === "tambah" ? "created" : "edited"}`,
});
navigate({ search: {} });
@ -212,59 +212,13 @@ export default function UserFormModal() {
</Center>
</Stack>
{/* {createInputComponents({
disableAll: mutation.isPending,
readonlyAll: formType === "detail",
inputs: [
{
type: "text",
readOnly: true,
variant: "filled",
...form.getInputProps("id"),
hidden: !form.values.id,
},
{
type: "text",
label: "Name",
...form.getInputProps("name"),
},
{
type: "text",
label: "Username",
...form.getInputProps("username"),
},
{
type: "text",
label: "Email",
...form.getInputProps("email"),
},
{
type: "password",
label: "Password",
hidden: formType !== "create",
...form.getInputProps("password"),
},
{
type: "multi-select",
label: "Roles",
value: form.values.roles,
onChange: (values) =>
form.setFieldValue("roles", values),
data: rolesQuery.data?.map((role) => ({
value: role.id,
label: role.name,
})),
error: form.errors.roles,
},
],
})} */}
{createInputComponents({
disableAll: mutation.isPending,
readonlyAll: formType === "detail",
inputs: [
{
type: "text",
label: "Id Pengguna",
readOnly: true,
variant: "filled",
...form.getInputProps("id"),
@ -272,17 +226,17 @@ export default function UserFormModal() {
},
{
type: "text",
label: "Name",
label: "Nama",
...form.getInputProps("name"),
},
{
type: "text",
label: "Position",
label: "Jabatan",
...form.getInputProps("position"),
},
{
type: "text",
label: "Work Experience",
label: "Pengalaman Kerja",
...form.getInputProps("workExperience"),
},
{
@ -292,17 +246,17 @@ export default function UserFormModal() {
},
{
type: "text",
label: "Company Name",
label: "Instansi/Perusahaan",
...form.getInputProps("companyName"),
},
{
type: "text",
label: "Address",
label: "Alamat",
...form.getInputProps("address"),
},
{
type: "text",
label: "Phone Number",
label: "Nomor Telepon",
...form.getInputProps("phoneNumber"),
},
@ -326,7 +280,7 @@ export default function UserFormModal() {
{
type: "password",
label: "Password",
hidden: formType !== "create",
hidden: formType !== "tambah",
...form.getInputProps("password"),
},
],

View File

@ -23,6 +23,10 @@ import { Route as DashboardLayoutDashboardIndexImport } from './routes/_dashboar
const IndexLazyImport = createFileRoute('/')()
const LogoutIndexLazyImport = createFileRoute('/logout/')()
const LoginIndexLazyImport = createFileRoute('/login/')()
const ForgotPasswordIndexLazyImport = createFileRoute('/forgot-password/')()
const ForgotPasswordVerifyLazyImport = createFileRoute(
'/forgot-password/verify',
)()
// Create/Update Routes
@ -46,6 +50,20 @@ const LoginIndexLazyRoute = LoginIndexLazyImport.update({
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/login/index.lazy').then((d) => d.Route))
const ForgotPasswordIndexLazyRoute = ForgotPasswordIndexLazyImport.update({
path: '/forgot-password/',
getParentRoute: () => rootRoute,
} as any).lazy(() =>
import('./routes/forgot-password/index.lazy').then((d) => d.Route),
)
const ForgotPasswordVerifyLazyRoute = ForgotPasswordVerifyLazyImport.update({
path: '/forgot-password/verify',
getParentRoute: () => rootRoute,
} as any).lazy(() =>
import('./routes/forgot-password/verify.lazy').then((d) => d.Route),
)
const DashboardLayoutUsersIndexRoute = DashboardLayoutUsersIndexImport.update({
path: '/users/',
getParentRoute: () => DashboardLayoutRoute,
@ -83,6 +101,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DashboardLayoutImport
parentRoute: typeof rootRoute
}
'/forgot-password/verify': {
id: '/forgot-password/verify'
path: '/forgot-password/verify'
fullPath: '/forgot-password/verify'
preLoaderRoute: typeof ForgotPasswordVerifyLazyImport
parentRoute: typeof rootRoute
}
'/forgot-password/': {
id: '/forgot-password/'
path: '/forgot-password'
fullPath: '/forgot-password'
preLoaderRoute: typeof ForgotPasswordIndexLazyImport
parentRoute: typeof rootRoute
}
'/login/': {
id: '/login/'
path: '/login'
@ -130,6 +162,8 @@ export const routeTree = rootRoute.addChildren({
DashboardLayoutTimetableIndexRoute,
DashboardLayoutUsersIndexRoute,
}),
ForgotPasswordVerifyLazyRoute,
ForgotPasswordIndexLazyRoute,
LoginIndexLazyRoute,
LogoutIndexLazyRoute,
})
@ -144,6 +178,8 @@ export const routeTree = rootRoute.addChildren({
"children": [
"/",
"/_dashboardLayout",
"/forgot-password/verify",
"/forgot-password/",
"/login/",
"/logout/"
]
@ -159,6 +195,12 @@ export const routeTree = rootRoute.addChildren({
"/_dashboardLayout/users/"
]
},
"/forgot-password/verify": {
"filePath": "forgot-password/verify.lazy.tsx"
},
"/forgot-password/": {
"filePath": "forgot-password/index.lazy.tsx"
},
"/login/": {
"filePath": "login/index.lazy.tsx"
},

View File

@ -20,7 +20,7 @@ const columnHelper = createColumnHelper<DataType>();
export default function UsersPage() {
return (
<PageTemplate
title="Users"
title="Manajemen Pengguna"
queryOptions={userQueryOptions}
modals={[<UserFormModal />, <UserDeleteModal />]}
columnDefs={[
@ -30,7 +30,7 @@ export default function UsersPage() {
}),
columnHelper.display({
header: "Name",
header: "Nama",
cell: (props) => props.row.original.name,
}),
@ -43,7 +43,7 @@ export default function UsersPage() {
cell: (props) => props.row.original.email,
}),
columnHelper.display({
header: "Company",
header: "Perusahaan",
cell: (props) => props.row.original.company,
}),
@ -54,12 +54,12 @@ export default function UsersPage() {
if (roles && roles.length > 0) {
return roles.map(role => role.name).join(", ");
}
return <div>No roles assigned</div>;
return <div>Tidak ada peran yang diberikan</div>;
},
}),
columnHelper.display({
header: "Actions",
header: "Aksi",
cell: (props) => (
<Flex gap="xs">
{createActionButtons([
@ -71,14 +71,14 @@ export default function UsersPage() {
icon: <TbEye />,
},
{
label: "Edit",
label: "Ubah",
permission: true,
action: `?edit=${props.row.original.id}`,
color: "orange",
icon: <TbPencil />,
},
{
label: "Delete",
label: "Hapus",
permission: true,
action: `?delete=${props.row.original.id}`,
color: "red",