Update user role management and dialog designs
Remove order receiver role toggle, redesign role dialogs with improved styling and responsiveness, and unify timestamp formatting.
This commit is contained in:
@@ -4148,9 +4148,6 @@ function UsersPanel({
|
||||
const createUser = useCreateUser();
|
||||
const updateUser = useUpdateUser();
|
||||
const deleteUser = useDeleteUser();
|
||||
const addUserRole = useAddUserRole();
|
||||
const removeUserRole = useRemoveUserRole();
|
||||
|
||||
const filtered = (users ?? []).slice().sort((a, b) => {
|
||||
const av =
|
||||
sortKey === "createdAt" ? a.createdAt : sortKey === "email" ? a.email : a.username;
|
||||
@@ -4368,57 +4365,6 @@ function UsersPanel({
|
||||
>
|
||||
<KeyRound size={14} />
|
||||
</button>
|
||||
{(() => {
|
||||
// The Switch toggles the DIRECT user_roles row only.
|
||||
// When the role is also inherited via a group, we show
|
||||
// a small badge so the admin understands removing the
|
||||
// direct row may not revoke effective access.
|
||||
const hasEffective = u.roles?.includes("order_receiver") ?? false;
|
||||
const hasDirect = u.directRoles?.includes("order_receiver") ?? false;
|
||||
const isInherited = hasEffective && !hasDirect;
|
||||
return (
|
||||
<label
|
||||
className="flex items-center gap-1 text-[10px] text-muted-foreground select-none"
|
||||
title={
|
||||
isInherited
|
||||
? t("admin.roleInheritedFromGroup")
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<span className="hidden lg:inline">
|
||||
{t("admin.orderReceiverRole")}
|
||||
</span>
|
||||
<Switch
|
||||
checked={hasDirect}
|
||||
onCheckedChange={(v) => {
|
||||
const opts = {
|
||||
onSuccess: invalidateUsers,
|
||||
onError: () =>
|
||||
toast({ title: t("common.error"), variant: "destructive" }),
|
||||
};
|
||||
if (v)
|
||||
addUserRole.mutate(
|
||||
{ id: u.id, data: { roleName: "order_receiver" } },
|
||||
opts,
|
||||
);
|
||||
else
|
||||
removeUserRole.mutate(
|
||||
{ id: u.id, roleName: "order_receiver" },
|
||||
opts,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{isInherited ? (
|
||||
<span
|
||||
className="ms-1 rounded bg-amber-100 px-1 py-0.5 text-[9px] font-medium text-amber-800"
|
||||
data-testid={`order-receiver-inherited-${u.id}`}
|
||||
>
|
||||
{t("admin.roleInheritedFromGroup")}
|
||||
</span>
|
||||
) : null}
|
||||
</label>
|
||||
);
|
||||
})()}
|
||||
{u.id !== currentUserId && (
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -6921,18 +6867,26 @@ function RolesPanel({
|
||||
)}
|
||||
|
||||
{editingId != null && (
|
||||
<div className="fixed inset-0 bg-slate-900/30 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||||
<div className="fixed inset-0 bg-slate-900/50 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||||
<div
|
||||
className="glass-panel rounded-3xl p-6 w-full max-w-md space-y-3 max-h-[90vh] overflow-y-auto"
|
||||
className="relative bg-white rounded-2xl sm:rounded-3xl shadow-2xl ring-1 ring-slate-200 p-5 sm:p-6 w-full max-w-[min(100%,560px)] sm:max-w-xl md:max-w-2xl space-y-4 max-h-[92vh] overflow-y-auto"
|
||||
data-testid="edit-role-dialog"
|
||||
>
|
||||
<h2 className="font-semibold text-foreground">{t("admin.roles.editRole")}</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeEditDialog}
|
||||
aria-label={t("common.cancel")}
|
||||
className="absolute top-3 end-3 inline-flex items-center justify-center w-9 h-9 rounded-full text-slate-500 hover:text-slate-900 hover:bg-slate-100"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
<h2 className="text-lg sm:text-xl font-semibold text-foreground pe-10">{t("admin.roles.editRole")}</h2>
|
||||
<div className="space-y-1">
|
||||
<Label>{t("admin.roles.name")}</Label>
|
||||
<Input
|
||||
value={editForm.name}
|
||||
onChange={(e) => setEditForm({ ...editForm, name: e.target.value })}
|
||||
className="bg-white/70 border-slate-200"
|
||||
className="bg-white border-2 border-slate-300 rounded-xl min-h-11 text-base"
|
||||
placeholder={t("admin.roles.namePlaceholder")}
|
||||
disabled={editingIsSystem}
|
||||
data-testid="role-edit-name"
|
||||
@@ -6972,7 +6926,7 @@ function RolesPanel({
|
||||
<Input
|
||||
value={editForm.descriptionAr}
|
||||
onChange={(e) => setEditForm({ ...editForm, descriptionAr: e.target.value })}
|
||||
className="bg-white/70 border-slate-200"
|
||||
className="bg-white border-2 border-slate-300 rounded-xl min-h-11 text-base"
|
||||
dir="rtl"
|
||||
data-testid="role-edit-desc-ar"
|
||||
/>
|
||||
@@ -6982,7 +6936,7 @@ function RolesPanel({
|
||||
<Input
|
||||
value={editForm.descriptionEn}
|
||||
onChange={(e) => setEditForm({ ...editForm, descriptionEn: e.target.value })}
|
||||
className="bg-white/70 border-slate-200"
|
||||
className="bg-white border-2 border-slate-300 rounded-xl min-h-11 text-base"
|
||||
dir="ltr"
|
||||
data-testid="role-edit-desc-en"
|
||||
/>
|
||||
@@ -6995,7 +6949,7 @@ function RolesPanel({
|
||||
: t("admin.roles.permissionsHint")}
|
||||
</p>
|
||||
<div
|
||||
className="border border-slate-200 rounded-xl bg-white/60 max-h-56 overflow-y-auto divide-y divide-slate-100"
|
||||
className="border-2 border-slate-300 rounded-xl bg-white max-h-[50vh] overflow-y-auto divide-y divide-slate-100"
|
||||
data-testid="role-permissions-list"
|
||||
>
|
||||
{editingPermsLoading || !allPermissions ? (
|
||||
@@ -7125,12 +7079,12 @@ function RolesPanel({
|
||||
onOpenAuditLogForTarget("role", editingPermissionsId);
|
||||
}}
|
||||
/>
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button variant="outline" className="flex-1" onClick={closeEditDialog}>
|
||||
<div className="flex gap-2 pt-3 sticky bottom-0 bg-white -mx-5 sm:-mx-6 px-5 sm:px-6 -mb-5 sm:-mb-6 pb-5 sm:pb-6 border-t border-slate-100">
|
||||
<Button variant="outline" className="flex-1 min-h-11 rounded-xl text-base" onClick={closeEditDialog}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1"
|
||||
className="flex-1 min-h-11 rounded-xl text-base"
|
||||
disabled={
|
||||
!editForm.name.trim() ||
|
||||
updateRole.isPending ||
|
||||
@@ -7152,9 +7106,9 @@ function RolesPanel({
|
||||
)}
|
||||
|
||||
{confirmRemoval && permissionsImpact && permissionsImpact.totalAffectedUsers > 0 && (
|
||||
<div className="fixed inset-0 bg-slate-900/40 backdrop-blur-sm z-[60] flex items-center justify-center p-4">
|
||||
<div className="fixed inset-0 bg-slate-900/50 backdrop-blur-sm z-[60] flex items-center justify-center p-4">
|
||||
<div
|
||||
className="glass-panel rounded-3xl p-6 w-full max-w-md space-y-3"
|
||||
className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl ring-1 ring-slate-200 p-5 sm:p-6 w-full max-w-[min(100%,520px)] space-y-3"
|
||||
data-testid="role-perm-confirm-dialog"
|
||||
role="alertdialog"
|
||||
aria-modal="true"
|
||||
@@ -7184,7 +7138,7 @@ function RolesPanel({
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
className="flex-1 min-h-11 rounded-xl text-base"
|
||||
onClick={() => setConfirmRemoval(false)}
|
||||
data-testid="role-perm-confirm-cancel"
|
||||
>
|
||||
@@ -7192,7 +7146,7 @@ function RolesPanel({
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="flex-1"
|
||||
className="flex-1 min-h-11 rounded-xl text-base"
|
||||
disabled={updateRole.isPending || replaceRolePermissions.isPending}
|
||||
onClick={handleEdit}
|
||||
data-testid="role-perm-confirm-submit"
|
||||
@@ -7209,8 +7163,8 @@ function RolesPanel({
|
||||
)}
|
||||
|
||||
{deleteTarget && (
|
||||
<div className="fixed inset-0 bg-slate-900/30 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||||
<div className="glass-panel rounded-3xl p-6 w-full max-w-sm space-y-3" data-testid="delete-role-dialog">
|
||||
<div className="fixed inset-0 bg-slate-900/50 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-2xl sm:rounded-3xl shadow-2xl ring-1 ring-slate-200 p-5 sm:p-6 w-full max-w-[min(100%,440px)] space-y-3" data-testid="delete-role-dialog">
|
||||
<h2 className="font-semibold text-foreground">
|
||||
{t("admin.roles.deleteTitle", { name: deleteTarget.name })}
|
||||
</h2>
|
||||
@@ -7229,7 +7183,7 @@ function RolesPanel({
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
className="flex-1 min-h-11 rounded-xl text-base"
|
||||
onClick={() => {
|
||||
setDeleteTarget(null);
|
||||
setDeleteConflict(null);
|
||||
@@ -7240,7 +7194,7 @@ function RolesPanel({
|
||||
{!deleteConflict && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="flex-1"
|
||||
className="flex-1 min-h-11 rounded-xl text-base"
|
||||
disabled={deleteRole.isPending}
|
||||
onClick={handleDelete}
|
||||
data-testid="delete-role-confirm"
|
||||
@@ -7261,12 +7215,16 @@ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
||||
|
||||
function formatAuditTimestamp(value: string | Date, lang: string): string {
|
||||
const d = value instanceof Date ? value : new Date(value);
|
||||
return new Intl.DateTimeFormat(lang === "ar" ? "ar-u-nu-latn" : "en-US", {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
numberingSystem: "latn",
|
||||
hour12: false,
|
||||
}).format(d);
|
||||
if (Number.isNaN(d.getTime())) return String(value);
|
||||
const dd = String(d.getDate()).padStart(2, "0");
|
||||
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const yyyy = d.getFullYear();
|
||||
let h = d.getHours();
|
||||
const min = String(d.getMinutes()).padStart(2, "0");
|
||||
const isPm = h >= 12;
|
||||
h = h % 12 || 12;
|
||||
const suffix = lang === "ar" ? (isPm ? "م" : "ص") : isPm ? "PM" : "AM";
|
||||
return `${dd}/${mm}/${yyyy} ${h}:${min} ${suffix}`;
|
||||
}
|
||||
|
||||
function actorLabel(actor: AuditLogEntry["actor"], lang: string): string {
|
||||
|
||||
Reference in New Issue
Block a user