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
9 changed files with 546 additions and 197 deletions
Showing only changes of commit 670e24935e - Show all commits

View File

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

View File

@ -15,7 +15,7 @@ export const respondents = pgTable("respondents", {
phoneNumber: varchar("phoneNumber", { length: 13 }).notNull(), phoneNumber: varchar("phoneNumber", { length: 13 }).notNull(),
createdAt: timestamp("createdAt", { mode: "date" }).defaultNow(), createdAt: timestamp("createdAt", { mode: "date" }).defaultNow(),
updatedAt: timestamp("updatedAt", { mode: "date" }).defaultNow(), updatedAt: timestamp("updatedAt", { mode: "date" }).defaultNow(),
deletedAt: timestamp("deletetAt", { mode: "date" }), deletedAt: timestamp("deletedAt", { mode: "date" }),
}); });
export const respondentsRelations = relations(respondents, ({ one }) => ({ export const respondentsRelations = relations(respondents, ({ one }) => ({

View File

@ -1,4 +1,4 @@
import { and, eq, ilike, isNull, or, sql } from "drizzle-orm"; import { and, eq, ilike, isNull, or, sql, not, inArray } from "drizzle-orm";
import { Hono } from "hono"; import { Hono } from "hono";
import { z } from "zod"; import { z } from "zod";
@ -12,30 +12,21 @@ import HonoEnv from "../../types/HonoEnv";
import requestValidator from "../../utils/requestValidator"; import requestValidator from "../../utils/requestValidator";
import authInfo from "../../middlewares/authInfo"; import authInfo from "../../middlewares/authInfo";
import checkPermission from "../../middlewares/checkPermission"; import checkPermission from "../../middlewares/checkPermission";
import { respondents } from "../../drizzle/schema/respondents";
import { forbidden, notFound } from "../../errors/DashboardError";
export const userFormSchema = z.object({ export const userFormSchema = z.object({
name: z.string().min(1).max(255), name: z.string().min(1, "Name is required").max(255),
username: z.string().min(1).max(255), username: z.string().min(1, "Username is required").max(255),
email: z.string().email().optional().or(z.literal("")), email: z.string().min(1, "Email is required").email().optional().or(z.literal("")),
password: z.string().min(6), 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"), isEnabled: z.string().default("false"),
roles: z roles: z.array(z.string().min(1, "Role is required")),
.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(),
}); });
export const userUpdateSchema = userFormSchema.extend({ export const userUpdateSchema = userFormSchema.extend({
@ -51,6 +42,8 @@ const usersRoute = new Hono<HonoEnv>()
* - includeTrashed: boolean (default: false)\ * - includeTrashed: boolean (default: false)\
* - withMetadata: boolean * - withMetadata: boolean
*/ */
// Get all users with search
.get( .get(
"/", "/",
checkPermission("users.readAll"), checkPermission("users.readAll"),
@ -66,17 +59,69 @@ const usersRoute = new Hono<HonoEnv>()
.optional() .optional()
.transform((v) => v?.toLowerCase() === "true"), .transform((v) => v?.toLowerCase() === "true"),
page: z.coerce.number().int().min(0).default(0), page: z.coerce.number().int().min(0).default(0),
limit: z.coerce.number().int().min(1).max(1000).default(1), limit: z.coerce.number().int().min(1).max(1000).default(10),
q: z.string().default(""), q: z.string().default(""), // Keyword search
}) })
), ),
async (c) => { async (c) => {
const { includeTrashed, page, limit, q } = c.req.valid("query"); const { includeTrashed, page, limit, q } = c.req.valid("query");
const totalCountQuery = includeTrashed // Query to count total data without duplicates
? sql<number>`(SELECT count(*) FROM ${users})` const totalCountQuery = db
: sql<number>`(SELECT count(*) FROM ${users} WHERE ${users.deletedAt} IS NULL)`; .select({
count: sql<number>`count(distinct ${users.id})`,
})
.from(users)
.leftJoin(respondents, eq(users.id, respondents.userId))
.leftJoin(rolesToUsers, eq(users.id, rolesToUsers.userId))
.leftJoin(rolesSchema, eq(rolesToUsers.roleId, rolesSchema.id))
.where(
and(
includeTrashed ? undefined : isNull(users.deletedAt),
q
? or(
ilike(users.name, `%${q}%`), // Search by name
ilike(users.username, `%${q}%`), // Search by username
ilike(users.email, `%${q}%`), // Search by email
ilike(respondents.companyName, `%${q}%`), // Search by companyName (from respondents)
ilike(rolesSchema.name, `%${q}%`) // Search by role name (from rolesSchema)
)
: undefined
)
);
// Get the total count result from the query
const totalCountResult = await totalCountQuery;
const totalCount = totalCountResult[0]?.count || 0;
// Query to get unique user IDs based on pagination (Sub Query)
const userIdsQuery = db
.select({
id: users.id,
})
.from(users)
.leftJoin(respondents, eq(users.id, respondents.userId))
.leftJoin(rolesToUsers, eq(users.id, rolesToUsers.userId))
.leftJoin(rolesSchema, eq(rolesToUsers.roleId, rolesSchema.id))
.where(
and(
includeTrashed ? undefined : isNull(users.deletedAt),
q
? or(
ilike(users.name, `%${q}%`), // Search by name
ilike(users.username, `%${q}%`), // Search by username
ilike(users.email, `%${q}%`),
ilike(respondents.companyName, `%${q}%`),
ilike(rolesSchema.name, `%${q}%`)
)
: undefined
)
)
.groupBy(users.id) // Group by user ID to avoid the effect of duplicate data
.offset(page * limit)
.limit(limit);
// Main Query
const result = await db const result = await db
.select({ .select({
id: users.id, id: users.id,
@ -87,38 +132,78 @@ const usersRoute = new Hono<HonoEnv>()
createdAt: users.createdAt, createdAt: users.createdAt,
updatedAt: users.updatedAt, updatedAt: users.updatedAt,
...(includeTrashed ? { deletedAt: users.deletedAt } : {}), ...(includeTrashed ? { deletedAt: users.deletedAt } : {}),
fullCount: totalCountQuery, company: respondents.companyName,
role: {
name: rolesSchema.name,
id: rolesSchema.id,
},
}) })
.from(users) .from(users)
.where( .leftJoin(respondents, eq(users.id, respondents.userId))
and( .leftJoin(rolesToUsers, eq(users.id, rolesToUsers.userId))
includeTrashed ? undefined : isNull(users.deletedAt), .leftJoin(rolesSchema, eq(rolesToUsers.roleId, rolesSchema.id))
q .where(inArray(users.id, userIdsQuery)) // Only take data based on IDs from subquery
? or( .orderBy(users.createdAt);
ilike(users.name, q),
ilike(users.username, q), // Group roles for each user to avoid duplication
ilike(users.email, q), const userMap = new Map<
eq(users.id, q) string,
) {
: undefined id: string;
) name: string;
) email: string | null;
.offset(page * limit) username: string;
.limit(limit); isEnabled: boolean;
createdAt: Date;
updatedAt: Date;
deletedAt?: Date;
company: string | null;
roles: { id: string; name: string }[];
}
>();
result.forEach((item) => {
if (!userMap.has(item.id)) {
userMap.set(item.id, {
id: item.id,
name: item.name,
email: item.email ?? null,
username: item.username,
isEnabled: item.isEnabled ?? false,
createdAt: item.createdAt ?? new Date(),
updatedAt: item.updatedAt ?? new Date(),
deletedAt: item.deletedAt ?? undefined,
company: item.company,
roles: item.role
? [{ id: item.role.id, name: item.role.name }]
: [],
});
} else {
const existingUser = userMap.get(item.id);
if (item.role) {
existingUser?.roles.push({
id: item.role.id,
name: item.role.name,
});
}
}
});
// Return user data without duplicates, with roles array
const groupedData = Array.from(userMap.values());
return c.json({ return c.json({
data: result.map((d) => ({ ...d, fullCount: undefined })), data: groupedData,
_metadata: { _metadata: {
currentPage: page, currentPage: page,
totalPages: Math.ceil( totalPages: Math.ceil(totalCount / limit),
(Number(result[0]?.fullCount) ?? 0) / limit totalItems: totalCount,
),
totalItems: Number(result[0]?.fullCount) ?? 0,
perPage: limit, perPage: limit,
}, },
}); });
} }
) )
//get user by id //get user by id
.get( .get(
"/:id", "/:id",
@ -139,7 +224,12 @@ const usersRoute = new Hono<HonoEnv>()
.select({ .select({
id: users.id, id: users.id,
name: users.name, name: users.name,
position: respondents.position,
workExperience: respondents.workExperience,
email: users.email, email: users.email,
companyName: respondents.companyName,
address: respondents.address,
phoneNumber: respondents.phoneNumber,
username: users.username, username: users.username,
isEnabled: users.isEnabled, isEnabled: users.isEnabled,
createdAt: users.createdAt, createdAt: users.createdAt,
@ -151,6 +241,7 @@ const usersRoute = new Hono<HonoEnv>()
}, },
}) })
.from(users) .from(users)
.leftJoin(respondents, eq(users.id, respondents.userId))
.leftJoin(rolesToUsers, eq(users.id, rolesToUsers.userId)) .leftJoin(rolesToUsers, eq(users.id, rolesToUsers.userId))
.leftJoin(rolesSchema, eq(rolesToUsers.roleId, rolesSchema.id)) .leftJoin(rolesSchema, eq(rolesToUsers.roleId, rolesSchema.id))
.where( .where(
@ -161,9 +252,9 @@ const usersRoute = new Hono<HonoEnv>()
); );
if (!queryResult.length) if (!queryResult.length)
throw new HTTPException(404, { throw notFound({
message : "The user does not exists", message : "The user does not exists",
}); })
const roles = queryResult.reduce((prev, curr) => { const roles = queryResult.reduce((prev, curr) => {
if (!curr.role) return prev; if (!curr.role) return prev;
@ -180,38 +271,121 @@ const usersRoute = new Hono<HonoEnv>()
return c.json(userData); return c.json(userData);
} }
) )
//create user //create user
.post( .post(
"/", "/",
checkPermission("users.create"), checkPermission("users.create"),
requestValidator("form", userFormSchema), requestValidator("json", userFormSchema),
async (c) => { async (c) => {
const userData = c.req.valid("form"); const userData = c.req.valid("json");
const user = await db // Check if the provided email or username is already exists in database
const conditions = [];
if (userData.email) {
conditions.push(eq(users.email, userData.email));
}
conditions.push(eq(users.username, userData.username));
const existingUser = await db
.select()
.from(users)
.where(
or(
eq(users.email, userData.email),
eq(users.username, userData.username)
)
);
const existingRespondent = await db
.select()
.from(respondents)
.where(eq(respondents.phoneNumber, userData.phoneNumber));
if (existingUser.length > 0) {
throw forbidden({
message: "Email or username has been registered",
})
}
if (existingRespondent.length > 0) {
throw forbidden({
message: "Phone number has been registered",
})
}
// Hash the password
const hashedPassword = await hashPassword(userData.password);
// Start a transaction
const result = await db.transaction(async (trx) => {
// Create user
const [newUser] = await trx
.insert(users) .insert(users)
.values({ .values({
name: userData.name, name: userData.name,
username: userData.username, username: userData.username,
email: userData.email, email: userData.email,
password: await hashPassword(userData.password), password: hashedPassword,
isEnabled: userData.isEnabled.toLowerCase() === "true", isEnabled: userData.isEnabled?.toLowerCase() === "true" || true,
}) })
.returning(); .returning()
.catch(() => {
throw forbidden({
message: "Error creating user",
})
});
if (userData.roles) { // Create respondent
const roles = JSON.parse(userData.roles) as string[]; const [newRespondent] = await trx
console.log(roles); .insert(respondents)
.values({
companyName: userData.companyName,
position: userData.position,
workExperience: userData.workExperience,
address: userData.address,
phoneNumber: userData.phoneNumber,
userId: newUser.id,
})
.returning()
.catch((err) => {
throw new HTTPException(500, {
message: "Error creating respondent: " + err.message,
});
});
if (roles.length) { // Add other roles if provided
await db.insert(rolesToUsers).values( if (userData.roles && userData.roles.length > 0) {
roles.map((role) => ({ const roles = userData.roles;
userId: user[0].id,
roleId: role, for (let roleId of roles) {
})) const role = (
); await trx
.select()
.from(rolesSchema)
.where(eq(rolesSchema.id, roleId))
.limit(1)
)[0];
if (role) {
await trx.insert(rolesToUsers).values({
userId: newUser.id,
roleId: role.id,
});
} else {
throw new HTTPException(404, {
message: `Role ${roleId} does not exists`,
});
} }
} }
} else {
throw forbidden({
message: "Harap pilih minimal satu role",
});
}
return newUser;
});
return c.json( return c.json(
{ {
@ -226,10 +400,32 @@ const usersRoute = new Hono<HonoEnv>()
.patch( .patch(
"/:id", "/:id",
checkPermission("users.update"), checkPermission("users.update"),
requestValidator("form", userUpdateSchema), requestValidator("json", userUpdateSchema),
async (c) => { async (c) => {
const userId = c.req.param("id"); const userId = c.req.param("id");
const userData = c.req.valid("form"); const userData = c.req.valid("json");
// Check if the provided email or username is already exists in the database (excluding the current user)
if (userData.email || userData.username) {
const existingUser = await db
.select()
.from(users)
.where(
and(
or(
eq(users.email, userData.email),
eq(users.username, userData.username)
),
not(eq(users.id, userId))
)
);
if (existingUser.length > 0) {
throw forbidden({
message: "Email or username has been registered by another user",
})
}
}
const user = await db const user = await db
.select() .select()
@ -238,7 +434,10 @@ const usersRoute = new Hono<HonoEnv>()
if (!user[0]) return c.notFound(); if (!user[0]) return c.notFound();
await db // Start transaction to update both user and respondent
await db.transaction(async (trx) => {
// Update user
await trx
.update(users) .update(users)
.set({ .set({
...userData, ...userData,
@ -250,6 +449,56 @@ const usersRoute = new Hono<HonoEnv>()
}) })
.where(eq(users.id, userId)); .where(eq(users.id, userId));
// Update respondent data if provided
if (userData.companyName || userData.position || userData.workExperience || userData.address || userData.phoneNumber) {
await trx
.update(respondents)
.set({
...(userData.companyName ? {companyName: userData.companyName} : {}),
...(userData.position ? {position: userData.position} : {}),
...(userData.workExperience ? {workExperience: userData.workExperience} : {}),
...(userData.address ? {address: userData.address} : {}),
...(userData.phoneNumber ? {phoneNumber: userData.phoneNumber} : {}),
updatedAt: new Date(),
})
.where(eq(respondents.userId, userId));
}
// Update roles if provided
if (userData.roles && userData.roles.length > 0) {
const roles = userData.roles;
// Remove existing roles for the user
await trx.delete(rolesToUsers).where(eq(rolesToUsers.userId, userId));
// Assign new roles
for (let roleId of roles) {
const role = (
await trx
.select()
.from(rolesSchema)
.where(eq(rolesSchema.id, roleId))
.limit(1)
)[0];
if (role) {
await trx.insert(rolesToUsers).values({
userId: userId,
roleId: role.id,
});
} else {
throw new HTTPException(404, {
message: `Role ${roleId} does not exist`,
});
}
}
} else {
throw forbidden({
message: "Harap pilih minimal satu role",
});
}
});
return c.json({ return c.json({
message: "User updated successfully", message: "User updated successfully",
}); });
@ -273,6 +522,7 @@ const usersRoute = new Hono<HonoEnv>()
const skipTrash = const skipTrash =
c.req.valid("form").skipTrash.toLowerCase() === "true"; c.req.valid("form").skipTrash.toLowerCase() === "true";
// Check if the user exists
const user = await db const user = await db
.select() .select()
.from(users) .from(users)
@ -283,17 +533,20 @@ const usersRoute = new Hono<HonoEnv>()
) )
); );
// Throw error if the user does not exist
if (!user[0]) if (!user[0])
throw new HTTPException(404, { throw notFound ({
message: "The user is not found", message: "The user is not found",
}); });
// Throw error if the user is trying to delete themselves
if (user[0].id === currentUserId) { if (user[0].id === currentUserId) {
throw new HTTPException(400, { throw forbidden ({
message: "You cannot delete yourself", message: "You cannot delete yourself",
}); });
} }
// Delete or soft delete user
if (skipTrash) { if (skipTrash) {
await db.delete(users).where(eq(users.id, userId)); await db.delete(users).where(eq(users.id, userId));
} else { } else {
@ -311,21 +564,27 @@ const usersRoute = new Hono<HonoEnv>()
) )
//undo delete //undo delete
.patch("/restore/:id", checkPermission("users.restore"), async (c) => { .patch(
"/restore/:id",
checkPermission("users.restore"),
async (c) => {
const userId = c.req.param("id"); const userId = c.req.param("id");
// Check if the user exists
const user = ( const user = (
await db.select().from(users).where(eq(users.id, userId)) await db.select().from(users).where(eq(users.id, userId))
)[0]; )[0];
if (!user) return c.notFound(); if (!user) return c.notFound();
// Throw error if the user is not deleted
if (!user.deletedAt) { if (!user.deletedAt) {
throw new HTTPException(400, { throw forbidden({
message: "The user is not deleted", message: "The user is not deleted",
}); });
} }
// Restore user
await db await db
.update(users) .update(users)
.set({ deletedAt: null }) .set({ deletedAt: null })

View File

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

View File

@ -42,7 +42,7 @@ export default function UserFormModal() {
const detailId = searchParams.detail; const detailId = searchParams.detail;
const editId = searchParams.edit; const editId = searchParams.edit;
const formType = detailId ? "detail" : editId ? "edit" : "create"; const formType = detailId ? "detail" : editId ? "ubah" : "tambah";
/** /**
* CHANGE FOLLOWING: * CHANGE FOLLOWING:
@ -51,7 +51,7 @@ export default function UserFormModal() {
const userQuery = useQuery(getUserByIdQueryOptions(dataId)); const userQuery = useQuery(getUserByIdQueryOptions(dataId));
const modalTitle = const modalTitle =
formType.charAt(0).toUpperCase() + formType.slice(1) + " User"; formType.charAt(0).toUpperCase() + formType.slice(1) + " Pengguna";
const form = useForm({ const form = useForm({
initialValues: { initialValues: {
@ -62,6 +62,11 @@ export default function UserFormModal() {
photoProfileUrl: "", photoProfileUrl: "",
password: "", password: "",
roles: [] as string[], roles: [] as string[],
companyName: "",
position: "",
workExperience: "",
address: "",
phoneNumber: "",
}, },
}); });
@ -81,6 +86,11 @@ export default function UserFormModal() {
username: data.username, username: data.username,
password: "", password: "",
roles: data.roles.map((v) => v.id), //only extract the id roles: data.roles.map((v) => v.id), //only extract the id
companyName: data.companyName ?? "",
position: data.position ?? "",
workExperience: data.workExperience ?? "",
address: data.address ?? "",
phoneNumber: data.phoneNumber ?? "",
}); });
form.setErrors({}); form.setErrors({});
@ -91,11 +101,11 @@ export default function UserFormModal() {
mutationKey: ["usersMutation"], mutationKey: ["usersMutation"],
mutationFn: async ( mutationFn: async (
options: options:
| { action: "edit"; data: Parameters<typeof updateUser>[0] } | { action: "ubah"; data: Parameters<typeof updateUser>[0] }
| { action: "create"; data: Parameters<typeof createUser>[0] } | { action: "tambah"; data: Parameters<typeof createUser>[0] }
) => { ) => {
console.log("called"); console.log("called");
return options.action === "edit" return options.action === "ubah"
? await updateUser(options.data) ? await updateUser(options.data)
: await createUser(options.data); : await createUser(options.data);
}, },
@ -120,16 +130,21 @@ export default function UserFormModal() {
if (formType === "detail") return; if (formType === "detail") return;
//TODO: OPtimize this code //TODO: OPtimize this code
if (formType === "create") { if (formType === "tambah") {
await mutation.mutateAsync({ await mutation.mutateAsync({
action: formType, action: formType,
data: { data: {
email: values.email, email: values.email,
name: values.name, name: values.name,
password: values.password, password: values.password,
roles: JSON.stringify(values.roles), roles: values.roles,
isEnabled: "true", isEnabled: "true",
username: values.username, username: values.username,
companyName: values.email,
position: values.position,
workExperience: values.workExperience,
address: values.address,
phoneNumber: values.phoneNumber,
}, },
}); });
} else { } else {
@ -140,15 +155,20 @@ export default function UserFormModal() {
email: values.email, email: values.email,
name: values.name, name: values.name,
password: values.password, password: values.password,
roles: JSON.stringify(values.roles), roles: values.roles,
isEnabled: "true", isEnabled: "true",
username: values.username, username: values.username,
companyName: values.companyName,
position: values.position,
workExperience: values.workExperience,
address: values.address,
phoneNumber: values.phoneNumber,
}, },
}); });
} }
queryClient.invalidateQueries({ queryKey: ["users"] }); queryClient.invalidateQueries({ queryKey: ["users"] });
notifications.show({ notifications.show({
message: `The ser is ${formType === "create" ? "created" : "edited"}`, message: `The ser is ${formType === "tambah" ? "created" : "edited"}`,
}); });
navigate({ search: {} }); navigate({ search: {} });
@ -198,20 +218,18 @@ export default function UserFormModal() {
inputs: [ inputs: [
{ {
type: "text", type: "text",
readOnly: true, label: "Nama",
variant: "filled",
...form.getInputProps("id"),
hidden: !form.values.id,
},
{
type: "text",
label: "Name",
...form.getInputProps("name"), ...form.getInputProps("name"),
}, },
{ {
type: "text", type: "text",
label: "Username", label: "Jabatan",
...form.getInputProps("username"), ...form.getInputProps("position"),
},
{
type: "text",
label: "Pengalaman Kerja",
...form.getInputProps("workExperience"),
}, },
{ {
type: "text", type: "text",
@ -219,11 +237,21 @@ export default function UserFormModal() {
...form.getInputProps("email"), ...form.getInputProps("email"),
}, },
{ {
type: "password", type: "text",
label: "Password", label: "Instansi/Perusahaan",
hidden: formType !== "create", ...form.getInputProps("companyName"),
...form.getInputProps("password"),
}, },
{
type: "text",
label: "Alamat",
...form.getInputProps("address"),
},
{
type: "text",
label: "Nomor Telepon",
...form.getInputProps("phoneNumber"),
},
{ {
type: "multi-select", type: "multi-select",
label: "Roles", label: "Roles",
@ -236,6 +264,17 @@ export default function UserFormModal() {
})), })),
error: form.errors.roles, error: form.errors.roles,
}, },
{
type: "text",
label: "Username",
...form.getInputProps("username"),
},
{
type: "password",
label: "Password",
hidden: formType !== "tambah",
...form.getInputProps("password"),
},
], ],
})} })}
@ -246,7 +285,7 @@ export default function UserFormModal() {
onClick={() => navigate({ search: {} })} onClick={() => navigate({ search: {} })}
disabled={mutation.isPending} disabled={mutation.isPending}
> >
Close Tutup
</Button> </Button>
{formType !== "detail" && ( {formType !== "detail" && (
<Button <Button
@ -255,7 +294,7 @@ export default function UserFormModal() {
type="submit" type="submit"
loading={mutation.isPending} loading={mutation.isPending}
> >
Save Simpan
</Button> </Button>
)} )}
</Flex> </Flex>

View File

@ -34,26 +34,26 @@ export const getUserByIdQueryOptions = (userId: string | undefined) =>
}); });
export const createUser = async ( export const createUser = async (
form: InferRequestType<typeof client.users.$post>["form"] json: InferRequestType<typeof client.users.$post>["json"]
) => { ) => {
return await fetchRPC( return await fetchRPC(
client.users.$post({ client.users.$post({
form, json,
}) })
); );
}; };
export const updateUser = async ( export const updateUser = async (
form: InferRequestType<(typeof client.users)[":id"]["$patch"]>["form"] & { json: InferRequestType<(typeof client.users)[":id"]["$patch"]>["json"] & {
id: string; id: string;
} }
) => { ) => {
return await fetchRPC( return await fetchRPC(
client.users[":id"].$patch({ client.users[":id"].$patch({
param: { param: {
id: form.id, id: json.id,
}, },
form, json,
}) })
); );
}; };

View File

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

View File

@ -20,17 +20,17 @@ const columnHelper = createColumnHelper<DataType>();
export default function UsersPage() { export default function UsersPage() {
return ( return (
<PageTemplate <PageTemplate
title="Users" title="Manajemen Pengguna"
queryOptions={userQueryOptions} queryOptions={userQueryOptions}
modals={[<UserFormModal />, <UserDeleteModal />]} modals={[<UserFormModal />, <UserDeleteModal />]}
columnDefs={[ columnDefs={[
columnHelper.display({ columnHelper.display({
header: "#", header: "No",
cell: (props) => props.row.index + 1, cell: (props) => props.row.index + 1,
}), }),
columnHelper.display({ columnHelper.display({
header: "Name", header: "Nama",
cell: (props) => props.row.original.name, cell: (props) => props.row.original.name,
}), }),
@ -38,19 +38,28 @@ export default function UsersPage() {
header: "Username", header: "Username",
cell: (props) => props.row.original.username, cell: (props) => props.row.original.username,
}), }),
columnHelper.display({ columnHelper.display({
header: "Status", header: "Email",
cell: (props) => cell: (props) => props.row.original.email,
props.row.original.isEnabled ? ( }),
<Badge color="green">Active</Badge> columnHelper.display({
) : ( header: "Perusahaan",
<Badge color="red">Inactive</Badge> cell: (props) => props.row.original.company,
),
}), }),
columnHelper.display({ columnHelper.display({
header: "Actions", header: "Roles",
cell: (props) => {
const roles = props.row.original.roles; // Get array of roles from data
if (roles && roles.length > 0) {
return roles.map(role => role.name).join(", ");
}
return <div>-</div>;
},
}),
columnHelper.display({
header: "Aksi",
cell: (props) => ( cell: (props) => (
<Flex gap="xs"> <Flex gap="xs">
{createActionButtons([ {createActionButtons([
@ -62,14 +71,14 @@ export default function UsersPage() {
icon: <TbEye />, icon: <TbEye />,
}, },
{ {
label: "Edit", label: "Ubah",
permission: true, permission: true,
action: `?edit=${props.row.original.id}`, action: `?edit=${props.row.original.id}`,
color: "orange", color: "orange",
icon: <TbPencil />, icon: <TbPencil />,
}, },
{ {
label: "Delete", label: "Hapus",
permission: true, permission: true,
action: `?delete=${props.row.original.id}`, action: `?delete=${props.row.original.id}`,
color: "red", color: "red",

View File

@ -8,7 +8,7 @@ export default defineConfig({
plugins: [react(), TanStackRouterVite()], plugins: [react(), TanStackRouterVite()],
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname,"/src"), "@": path.resolve(__dirname,"src"),
}, },
}, },
}); });