Merge pull request #23 from digitalsolutiongroup/feat/user-management
Slicing and Integration API for User Management
This commit is contained in:
commit
670e24935e
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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 }) => ({
|
||||||
|
|
|
||||||
|
|
@ -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 })
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user