Improve user settings layout and forms on mobile
This commit is contained in:
parent
32d4a82cdf
commit
8f0407c49c
1 changed files with 36 additions and 16 deletions
|
|
@ -33,15 +33,35 @@ export function UsersView({ users, sites, isAdmin }: { users: ManagedUser[]; sit
|
|||
function edit(user: ManagedUser) { setSelected(user); setFormKey((key) => key + 1); setOpen(true); }
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-5xl space-y-5 p-4 sm:p-6">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="mx-auto min-w-0 max-w-5xl space-y-5 p-4 sm:p-6">
|
||||
<div className="flex flex-col items-stretch justify-between gap-3 sm:flex-row sm:items-center">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">Users</h2>
|
||||
<p className="text-sm text-muted-foreground">{isAdmin ? "Admins manage all sites and users. Clock users start in the clock app." : "Add clock users for your assigned sites. They can scan staff, vehicles and delivery notes in the clock app."}</p>
|
||||
</div>
|
||||
<Button onClick={add}><Plus className="mr-2 h-4 w-4" /> Add user</Button>
|
||||
<Button onClick={add} className="min-h-11 w-full sm:min-h-9 sm:w-auto sm:shrink-0"><Plus className="mr-2 h-4 w-4" /> Add user</Button>
|
||||
</div>
|
||||
<div className="overflow-x-auto rounded-xl border border-border bg-card">
|
||||
<div className="space-y-3 sm:hidden">
|
||||
{users.map((user) => (
|
||||
<article key={user.id} className="min-w-0 rounded-xl border border-border bg-card p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 space-y-1">
|
||||
<h3 className="break-words font-semibold">{user.name || user.email}</h3>
|
||||
<p className="break-all text-sm text-muted-foreground">{user.email}</p>
|
||||
<p className="text-sm text-muted-foreground">{ROLE_LABELS[user.role]}</p>
|
||||
</div>
|
||||
{isAdmin && <Button variant="outline" className="min-h-11 min-w-11 shrink-0 px-3" onClick={() => edit(user)} aria-label={`Edit ${user.name || user.email}`}><Pencil className="h-4 w-4" /><span className="sr-only">Edit</span></Button>}
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2"><Badge variant={user.is_active ? "default" : "outline"}>{user.is_active ? "Active" : "Inactive"}</Badge>{user.must_change_password && <Badge variant="outline">Password change</Badge>}</div>
|
||||
<div className="mt-3 border-t border-border pt-3 text-sm">
|
||||
<p className="mb-1 text-xs font-medium text-muted-foreground">Site access</p>
|
||||
<p className="break-words">{user.role === "admin" ? "All sites" : user.sites.map((id) => sites.find((site) => site.id === id)?.name ?? id).join(", ") || "No sites"}</p>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
{users.length === 0 && <p className="rounded-xl border border-dashed border-border p-6 text-center text-sm text-muted-foreground">No users yet. Add a user to get started.</p>}
|
||||
<div className="hidden overflow-x-auto rounded-xl border border-border bg-card sm:block">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead className="border-b border-border bg-muted/50 text-muted-foreground">
|
||||
<tr><th className="p-3">Name</th><th className="p-3">Role</th><th className="p-3">Sites</th><th className="p-3">Status</th><th className="p-3"><span className="sr-only">Edit</span></th></tr>
|
||||
|
|
@ -101,29 +121,29 @@ function UserDialog({ open, onOpenChange, user, sites, isAdmin }: {
|
|||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(next) => { if (!busy) onOpenChange(next); }}>
|
||||
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader><DialogTitle>{user ? "Edit user" : "Add user"}</DialogTitle></DialogHeader>
|
||||
<form onSubmit={save} className="space-y-4">
|
||||
<label className="block text-sm font-medium">Name<input required value={name} onChange={(e) => setName(e.target.value)} className="mt-1 w-full rounded-md border border-input bg-background px-3 py-2" /></label>
|
||||
<label className="block text-sm font-medium">Email<input required type="email" disabled={!!user} value={email} onChange={(e) => setEmail(e.target.value)} className="mt-1 w-full rounded-md border border-input bg-background px-3 py-2 disabled:opacity-60" /></label>
|
||||
<DialogContent className="w-[calc(100%-2rem)] max-h-[calc(100dvh-2rem)] min-w-0 overflow-y-auto overscroll-contain p-5 sm:max-h-[90dvh] sm:max-w-lg sm:p-6 [&>button]:right-2 [&>button]:top-2 [&>button]:flex [&>button]:h-11 [&>button]:w-11 [&>button]:items-center [&>button]:justify-center">
|
||||
<DialogHeader className="pr-9"><DialogTitle>{user ? "Edit user" : "Add user"}</DialogTitle></DialogHeader>
|
||||
<form onSubmit={save} className="min-w-0 space-y-4">
|
||||
<label className="block text-sm font-medium">Name<input required value={name} onChange={(e) => setName(e.target.value)} className="mt-1 min-h-11 w-full rounded-md border border-input bg-background px-3 py-2 text-base sm:min-h-10 sm:text-sm" /></label>
|
||||
<label className="block text-sm font-medium">Email<input required type="email" disabled={!!user} value={email} onChange={(e) => setEmail(e.target.value)} className="mt-1 min-h-11 w-full rounded-md border border-input bg-background px-3 py-2 text-base sm:min-h-10 sm:text-sm disabled:opacity-60" /></label>
|
||||
<label className="block text-sm font-medium">Role
|
||||
<select value={role} onChange={(e) => setRole(e.target.value as Role)} className="mt-1 w-full rounded-md border border-input bg-background px-3 py-2">
|
||||
<select value={role} onChange={(e) => setRole(e.target.value as Role)} className="mt-1 min-h-11 w-full rounded-md border border-input bg-background px-3 py-2 text-base sm:min-h-10 sm:text-sm">
|
||||
{isAdmin && <option value="site_user">Site manager</option>}<option value="clock_only">Clock user</option>{isAdmin && <option value="admin">Admin</option>}
|
||||
</select>
|
||||
</label>
|
||||
{role !== "admin" && <fieldset className="rounded-lg border border-border p-3"><legend className="px-1 text-sm font-medium">Allowed sites</legend>
|
||||
<div className="mt-2 space-y-2">{sites.map((site) => <label key={site.id} className="flex items-center gap-2 text-sm"><input type="checkbox" checked={chosenSites.includes(site.id)} onChange={() => toggleSite(site.id)} />{site.name}</label>)}</div>
|
||||
{role !== "admin" && <fieldset className="min-w-0 rounded-lg border border-border p-3"><legend className="px-1 text-sm font-medium">Allowed sites</legend>
|
||||
<div className="mt-2 space-y-2">{sites.map((site) => <label key={site.id} className="flex min-h-11 items-center gap-3 py-1 text-sm"><input className="h-5 w-5 shrink-0 accent-primary" type="checkbox" checked={chosenSites.includes(site.id)} onChange={() => toggleSite(site.id)} />{site.name}</label>)}</div>
|
||||
</fieldset>}
|
||||
<label className="block text-sm font-medium">{user ? "Set a new password (optional)" : "Initial password"}
|
||||
<input type="text" minLength={8} maxLength={128} required={!user} value={password} onChange={(e) => setPassword(e.target.value)} autoComplete="off" className="mt-1 w-full rounded-md border border-input bg-background px-3 py-2" />
|
||||
<input type="text" minLength={8} maxLength={128} required={!user} value={password} onChange={(e) => setPassword(e.target.value)} autoComplete="off" className="mt-1 min-h-11 w-full rounded-md border border-input bg-background px-3 py-2 text-base sm:min-h-10 sm:text-sm" />
|
||||
</label>
|
||||
{user && <div className="space-y-2 rounded-lg border border-border p-3 text-sm">
|
||||
<label className="flex items-center gap-2"><input type="checkbox" checked={active} onChange={(e) => setActive(e.target.checked)} />Active account</label>
|
||||
<label className="flex items-center gap-2"><input type="checkbox" checked={changePassword} onChange={(e) => setChangePassword(e.target.checked)} />Require password change on next sign-in</label>
|
||||
<label className="flex min-h-11 items-center gap-3 py-1"><input className="h-5 w-5 shrink-0 accent-primary" type="checkbox" checked={active} onChange={(e) => setActive(e.target.checked)} />Active account</label>
|
||||
<label className="flex min-h-11 items-center gap-3 py-1"><input className="h-5 w-5 shrink-0 accent-primary" type="checkbox" checked={changePassword} onChange={(e) => setChangePassword(e.target.checked)} />Require password change on next sign-in</label>
|
||||
</div>}
|
||||
{!user && <p className="text-xs text-muted-foreground">The user will choose a new password at first sign-in. You can change the initial password above.</p>}
|
||||
{error && <p role="alert" className="text-sm text-destructive">{error}</p>}
|
||||
<Button type="submit" className="w-full" disabled={busy || (role !== "admin" && chosenSites.length === 0)}>{busy ? "Saving…" : user ? "Save changes" : "Add user"}</Button>
|
||||
<div className="sticky -bottom-5 -mx-5 -mb-5 border-t border-border bg-card px-5 pt-4 pb-[max(1.25rem,env(safe-area-inset-bottom))] sm:static sm:m-0 sm:border-0 sm:p-0"><Button type="submit" className="min-h-11 w-full sm:min-h-9" disabled={busy || (role !== "admin" && chosenSites.length === 0)}>{busy ? "Saving…" : user ? "Save changes" : "Add user"}</Button></div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
|
|
|||
Loading…
Reference in a new issue