1039 lines
31 KiB
TypeScript
1039 lines
31 KiB
TypeScript
import {
|
|
useState,
|
|
useEffect,
|
|
type ReactElement,
|
|
forwardRef,
|
|
useImperativeHandle,
|
|
} from "react";
|
|
import { useForm } from "react-hook-form";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { z } from "zod";
|
|
import { Plus, ArrowUpDown, Loader2 } from "lucide-react";
|
|
import {
|
|
PrimaryButton,
|
|
SecondaryButton,
|
|
StatusBadge,
|
|
ActionDropdown,
|
|
DataTable,
|
|
Pagination,
|
|
FilterDropdown,
|
|
SearchBox,
|
|
Modal,
|
|
FormField,
|
|
FormSelect,
|
|
type Column,
|
|
} from "@/components/shared";
|
|
import { userService } from "@/services/user-service";
|
|
import { roleService } from "@/services/role-service";
|
|
import type { Role } from "@/types/role";
|
|
import type { User } from "@/types/user";
|
|
import { showToast } from "@/utils/toast";
|
|
import { formatDate } from "@/utils/format-date";
|
|
import { usePermissions } from "@/hooks/usePermissions";
|
|
|
|
// Helper function to get user initials
|
|
const getUserInitials = (firstName: string | null | undefined, lastName: string | null | undefined): string => {
|
|
const first = (firstName || "").trim().slice(0, 1);
|
|
const last = (lastName || "").trim().slice(0, 1);
|
|
return `${first}${last}`.toUpperCase() || "?";
|
|
};
|
|
|
|
// Helper function to get status badge variant
|
|
const getStatusVariant = (
|
|
status: string,
|
|
): "success" | "failure" | "process" => {
|
|
switch (status?.toLowerCase()) {
|
|
case "active":
|
|
return "success";
|
|
case "deleted":
|
|
return "failure";
|
|
case "suspended":
|
|
return "process";
|
|
default:
|
|
return "success";
|
|
}
|
|
};
|
|
|
|
// Zod schemas for validation
|
|
const newPlatformUserSchema = z.object({
|
|
email: z.string().email({ message: "Please enter a valid email address" }),
|
|
first_name: z
|
|
.string()
|
|
.min(1, "First name is required")
|
|
.max(255, "Maximum 255 characters allowed"),
|
|
last_name: z
|
|
.string()
|
|
.min(1, "Last name is required")
|
|
.max(255, "Maximum 255 characters allowed"),
|
|
status: z.enum(["active", "suspended", "deleted"], {
|
|
message: "Status is required",
|
|
}),
|
|
role_id: z.string().min(1, "Role is required"),
|
|
});
|
|
|
|
type NewPlatformUserFormData = z.infer<typeof newPlatformUserSchema>;
|
|
|
|
const editPlatformUserSchema = z.object({
|
|
email: z.string().email({ message: "Please enter a valid email address" }),
|
|
first_name: z
|
|
.string()
|
|
.min(1, "First name is required")
|
|
.max(255, "Maximum 255 characters allowed"),
|
|
last_name: z
|
|
.string()
|
|
.min(1, "Last name is required")
|
|
.max(255, "Maximum 255 characters allowed"),
|
|
status: z.enum(["active", "suspended", "deleted"], {
|
|
message: "Status is required",
|
|
}),
|
|
role_id: z.string().min(1, "Role is required"),
|
|
});
|
|
|
|
type EditPlatformUserFormData = z.infer<typeof editPlatformUserSchema>;
|
|
|
|
// Dedicated Creation Modal
|
|
interface NewPlatformUserModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onSubmit: (data: NewPlatformUserFormData) => Promise<void>;
|
|
isLoading: boolean;
|
|
roles: Role[];
|
|
}
|
|
|
|
const NewPlatformUserModal = ({
|
|
isOpen,
|
|
onClose,
|
|
onSubmit,
|
|
isLoading,
|
|
roles,
|
|
}: NewPlatformUserModalProps): ReactElement | null => {
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
setValue,
|
|
watch,
|
|
reset,
|
|
setError,
|
|
formState: { errors },
|
|
} = useForm<NewPlatformUserFormData>({
|
|
resolver: zodResolver(newPlatformUserSchema),
|
|
defaultValues: {
|
|
status: "active",
|
|
role_id: "",
|
|
},
|
|
});
|
|
|
|
const statusValue = watch("status");
|
|
const roleIdValue = watch("role_id");
|
|
|
|
useEffect(() => {
|
|
if (!isOpen) {
|
|
reset({
|
|
email: "",
|
|
first_name: "",
|
|
last_name: "",
|
|
status: "active",
|
|
role_id: "",
|
|
});
|
|
}
|
|
}, [isOpen, reset]);
|
|
|
|
const handleFormSubmit = async (data: NewPlatformUserFormData) => {
|
|
try {
|
|
await onSubmit(data);
|
|
} catch (error: any) {
|
|
if (
|
|
error?.response?.data?.details &&
|
|
Array.isArray(error.response.data.details)
|
|
) {
|
|
const first = error.response.data.details[0];
|
|
setError("root", {
|
|
type: "server",
|
|
message: first?.message || "Validation failed",
|
|
});
|
|
return;
|
|
}
|
|
const message =
|
|
error?.response?.data?.error?.message ||
|
|
error?.response?.data?.message ||
|
|
error?.message ||
|
|
"Failed to create platform user. Please try again.";
|
|
setError("root", { type: "server", message });
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Modal
|
|
isOpen={isOpen}
|
|
onClose={onClose}
|
|
title="Create New Platform User"
|
|
description="Register a new platform-scoped administrator"
|
|
maxWidth="md"
|
|
footer={
|
|
<>
|
|
<SecondaryButton
|
|
type="button"
|
|
onClick={onClose}
|
|
disabled={isLoading}
|
|
className="px-4 py-2.5 text-sm"
|
|
>
|
|
Cancel
|
|
</SecondaryButton>
|
|
<PrimaryButton
|
|
type="button"
|
|
onClick={handleSubmit(handleFormSubmit)}
|
|
disabled={isLoading}
|
|
size="default"
|
|
className="px-4 py-2.5 text-sm"
|
|
>
|
|
{isLoading ? "Creating..." : "Create User"}
|
|
</PrimaryButton>
|
|
</>
|
|
}
|
|
>
|
|
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
|
{errors.root && (
|
|
<div className="p-4 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md mb-4">
|
|
<p className="text-sm text-[#ef4444]">{errors.root.message}</p>
|
|
</div>
|
|
)}
|
|
|
|
<FormField
|
|
label="Email"
|
|
type="email"
|
|
required
|
|
placeholder="Enter email address"
|
|
error={errors.email?.message}
|
|
{...register("email")}
|
|
/>
|
|
<div className="grid grid-cols-2 gap-5 pb-4">
|
|
<FormField
|
|
label="First Name"
|
|
required
|
|
placeholder="Enter first name"
|
|
error={errors.first_name?.message}
|
|
{...register("first_name")}
|
|
/>
|
|
<FormField
|
|
label="Last Name"
|
|
required
|
|
placeholder="Enter last name"
|
|
error={errors.last_name?.message}
|
|
{...register("last_name")}
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-5 pb-4">
|
|
<FormSelect
|
|
label="Role"
|
|
required
|
|
placeholder="Select Role"
|
|
options={roles.map((r) => ({ value: r.id, label: r.name }))}
|
|
value={roleIdValue || ""}
|
|
onValueChange={(value) =>
|
|
setValue("role_id", value, { shouldValidate: true })
|
|
}
|
|
error={errors.role_id?.message}
|
|
/>
|
|
<FormSelect
|
|
label="Status"
|
|
required
|
|
placeholder="Select Status"
|
|
options={[
|
|
{ value: "active", label: "Active" },
|
|
{ value: "suspended", label: "Suspended" },
|
|
{ value: "deleted", label: "Deleted" },
|
|
]}
|
|
value={statusValue || "active"}
|
|
onValueChange={(value) =>
|
|
setValue("status", value as "active" | "suspended" | "deleted", {
|
|
shouldValidate: true,
|
|
})
|
|
}
|
|
error={errors.status?.message}
|
|
/>
|
|
</div>
|
|
</form>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
// Dedicated Edit Modal
|
|
interface EditPlatformUserModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
userId: string | null;
|
|
onSubmit: (id: string, data: EditPlatformUserFormData) => Promise<void>;
|
|
isLoading: boolean;
|
|
roles: Role[];
|
|
}
|
|
|
|
const EditPlatformUserModal = ({
|
|
isOpen,
|
|
onClose,
|
|
userId,
|
|
onSubmit,
|
|
isLoading,
|
|
roles,
|
|
}: EditPlatformUserModalProps): ReactElement | null => {
|
|
const [loadingData, setLoadingData] = useState(false);
|
|
const [loadedUser, setLoadedUser] = useState<any>(null);
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
setValue,
|
|
watch,
|
|
reset,
|
|
setError,
|
|
formState: { errors },
|
|
} = useForm<EditPlatformUserFormData>({
|
|
resolver: zodResolver(editPlatformUserSchema),
|
|
});
|
|
|
|
const statusValue = watch("status");
|
|
const roleIdValue = watch("role_id");
|
|
|
|
const selectedRole = roles.find((r) => r.id === roleIdValue);
|
|
const primaryRoleName = loadedUser?.role_module_combinations?.[0]?.role_name || "";
|
|
const primaryRoleCode = loadedUser?.role_module_combinations?.[0]?.role_code || "";
|
|
const isSuperAdminUser =
|
|
primaryRoleName === "Super Admin" ||
|
|
primaryRoleName === "super_admin" ||
|
|
primaryRoleCode === "super_admin" ||
|
|
selectedRole?.name === "Super Admin" ||
|
|
selectedRole?.code === "super_admin";
|
|
|
|
useEffect(() => {
|
|
if (isOpen && userId) {
|
|
setLoadingData(true);
|
|
userService
|
|
.getById(userId)
|
|
.then((response) => {
|
|
if (response.success && response.data) {
|
|
const user = response.data;
|
|
setLoadedUser(user);
|
|
const primaryRole =
|
|
user.role_module_combinations?.[0]?.role_id || "";
|
|
reset({
|
|
email: user.email || "",
|
|
first_name: user.first_name || "",
|
|
last_name: user.last_name || "",
|
|
status: user.status || "active",
|
|
role_id: primaryRole,
|
|
});
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
console.error("Failed to load platform user details", err);
|
|
showToast.error("Failed to load user details");
|
|
onClose();
|
|
})
|
|
.finally(() => {
|
|
setLoadingData(false);
|
|
});
|
|
} else if (!isOpen) {
|
|
setLoadedUser(null);
|
|
}
|
|
}, [isOpen, userId, reset, onClose]);
|
|
|
|
const handleFormSubmit = async (data: EditPlatformUserFormData) => {
|
|
if (!userId) return;
|
|
try {
|
|
await onSubmit(userId, data);
|
|
} catch (error: any) {
|
|
if (
|
|
error?.response?.data?.details &&
|
|
Array.isArray(error.response.data.details)
|
|
) {
|
|
const first = error.response.data.details[0];
|
|
setError("root", {
|
|
type: "server",
|
|
message: first?.message || "Validation failed",
|
|
});
|
|
return;
|
|
}
|
|
const message =
|
|
error?.response?.data?.error?.message ||
|
|
error?.response?.data?.message ||
|
|
error?.message ||
|
|
"Failed to update platform user. Please try again.";
|
|
setError("root", { type: "server", message });
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Modal
|
|
isOpen={isOpen}
|
|
onClose={onClose}
|
|
title="Edit Platform User"
|
|
description="Update platform user settings and role"
|
|
maxWidth="md"
|
|
footer={
|
|
<>
|
|
<SecondaryButton
|
|
type="button"
|
|
onClick={onClose}
|
|
disabled={isLoading || loadingData}
|
|
className="px-4 py-2.5 text-sm"
|
|
>
|
|
Cancel
|
|
</SecondaryButton>
|
|
<PrimaryButton
|
|
type="button"
|
|
onClick={handleSubmit(handleFormSubmit)}
|
|
disabled={isLoading || loadingData}
|
|
size="default"
|
|
className="px-4 py-2.5 text-sm"
|
|
>
|
|
{isLoading ? "Saving..." : "Save Changes"}
|
|
</PrimaryButton>
|
|
</>
|
|
}
|
|
>
|
|
{loadingData ? (
|
|
<div className="flex flex-col items-center justify-center py-12">
|
|
<Loader2 className="w-8 h-8 text-[#2563eb] animate-spin" />
|
|
<p className="text-sm text-[#4b5563] mt-2">Loading user details...</p>
|
|
</div>
|
|
) : (
|
|
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
|
{errors.root && (
|
|
<div className="p-4 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md mb-4">
|
|
<p className="text-sm text-[#ef4444]">{errors.root.message}</p>
|
|
</div>
|
|
)}
|
|
|
|
<FormField
|
|
label="Email"
|
|
type="email"
|
|
required
|
|
placeholder="Enter email address"
|
|
error={errors.email?.message}
|
|
{...register("email")}
|
|
/>
|
|
<div className="grid grid-cols-2 gap-5 pb-4">
|
|
<FormField
|
|
label="First Name"
|
|
required
|
|
placeholder="Enter first name"
|
|
error={errors.first_name?.message}
|
|
{...register("first_name")}
|
|
/>
|
|
<FormField
|
|
label="Last Name"
|
|
required
|
|
placeholder="Enter last name"
|
|
error={errors.last_name?.message}
|
|
{...register("last_name")}
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-5 pb-4">
|
|
{!isSuperAdminUser && (
|
|
<>
|
|
<FormSelect
|
|
label="Role"
|
|
required
|
|
placeholder="Select Role"
|
|
options={roles.map((r) => ({ value: r.id, label: r.name }))}
|
|
value={roleIdValue || ""}
|
|
onValueChange={(value) =>
|
|
setValue("role_id", value, { shouldValidate: true })
|
|
}
|
|
error={errors.role_id?.message}
|
|
/>
|
|
<FormSelect
|
|
label="Status"
|
|
required
|
|
placeholder="Select Status"
|
|
options={[
|
|
{ value: "active", label: "Active" },
|
|
{ value: "suspended", label: "Suspended" },
|
|
{ value: "deleted", label: "Deleted" },
|
|
]}
|
|
value={statusValue || "active"}
|
|
onValueChange={(value) =>
|
|
setValue(
|
|
"status",
|
|
value as "active" | "suspended" | "deleted",
|
|
{ shouldValidate: true },
|
|
)
|
|
}
|
|
error={errors.status?.message}
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
</form>
|
|
)}
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
// Dedicated View Modal
|
|
interface ViewPlatformUserModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
userId: string | null;
|
|
}
|
|
|
|
const ViewPlatformUserModal = ({
|
|
isOpen,
|
|
onClose,
|
|
userId,
|
|
}: ViewPlatformUserModalProps): ReactElement | null => {
|
|
const [user, setUser] = useState<User | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (isOpen && userId) {
|
|
setLoading(true);
|
|
userService
|
|
.getById(userId)
|
|
.then((response) => {
|
|
if (response.success && response.data) {
|
|
setUser(response.data);
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
console.error("Failed to fetch user details", err);
|
|
showToast.error("Failed to load user details");
|
|
})
|
|
.finally(() => {
|
|
setLoading(false);
|
|
});
|
|
}
|
|
}, [isOpen, userId]);
|
|
|
|
return (
|
|
<Modal
|
|
isOpen={isOpen}
|
|
onClose={onClose}
|
|
title="Platform User Details"
|
|
description="View full configuration details"
|
|
maxWidth="md"
|
|
footer={
|
|
<PrimaryButton type="button" onClick={onClose} className="px-4 py-2">
|
|
Close
|
|
</PrimaryButton>
|
|
}
|
|
>
|
|
{loading ? (
|
|
<div className="flex flex-col items-center justify-center py-12">
|
|
<Loader2 className="w-8 h-8 text-[#2563eb] animate-spin" />
|
|
<p className="text-sm text-[#4b5563] mt-2">Loading details...</p>
|
|
</div>
|
|
) : user ? (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center gap-4 p-4 bg-[#f8fafc] border border-[rgba(0,0,0,0.05)] rounded-xl">
|
|
<div className="w-12 h-12 bg-white border border-[rgba(0,0,0,0.08)] rounded-xl flex items-center justify-center shadow-sm">
|
|
<span className="text-lg font-semibold text-[#1e293b]">
|
|
{getUserInitials(user.first_name, user.last_name)}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<h3 className="text-base font-semibold text-[#0f172a]">
|
|
{user.first_name} {user.last_name}
|
|
</h3>
|
|
<p className="text-sm text-[#475569]">{user.email}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-x-6 gap-y-4 text-sm">
|
|
<div>
|
|
<span className="text-xs font-semibold text-[#64748b] uppercase tracking-wider block mb-1">
|
|
Status
|
|
</span>
|
|
<StatusBadge variant={getStatusVariant(user.status)}>
|
|
{user.status}
|
|
</StatusBadge>
|
|
</div>
|
|
<div>
|
|
<span className="text-xs font-semibold text-[#64748b] uppercase tracking-wider block mb-1">
|
|
Role
|
|
</span>
|
|
<p className="text-[#0f172a] font-medium">
|
|
{user.role_module_combinations?.[0]?.role_name ||
|
|
"Platform User"}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<span className="text-xs font-semibold text-[#64748b] uppercase tracking-wider block mb-1">
|
|
Joined Date
|
|
</span>
|
|
<p className="text-[#334155]">{formatDate(user.created_at)}</p>
|
|
</div>
|
|
<div>
|
|
<span className="text-xs font-semibold text-[#64748b] uppercase tracking-wider block mb-1">
|
|
Last Updated
|
|
</span>
|
|
<p className="text-[#334155]">{formatDate(user.updated_at)}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="py-8 text-center text-sm text-[#64748b]">
|
|
No user data available.
|
|
</div>
|
|
)}
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
export interface PlatformUsersTableRef {
|
|
openNewModal: () => void;
|
|
refresh: () => void;
|
|
}
|
|
|
|
export const PlatformUsersTable = forwardRef<PlatformUsersTableRef>(
|
|
(_, ref): ReactElement => {
|
|
const { canCreate, canUpdate } = usePermissions();
|
|
const [users, setUsers] = useState<User[]>([]);
|
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [isCreateOpen, setIsCreateOpen] = useState<boolean>(false);
|
|
const [isCreating, setIsCreating] = useState<boolean>(false);
|
|
const [isEditOpen, setIsEditOpen] = useState<boolean>(false);
|
|
const [isUpdating, setIsUpdating] = useState<boolean>(false);
|
|
const [isViewOpen, setIsViewOpen] = useState<boolean>(false);
|
|
const [selectedUserId, setSelectedUserId] = useState<string | null>(null);
|
|
|
|
// Filter states
|
|
const [search, setSearch] = useState<string>("");
|
|
const [debouncedSearch, setDebouncedSearch] = useState<string>("");
|
|
const [statusFilter, setStatusFilter] = useState<string | null>(null);
|
|
const [roleFilter, setRoleFilter] = useState<string | null>(null);
|
|
const [orderBy, setOrderBy] = useState<string[] | null>(null);
|
|
|
|
// Pagination states
|
|
const [currentPage, setCurrentPage] = useState<number>(1);
|
|
const [limit, setLimit] = useState<number>(10);
|
|
const [pagination, setPagination] = useState({
|
|
page: 1,
|
|
limit: 10,
|
|
total: 0,
|
|
totalPages: 1,
|
|
hasMore: false,
|
|
});
|
|
|
|
const [roles, setRoles] = useState<Role[]>([]);
|
|
|
|
// Expose methods
|
|
useImperativeHandle(ref, () => ({
|
|
openNewModal: () => setIsCreateOpen(true),
|
|
refresh: () => fetchPlatformUsers(),
|
|
}));
|
|
|
|
// Debounce search
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => {
|
|
setDebouncedSearch(search);
|
|
setCurrentPage(1);
|
|
}, 300);
|
|
return () => clearTimeout(timer);
|
|
}, [search]);
|
|
|
|
const fetchPlatformUsers = async () => {
|
|
try {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
// Call backend with scope="platform"
|
|
const response = await userService.getAll(
|
|
currentPage,
|
|
limit,
|
|
statusFilter,
|
|
orderBy,
|
|
debouncedSearch,
|
|
roleFilter,
|
|
"platform",
|
|
);
|
|
if (response.success) {
|
|
setUsers(response.data);
|
|
setPagination(response.pagination);
|
|
} else {
|
|
setError("Failed to fetch platform users");
|
|
}
|
|
} catch (err: any) {
|
|
setError(
|
|
err?.response?.data?.error?.message ||
|
|
"Failed to load platform users",
|
|
);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
// Fetch users when parameters change
|
|
useEffect(() => {
|
|
fetchPlatformUsers();
|
|
}, [
|
|
currentPage,
|
|
limit,
|
|
statusFilter,
|
|
roleFilter,
|
|
orderBy,
|
|
debouncedSearch,
|
|
]);
|
|
|
|
// Fetch platform roles
|
|
useEffect(() => {
|
|
const fetchPlatformRoles = async () => {
|
|
try {
|
|
const response = await roleService.getAll(
|
|
1,
|
|
100,
|
|
null,
|
|
null,
|
|
"platform",
|
|
true,
|
|
);
|
|
if (response.success) {
|
|
setRoles(response.data);
|
|
}
|
|
} catch (err: any) {
|
|
console.error("Failed to load platform roles", err);
|
|
}
|
|
};
|
|
fetchPlatformRoles();
|
|
}, []);
|
|
|
|
const handleCreateUser = async (data: NewPlatformUserFormData) => {
|
|
try {
|
|
setIsCreating(true);
|
|
const payload = {
|
|
email: data.email,
|
|
first_name: data.first_name,
|
|
last_name: data.last_name,
|
|
status: data.status,
|
|
auth_provider: "local" as const,
|
|
tenant_id: "00000000-0000-0000-0000-000000000001",
|
|
role_module_combinations: [
|
|
{ role_id: data.role_id, module_id: null },
|
|
],
|
|
department_id: null,
|
|
designation_id: null,
|
|
category: "tenant_user" as const,
|
|
};
|
|
const response = await userService.create(payload);
|
|
showToast.success(
|
|
response.message || "Platform user created successfully",
|
|
);
|
|
setIsCreateOpen(false);
|
|
fetchPlatformUsers();
|
|
} catch (err: any) {
|
|
throw err;
|
|
} finally {
|
|
setIsCreating(false);
|
|
}
|
|
};
|
|
|
|
const handleUpdateUser = async (
|
|
id: string,
|
|
data: EditPlatformUserFormData,
|
|
) => {
|
|
try {
|
|
setIsUpdating(true);
|
|
const payload = {
|
|
email: data.email,
|
|
first_name: data.first_name,
|
|
last_name: data.last_name,
|
|
status: data.status,
|
|
tenant_id: "00000000-0000-0000-0000-000000000001",
|
|
role_module_combinations: [
|
|
{ role_id: data.role_id, module_id: null },
|
|
],
|
|
department_id: null,
|
|
designation_id: null,
|
|
};
|
|
const response = await userService.update(id, payload);
|
|
showToast.success(
|
|
response.message || "Platform user updated successfully",
|
|
);
|
|
setIsEditOpen(false);
|
|
fetchPlatformUsers();
|
|
} catch (err: any) {
|
|
throw err;
|
|
} finally {
|
|
setIsUpdating(false);
|
|
}
|
|
};
|
|
|
|
const columns: Column<User>[] = [
|
|
{
|
|
key: "name",
|
|
label: "Name",
|
|
render: (user) => (
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-8 h-8 bg-[#f1f5f9] border border-[rgba(0,0,0,0.08)] rounded-lg flex items-center justify-center shrink-0">
|
|
<span className="text-xs font-semibold text-[#475569]">
|
|
{getUserInitials(user.first_name, user.last_name)}
|
|
</span>
|
|
</div>
|
|
<span className="font-medium text-[#0f172a]">
|
|
{user.first_name} {user.last_name}
|
|
</span>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: "email",
|
|
label: "Email",
|
|
render: (user) => <span className="text-[#334155]">{user.email}</span>,
|
|
},
|
|
{
|
|
key: "role",
|
|
label: "Role",
|
|
render: (user) => (
|
|
<span className="text-[#334155] font-medium">
|
|
{user.role_module_combinations?.[0]?.role_name || "Platform User"}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: "status",
|
|
label: "Status",
|
|
render: (user) => (
|
|
<StatusBadge variant={getStatusVariant(user.status)}>
|
|
{user.status}
|
|
</StatusBadge>
|
|
),
|
|
},
|
|
{
|
|
key: "created_at",
|
|
label: "Joined Date",
|
|
render: (user) => (
|
|
<span className="text-[#64748b]">{formatDate(user.created_at)}</span>
|
|
),
|
|
mobileLabel: "Joined",
|
|
},
|
|
{
|
|
key: "actions",
|
|
label: "Actions",
|
|
align: "right",
|
|
render: (user) => (
|
|
<div className="flex justify-end">
|
|
<ActionDropdown
|
|
onView={() => {
|
|
setSelectedUserId(user.id);
|
|
setIsViewOpen(true);
|
|
}}
|
|
onEdit={
|
|
canUpdate("users")
|
|
? () => {
|
|
setSelectedUserId(user.id);
|
|
setIsEditOpen(true);
|
|
}
|
|
: undefined
|
|
}
|
|
/>
|
|
</div>
|
|
),
|
|
},
|
|
];
|
|
|
|
const mobileCardRenderer = (user: User) => (
|
|
<div className="p-4 border-b border-[rgba(0,0,0,0.05)]">
|
|
<div className="flex items-start justify-between gap-3 mb-3">
|
|
<div className="flex items-center gap-3 flex-1 min-w-0">
|
|
<div className="w-8 h-8 bg-[#f1f5f9] border border-[rgba(0,0,0,0.08)] rounded-lg flex items-center justify-center shrink-0">
|
|
<span className="text-xs font-semibold text-[#475569]">
|
|
{getUserInitials(user.first_name, user.last_name)}
|
|
</span>
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="text-sm font-medium text-[#0f1724] truncate">
|
|
{user.first_name} {user.last_name}
|
|
</h3>
|
|
<p className="text-xs text-[#64748b] mt-0.5 truncate">
|
|
{user.email}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<ActionDropdown
|
|
onView={() => {
|
|
setSelectedUserId(user.id);
|
|
setIsViewOpen(true);
|
|
}}
|
|
onEdit={
|
|
canUpdate("users")
|
|
? () => {
|
|
setSelectedUserId(user.id);
|
|
setIsEditOpen(true);
|
|
}
|
|
: undefined
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-3 text-xs">
|
|
<div>
|
|
<span className="text-[#94a3b8] block">Role</span>
|
|
<span className="text-[#0f172a] font-medium mt-0.5 block">
|
|
{user.role_module_combinations?.[0]?.role_name || "Platform User"}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-[#94a3b8] block">Status</span>
|
|
<div className="mt-1">
|
|
<StatusBadge variant={getStatusVariant(user.status)}>
|
|
{user.status}
|
|
</StatusBadge>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<span className="text-[#94a3b8] block">Joined</span>
|
|
<span className="text-[#64748b] mt-0.5 block">
|
|
{formatDate(user.created_at)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const handleClearFilters = () => {
|
|
setSearch("");
|
|
setStatusFilter(null);
|
|
setRoleFilter(null);
|
|
setOrderBy(null);
|
|
setCurrentPage(1);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="overflow-hidden">
|
|
<div className="pb-4 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<SearchBox
|
|
value={search}
|
|
onChange={setSearch}
|
|
placeholder="Search platform users..."
|
|
/>
|
|
<FilterDropdown
|
|
label="Status"
|
|
options={[
|
|
{ value: "active", label: "Active" },
|
|
{ value: "suspended", label: "Suspended" },
|
|
{ value: "deleted", label: "Deleted" },
|
|
]}
|
|
value={statusFilter}
|
|
onChange={(value) => {
|
|
setStatusFilter(value as string | null);
|
|
setCurrentPage(1);
|
|
}}
|
|
placeholder="All Status"
|
|
/>
|
|
<FilterDropdown
|
|
label="Role"
|
|
options={roles.map((r) => ({ value: r.id, label: r.name }))}
|
|
value={roleFilter}
|
|
onChange={(value) => {
|
|
setRoleFilter(value as string | null);
|
|
setCurrentPage(1);
|
|
}}
|
|
placeholder="All Roles"
|
|
/>
|
|
<FilterDropdown
|
|
label="Sort by"
|
|
options={[
|
|
{ value: ["first_name", "asc"], label: "First Name (A-Z)" },
|
|
{ value: ["first_name", "desc"], label: "First Name (Z-A)" },
|
|
{ value: ["email", "asc"], label: "Email (A-Z)" },
|
|
{ value: ["email", "desc"], label: "Email (Z-A)" },
|
|
{ value: ["created_at", "asc"], label: "Joined (Oldest)" },
|
|
{ value: ["created_at", "desc"], label: "Joined (Newest)" },
|
|
]}
|
|
value={orderBy}
|
|
onChange={(value) => {
|
|
setOrderBy(value as string[] | null);
|
|
setCurrentPage(1);
|
|
}}
|
|
placeholder="Default Sort"
|
|
showIcon
|
|
icon={<ArrowUpDown className="w-3.5 h-3.5 text-[#475569]" />}
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{canCreate("users") && (
|
|
<PrimaryButton
|
|
size="default"
|
|
className="flex items-center gap-2"
|
|
onClick={() => setIsCreateOpen(true)}
|
|
>
|
|
<Plus className="w-3.5 h-3.5" />
|
|
<span className="text-xs">New Platform User</span>
|
|
</PrimaryButton>
|
|
)}
|
|
{(!!search ||
|
|
statusFilter !== null ||
|
|
roleFilter !== null ||
|
|
orderBy !== null) && (
|
|
<button
|
|
onClick={handleClearFilters}
|
|
className="text-sm font-medium text-[#64748b] hover:text-[#0f172a] transition-colors cursor-pointer"
|
|
>
|
|
Clear filters
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<DataTable
|
|
data={users}
|
|
columns={columns}
|
|
keyExtractor={(user) => user.id}
|
|
mobileCardRenderer={mobileCardRenderer}
|
|
emptyMessage="No platform users found"
|
|
isLoading={isLoading}
|
|
error={error}
|
|
/>
|
|
|
|
{pagination.totalPages > 0 && (
|
|
<Pagination
|
|
currentPage={currentPage}
|
|
totalPages={pagination.totalPages}
|
|
totalItems={pagination.total}
|
|
limit={limit}
|
|
onPageChange={(page: number) => {
|
|
setCurrentPage(page);
|
|
}}
|
|
onLimitChange={(newLimit: number) => {
|
|
setLimit(newLimit);
|
|
setCurrentPage(1);
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* Modal Declarations */}
|
|
<NewPlatformUserModal
|
|
isOpen={isCreateOpen}
|
|
onClose={() => setIsCreateOpen(false)}
|
|
onSubmit={handleCreateUser}
|
|
isLoading={isCreating}
|
|
roles={roles}
|
|
/>
|
|
|
|
<EditPlatformUserModal
|
|
isOpen={isEditOpen}
|
|
onClose={() => {
|
|
setIsEditOpen(false);
|
|
setSelectedUserId(null);
|
|
}}
|
|
userId={selectedUserId}
|
|
onSubmit={handleUpdateUser}
|
|
isLoading={isUpdating}
|
|
roles={roles}
|
|
/>
|
|
|
|
<ViewPlatformUserModal
|
|
isOpen={isViewOpen}
|
|
onClose={() => {
|
|
setIsViewOpen(false);
|
|
setSelectedUserId(null);
|
|
}}
|
|
userId={selectedUserId}
|
|
/>
|
|
</>
|
|
);
|
|
},
|
|
);
|
|
|
|
PlatformUsersTable.displayName = "PlatformUsersTable";
|