Files
pantry-management-frontend/src/pages/AdminPage/AdminPage.jsx

603 lines
21 KiB
JavaScript

import { useEffect, useState } from 'react'
import { householdsApi } from '../../api/client.js'
import { useAuth } from '../../context/AuthContext.jsx'
import { formatDate } from '../../utils/searchUtils.js'
import './AdminPage.css'
const EMPTY_HOUSEHOLD_FORM = {
name: '',
description: '',
}
const EMPTY_USER_FORM = {
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: '',
}
function formatPersonName(person) {
const fullName = [person.firstName, person.lastName]
.filter(Boolean)
.join(' ')
return fullName || person.email || 'Unnamed person'
}
function AdminPage() {
const { isAuthenticated, isSiteAdmin, register } = useAuth()
const [loading, setLoading] = useState(false)
const [errorMessage, setErrorMessage] = useState('')
const [statusMessage, setStatusMessage] = useState('')
const [households, setHouseholds] = useState([])
const [selectedHouseholdId, setSelectedHouseholdId] = useState('')
const [editingHouseholdId, setEditingHouseholdId] = useState('')
const [householdForm, setHouseholdForm] = useState(EMPTY_HOUSEHOLD_FORM)
const [inviteEmail, setInviteEmail] = useState('')
const [userForm, setUserForm] = useState(EMPTY_USER_FORM)
const [createdUser, setCreatedUser] = useState(null)
async function loadHouseholds(preferredSelectionId = '') {
const response = await householdsApi.getHouseholds()
const nextHouseholds = Array.isArray(response) ? response : []
setHouseholds(nextHouseholds)
setSelectedHouseholdId(currentSelectionId => {
const targetSelectionId = preferredSelectionId || currentSelectionId
if (nextHouseholds.some(household => household.id === targetSelectionId)) {
return targetSelectionId
}
return nextHouseholds[0]?.id ?? ''
})
setEditingHouseholdId(currentEditingId => (
nextHouseholds.some(household => household.id === currentEditingId)
? currentEditingId
: ''
))
}
useEffect(() => {
let cancelled = false
async function loadInitialHouseholds() {
if (!isAuthenticated) {
setHouseholds([])
setSelectedHouseholdId('')
setEditingHouseholdId('')
setHouseholdForm(EMPTY_HOUSEHOLD_FORM)
setInviteEmail('')
setUserForm(EMPTY_USER_FORM)
setCreatedUser(null)
setErrorMessage('')
setStatusMessage('')
return
}
setLoading(true)
setErrorMessage('')
try {
const response = await householdsApi.getHouseholds()
if (cancelled) return
const nextHouseholds = Array.isArray(response) ? response : []
setHouseholds(nextHouseholds)
setSelectedHouseholdId(nextHouseholds[0]?.id ?? '')
} catch (error) {
if (!cancelled) {
setErrorMessage(error.message)
}
} finally {
if (!cancelled) {
setLoading(false)
}
}
}
loadInitialHouseholds()
return () => {
cancelled = true
}
}, [isAuthenticated])
const selectedHousehold = households.find(household => household.id === selectedHouseholdId) ?? null
const editingHousehold = households.find(household => household.id === editingHouseholdId) ?? null
const canManageSelectedHousehold = Boolean(selectedHousehold && (isSiteAdmin || selectedHousehold.isCurrentUserHouseholdAdmin))
const canSubmitHouseholdForm = editingHouseholdId
? Boolean(editingHousehold && (isSiteAdmin || editingHousehold.isCurrentUserHouseholdAdmin))
: isSiteAdmin
const createdUserRoles = Array.isArray(createdUser?.roles) ? createdUser.roles : []
function resetHouseholdEditor() {
setEditingHouseholdId('')
setHouseholdForm(EMPTY_HOUSEHOLD_FORM)
}
function resetUserForm() {
setUserForm(EMPTY_USER_FORM)
}
async function handleHouseholdSubmit(event) {
event.preventDefault()
const name = householdForm.name.trim()
if (!name) {
setErrorMessage('Household name is required.')
return
}
if (!canSubmitHouseholdForm) {
setErrorMessage(editingHouseholdId
? 'You can only edit households you administer.'
: 'Only site admins can create households.')
return
}
setLoading(true)
setErrorMessage('')
setStatusMessage('')
try {
const payload = {
name,
description: householdForm.description.trim() || null,
}
const result = editingHouseholdId
? await householdsApi.updateHousehold(editingHouseholdId, payload)
: await householdsApi.createHousehold(payload)
await loadHouseholds(result?.id ?? editingHouseholdId)
resetHouseholdEditor()
setStatusMessage(editingHouseholdId ? 'Household updated.' : 'Household created.')
} catch (error) {
setErrorMessage(error.message)
} finally {
setLoading(false)
}
}
async function handleInviteSubmit(event) {
event.preventDefault()
const email = inviteEmail.trim()
if (!selectedHouseholdId) {
setErrorMessage('Select a household before sending an invite.')
return
}
if (!email) {
setErrorMessage('Invite email is required.')
return
}
if (!canManageSelectedHousehold) {
setErrorMessage('You can only invite users to households you administer.')
return
}
setLoading(true)
setErrorMessage('')
setStatusMessage('')
try {
await householdsApi.inviteHouseholdMember(selectedHouseholdId, { email })
await loadHouseholds(selectedHouseholdId)
setInviteEmail('')
setStatusMessage('Invitation sent.')
} catch (error) {
setErrorMessage(error.message)
} finally {
setLoading(false)
}
}
async function handleUserSubmit(event) {
event.preventDefault()
const email = userForm.email.trim()
if (!isSiteAdmin) {
setErrorMessage('Only site admins can create users from the admin page.')
return
}
if (!email) {
setErrorMessage('User email is required.')
return
}
if (!userForm.password) {
setErrorMessage('A password is required to create a user.')
return
}
if (userForm.password !== userForm.confirmPassword) {
setErrorMessage('Passwords do not match.')
return
}
setLoading(true)
setErrorMessage('')
setStatusMessage('')
try {
const result = await register({
email,
password: userForm.password,
confirmPassword: userForm.confirmPassword,
firstName: userForm.firstName.trim() || null,
lastName: userForm.lastName.trim() || null,
})
setCreatedUser(result?.user ?? null)
resetUserForm()
setStatusMessage(result?.message || 'User created.')
} catch (error) {
setErrorMessage(error.message)
} finally {
setLoading(false)
}
}
async function handleLeaveHousehold(householdId) {
const confirmed = window.confirm('Leave this household?')
if (!confirmed) return
setLoading(true)
setErrorMessage('')
setStatusMessage('')
try {
await householdsApi.leaveHousehold(householdId)
await loadHouseholds(selectedHouseholdId === householdId ? '' : selectedHouseholdId)
if (selectedHouseholdId === householdId) {
setInviteEmail('')
resetHouseholdEditor()
}
setStatusMessage('Household left.')
} catch (error) {
setErrorMessage(error.message)
} finally {
setLoading(false)
}
}
return (
<>
<h2>Admin</h2>
<hr />
{!isAuthenticated ? (
<div className="auth-required">
Sign in on the dashboard before using the household administration endpoints.
</div>
) : (
<>
{errorMessage && <div className="status-banner status-banner--error">{errorMessage}</div>}
{statusMessage && <div className="status-banner status-banner--success">{statusMessage}</div>}
{loading && <div className="status-banner status-banner--info">Processing admin request...</div>}
<section className="panel admin-summary-panel">
<h3>Household Summary</h3>
<div className="stats-grid admin-stats-grid">
<div className="stat-card">
<span className="stat-label">Households</span>
<strong>{households.length}</strong>
</div>
<div className="stat-card">
<span className="stat-label">Managed Households</span>
<strong>{households.filter(household => household.isCurrentUserHouseholdAdmin || isSiteAdmin).length}</strong>
</div>
<div className="stat-card">
<span className="stat-label">Members</span>
<strong>{households.reduce((count, household) => count + (household.members?.length ?? 0), 0)}</strong>
</div>
</div>
</section>
<div className="management-grid admin-grid">
<section className="panel">
<div className="section-heading">
<h3>{editingHouseholdId ? 'Edit Household' : 'Create Household'}</h3>
{editingHouseholdId && (
<button type="button" className="btn btn-secondary" onClick={resetHouseholdEditor}>
New household
</button>
)}
</div>
{!isSiteAdmin && !editingHouseholdId && (
<p className="form-note">
Only site admins can create households. Household admins can still edit an existing household from the list.
</p>
)}
<form className="editor-form" onSubmit={handleHouseholdSubmit}>
<div className="field-group">
<label htmlFor="household-name">Name</label>
<input
id="household-name"
type="text"
value={householdForm.name}
onChange={event => setHouseholdForm(current => ({ ...current, name: event.target.value }))}
placeholder="Main Household"
required
/>
</div>
<div className="field-group">
<label htmlFor="household-description">Description</label>
<textarea
id="household-description"
rows="3"
value={householdForm.description}
onChange={event => setHouseholdForm(current => ({ ...current, description: event.target.value }))}
placeholder="Shared pantry and fridge access"
/>
</div>
<div className="button-row">
<button type="submit" className="btn btn-primary" disabled={!canSubmitHouseholdForm}>
{editingHouseholdId ? 'Update household' : 'Create household'}
</button>
<button type="button" className="btn btn-secondary" onClick={resetHouseholdEditor}>
Clear
</button>
</div>
</form>
</section>
<section className="panel">
<div className="section-heading">
<h3>Invite Member</h3>
<span className="subtle-text">{selectedHousehold?.name || 'No household selected'}</span>
</div>
{!selectedHousehold ? (
<div className="empty-state compact-empty-state">Select a household to invite a member.</div>
) : (
<>
{!canManageSelectedHousehold && (
<p className="form-note">
You can only invite users to households you administer.
</p>
)}
<form className="editor-form" onSubmit={handleInviteSubmit}>
<div className="field-group">
<label htmlFor="invite-email">Email</label>
<input
id="invite-email"
type="email"
value={inviteEmail}
onChange={event => setInviteEmail(event.target.value)}
placeholder="member@example.com"
disabled={!canManageSelectedHousehold}
required
/>
</div>
<div className="button-row">
<button type="submit" className="btn btn-primary" disabled={!canManageSelectedHousehold}>
Send invite
</button>
<button type="button" className="btn btn-secondary" onClick={() => setInviteEmail('')}>
Clear
</button>
</div>
</form>
</>
)}
</section>
<section className="panel">
<div className="section-heading">
<h3>Create User</h3>
<span className="subtle-text">{createdUser?.email || 'Register endpoint'}</span>
</div>
<p className="form-note">
{isSiteAdmin
? 'This creates a new account with the register endpoint. Role assignment still happens separately in the backend.'
: 'Only site admins can create users from this page.'}
</p>
<form className="editor-form" onSubmit={handleUserSubmit}>
<div className="form-grid">
<div className="field-group">
<label htmlFor="register-first-name">First Name</label>
<input
id="register-first-name"
type="text"
value={userForm.firstName}
onChange={event => setUserForm(current => ({ ...current, firstName: event.target.value }))}
placeholder="Jordan"
disabled={!isSiteAdmin}
/>
</div>
<div className="field-group">
<label htmlFor="register-last-name">Last Name</label>
<input
id="register-last-name"
type="text"
value={userForm.lastName}
onChange={event => setUserForm(current => ({ ...current, lastName: event.target.value }))}
placeholder="Lee"
disabled={!isSiteAdmin}
/>
</div>
</div>
<div className="field-group">
<label htmlFor="register-email">Email</label>
<input
id="register-email"
type="email"
value={userForm.email}
onChange={event => setUserForm(current => ({ ...current, email: event.target.value }))}
placeholder="new-user@example.com"
disabled={!isSiteAdmin}
required
/>
</div>
<div className="form-grid">
<div className="field-group">
<label htmlFor="register-password">Password</label>
<input
id="register-password"
type="password"
value={userForm.password}
onChange={event => setUserForm(current => ({ ...current, password: event.target.value }))}
placeholder="Create a password"
disabled={!isSiteAdmin}
required
/>
</div>
<div className="field-group">
<label htmlFor="register-confirm-password">Confirm Password</label>
<input
id="register-confirm-password"
type="password"
value={userForm.confirmPassword}
onChange={event => setUserForm(current => ({ ...current, confirmPassword: event.target.value }))}
placeholder="Repeat the password"
disabled={!isSiteAdmin}
required
/>
</div>
</div>
<div className="button-row">
<button type="submit" className="btn btn-primary" disabled={!isSiteAdmin}>
Create user
</button>
<button type="button" className="btn btn-secondary" onClick={resetUserForm} disabled={!isSiteAdmin}>
Clear
</button>
</div>
</form>
{createdUser && (
<div className="admin-created-user-card">
<div className="admin-created-user-copy">
<strong>{formatPersonName(createdUser)}</strong>
<div className="entity-meta">{createdUser.email || 'No email provided.'}</div>
<div className="entity-meta">ID: {createdUser.id || 'Not available'}</div>
</div>
<div className="admin-badge-row">
{createdUserRoles.length === 0 ? (
<span className="member-badge">No roles assigned</span>
) : (
createdUserRoles.map(role => (
<span className="member-badge" key={role}>{role}</span>
))
)}
</div>
</div>
)}
</section>
</div>
<section className="panel admin-list-panel">
<div className="section-heading">
<h3>Households</h3>
<span className="subtle-text">{households.length} total</span>
</div>
{households.length === 0 ? (
<div className="empty-state compact-empty-state">No households were returned for this account yet.</div>
) : (
<div className="entity-list">
{households.map(household => {
const canManageHousehold = isSiteAdmin || household.isCurrentUserHouseholdAdmin
return (
<article
className={`entity-row admin-household-row ${selectedHouseholdId === household.id ? 'is-selected' : ''}`}
key={household.id}
>
<div className="entity-copy admin-household-copy">
<div className="admin-household-header">
<strong>{household.name}</strong>
<div className="admin-badge-row">
{household.isCurrentUserHouseholdAdmin && <span className="admin-badge">Household admin</span>}
{household.adminEmail && <span className="member-badge">Owner: {household.adminEmail}</span>}
</div>
</div>
<div className="entity-meta">{household.description || 'No description provided.'}</div>
<div className="entity-meta">Created: {formatDate(household.createdAt) || 'Not available'}</div>
<div className="member-list">
{(household.members ?? []).map(member => (
<div className="member-row" key={member.userId}>
<div className="member-copy">
<strong>{formatPersonName(member)}</strong>
<span className="entity-meta">{member.email}</span>
</div>
{member.isHouseholdAdmin && <span className="member-badge">Admin</span>}
</div>
))}
</div>
</div>
<div className="entity-actions">
<button
type="button"
className="btn btn-secondary"
onClick={() => setSelectedHouseholdId(household.id)}
>
Select
</button>
{canManageHousehold && (
<button
type="button"
className="btn btn-secondary"
onClick={() => {
setSelectedHouseholdId(household.id)
setEditingHouseholdId(household.id)
setHouseholdForm({
name: household.name ?? '',
description: household.description ?? '',
})
}}
>
Edit
</button>
)}
<button
type="button"
className="btn btn-danger"
onClick={() => handleLeaveHousehold(household.id)}
>
Leave
</button>
</div>
</article>
)
})}
</div>
)}
</section>
</>
)}
</>
)
}
export default AdminPage