Build 2602261442: tidy users and invite layouts
This commit is contained in:
@@ -63,6 +63,8 @@ type ProfileForm = {
|
||||
is_active: boolean
|
||||
}
|
||||
|
||||
type InviteManagementTab = 'bulk' | 'profiles' | 'invites'
|
||||
|
||||
const defaultInviteForm = (): InviteForm => ({
|
||||
code: '',
|
||||
label: '',
|
||||
@@ -113,6 +115,7 @@ export default function AdminInviteManagementPage() {
|
||||
|
||||
const [bulkProfileId, setBulkProfileId] = useState('')
|
||||
const [bulkExpiryDays, setBulkExpiryDays] = useState('')
|
||||
const [activeTab, setActiveTab] = useState<InviteManagementTab>('bulk')
|
||||
|
||||
const signupBaseUrl = useMemo(() => {
|
||||
if (typeof window === 'undefined') return '/signup'
|
||||
@@ -461,6 +464,9 @@ export default function AdminInviteManagementPage() {
|
||||
const nonAdminUsers = users.filter((user) => user.role !== 'admin')
|
||||
const profiledUsers = nonAdminUsers.filter((user) => user.profile_id != null).length
|
||||
const expiringUsers = nonAdminUsers.filter((user) => Boolean(user.expires_at)).length
|
||||
const usableInvites = invites.filter((invite) => invite.is_usable !== false).length
|
||||
const disabledInvites = invites.filter((invite) => invite.enabled === false).length
|
||||
const activeProfiles = profiles.filter((profile) => profile.is_active !== false).length
|
||||
|
||||
return (
|
||||
<AdminShell
|
||||
@@ -471,10 +477,24 @@ export default function AdminInviteManagementPage() {
|
||||
<button type="button" onClick={loadData} disabled={loading}>
|
||||
{loading ? 'Loading…' : 'Reload'}
|
||||
</button>
|
||||
<button type="button" className="ghost-button" onClick={resetInviteEditor}>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-button"
|
||||
onClick={() => {
|
||||
resetInviteEditor()
|
||||
setActiveTab('invites')
|
||||
}}
|
||||
>
|
||||
New invite
|
||||
</button>
|
||||
<button type="button" className="ghost-button" onClick={resetProfileEditor}>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-button"
|
||||
onClick={() => {
|
||||
resetProfileEditor()
|
||||
setActiveTab('profiles')
|
||||
}}
|
||||
>
|
||||
New profile
|
||||
</button>
|
||||
</div>
|
||||
@@ -484,63 +504,165 @@ export default function AdminInviteManagementPage() {
|
||||
{error && <div className="error-banner">{error}</div>}
|
||||
{status && <div className="status-banner">{status}</div>}
|
||||
|
||||
<div className="admin-panel">
|
||||
<h2>Blanket controls</h2>
|
||||
<p className="lede">
|
||||
Apply invite profile defaults or expiry to all non-admin users. Individual users can still be edited from their user page.
|
||||
</p>
|
||||
<div className="admin-meta-row">
|
||||
<span>Non-admin users: {nonAdminUsers.length}</span>
|
||||
<span>Profile assigned: {profiledUsers}</span>
|
||||
<span>Custom expiry set: {expiringUsers}</span>
|
||||
<div className="invite-admin-summary-grid">
|
||||
<div className="admin-summary-tile invite-admin-summary-tile">
|
||||
<span className="label">Invites</span>
|
||||
<strong>{invites.length}</strong>
|
||||
<small>{usableInvites} usable • {disabledInvites} disabled</small>
|
||||
</div>
|
||||
<div className="user-bulk-groups">
|
||||
<div className="user-bulk-group">
|
||||
<label className="admin-select">
|
||||
<span>Profile</span>
|
||||
<select
|
||||
value={bulkProfileId}
|
||||
onChange={(e) => setBulkProfileId(e.target.value)}
|
||||
disabled={bulkProfileBusy}
|
||||
>
|
||||
<option value="">None / clear assignment</option>
|
||||
{profiles.map((profile) => (
|
||||
<option key={profile.id} value={profile.id}>
|
||||
{profile.name}{profile.is_active === false ? ' (disabled)' : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<button type="button" onClick={bulkApplyProfile} disabled={bulkProfileBusy}>
|
||||
{bulkProfileBusy ? 'Applying…' : 'Apply profile to all users'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="user-bulk-group">
|
||||
<label>
|
||||
<span className="user-bulk-label">Expiry days</span>
|
||||
<input
|
||||
value={bulkExpiryDays}
|
||||
onChange={(e) => setBulkExpiryDays(e.target.value)}
|
||||
inputMode="numeric"
|
||||
placeholder="e.g. 30"
|
||||
disabled={bulkExpiryBusy}
|
||||
/>
|
||||
</label>
|
||||
<button type="button" onClick={bulkSetExpiryDays} disabled={bulkExpiryBusy}>
|
||||
{bulkExpiryBusy ? 'Working…' : 'Set expiry for all users'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-button"
|
||||
onClick={bulkClearExpiry}
|
||||
disabled={bulkExpiryBusy}
|
||||
>
|
||||
{bulkExpiryBusy ? 'Working…' : 'Clear expiry for all users'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="admin-summary-tile invite-admin-summary-tile">
|
||||
<span className="label">Profiles</span>
|
||||
<strong>{profiles.length}</strong>
|
||||
<small>{activeProfiles} active profiles</small>
|
||||
</div>
|
||||
<div className="admin-summary-tile invite-admin-summary-tile">
|
||||
<span className="label">Non-admin users</span>
|
||||
<strong>{nonAdminUsers.length}</strong>
|
||||
<small>{profiledUsers} with profile</small>
|
||||
</div>
|
||||
<div className="admin-summary-tile invite-admin-summary-tile">
|
||||
<span className="label">Expiry rules</span>
|
||||
<strong>{expiringUsers}</strong>
|
||||
<small>users with custom expiry</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-segmented" role="tablist" aria-label="Invite management sections">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={activeTab === 'bulk'}
|
||||
className={activeTab === 'bulk' ? 'is-active' : ''}
|
||||
onClick={() => setActiveTab('bulk')}
|
||||
>
|
||||
Blanket controls
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={activeTab === 'profiles'}
|
||||
className={activeTab === 'profiles' ? 'is-active' : ''}
|
||||
onClick={() => setActiveTab('profiles')}
|
||||
>
|
||||
Profiles
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={activeTab === 'invites'}
|
||||
className={activeTab === 'invites' ? 'is-active' : ''}
|
||||
onClick={() => setActiveTab('invites')}
|
||||
>
|
||||
Invites
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{activeTab === 'bulk' && (
|
||||
<div className="admin-split-grid invite-admin-bulk-grid">
|
||||
<div className="admin-panel">
|
||||
<div className="user-directory-panel-header">
|
||||
<div>
|
||||
<h2>Blanket controls</h2>
|
||||
<p className="lede">
|
||||
Apply invite profile defaults or expiry to all non-admin users. Individual users can still be edited from their user page.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-meta-row">
|
||||
<span>Non-admin users: {nonAdminUsers.length}</span>
|
||||
<span>Profile assigned: {profiledUsers}</span>
|
||||
<span>Custom expiry set: {expiringUsers}</span>
|
||||
</div>
|
||||
<div className="user-bulk-groups">
|
||||
<div className="user-bulk-group">
|
||||
<label className="admin-select">
|
||||
<span>Profile</span>
|
||||
<select
|
||||
value={bulkProfileId}
|
||||
onChange={(e) => setBulkProfileId(e.target.value)}
|
||||
disabled={bulkProfileBusy}
|
||||
>
|
||||
<option value="">None / clear assignment</option>
|
||||
{profiles.map((profile) => (
|
||||
<option key={profile.id} value={profile.id}>
|
||||
{profile.name}{profile.is_active === false ? ' (disabled)' : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<button type="button" onClick={bulkApplyProfile} disabled={bulkProfileBusy}>
|
||||
{bulkProfileBusy ? 'Applying…' : 'Apply profile to all users'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="user-bulk-group">
|
||||
<label>
|
||||
<span className="user-bulk-label">Expiry days</span>
|
||||
<input
|
||||
value={bulkExpiryDays}
|
||||
onChange={(e) => setBulkExpiryDays(e.target.value)}
|
||||
inputMode="numeric"
|
||||
placeholder="e.g. 30"
|
||||
disabled={bulkExpiryBusy}
|
||||
/>
|
||||
</label>
|
||||
<button type="button" onClick={bulkSetExpiryDays} disabled={bulkExpiryBusy}>
|
||||
{bulkExpiryBusy ? 'Working…' : 'Set expiry for all users'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-button"
|
||||
onClick={bulkClearExpiry}
|
||||
disabled={bulkExpiryBusy}
|
||||
>
|
||||
{bulkExpiryBusy ? 'Working…' : 'Clear expiry for all users'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-panel">
|
||||
<div className="user-directory-panel-header">
|
||||
<div>
|
||||
<h2>How this page is organized</h2>
|
||||
<p className="lede">Use tabs to switch between blanket controls, reusable profiles, and invite links.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-list">
|
||||
<div className="admin-list-item">
|
||||
<div className="admin-list-item-main">
|
||||
<div className="admin-list-item-title-row">
|
||||
<strong>Profiles</strong>
|
||||
</div>
|
||||
<p className="admin-list-item-text">
|
||||
Create reusable account defaults and apply them to invite links or existing users.
|
||||
</p>
|
||||
</div>
|
||||
<div className="admin-inline-actions">
|
||||
<button type="button" className="ghost-button" onClick={() => setActiveTab('profiles')}>
|
||||
Open
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-list-item">
|
||||
<div className="admin-list-item-main">
|
||||
<div className="admin-list-item-title-row">
|
||||
<strong>Invites</strong>
|
||||
</div>
|
||||
<p className="admin-list-item-text">
|
||||
Create and manage signup links, assign profiles, and copy shareable URLs.
|
||||
</p>
|
||||
</div>
|
||||
<div className="admin-inline-actions">
|
||||
<button type="button" className="ghost-button" onClick={() => setActiveTab('invites')}>
|
||||
Open
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'profiles' && (
|
||||
<div className="admin-split-grid">
|
||||
<div className="admin-panel">
|
||||
<h2>{profileEditingId == null ? 'Create profile' : 'Edit profile'}</h2>
|
||||
@@ -684,7 +806,9 @@ export default function AdminInviteManagementPage() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'invites' && (
|
||||
<div className="admin-split-grid">
|
||||
<div className="admin-panel">
|
||||
<h2>{inviteEditingId == null ? 'Create invite' : 'Edit invite'}</h2>
|
||||
@@ -856,8 +980,8 @@ export default function AdminInviteManagementPage() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</AdminShell>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user