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:
Riyadh
2026-05-19 14:45:16 +00:00
parent 028fe9469e
commit 4944d45c75
+36 -78
View File
@@ -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 {