Refactored Barcode Page and made fixes to logic
This commit is contained in:
427
src/pages/AdminPage/AdminPage.jsx
Normal file
427
src/pages/AdminPage/AdminPage.jsx
Normal file
@@ -0,0 +1,427 @@
|
||||
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: '',
|
||||
}
|
||||
|
||||
function formatMemberName(member) {
|
||||
const fullName = [member.firstName, member.lastName]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
return fullName || member.email || 'Unnamed member'
|
||||
}
|
||||
|
||||
function AdminPage() {
|
||||
const { isAuthenticated, isSiteAdmin } = 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('')
|
||||
|
||||
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('')
|
||||
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
|
||||
|
||||
function resetHouseholdEditor() {
|
||||
setEditingHouseholdId('')
|
||||
setHouseholdForm(EMPTY_HOUSEHOLD_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 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">Syncing household data...</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>
|
||||
</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>{formatMemberName(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
|
||||
Reference in New Issue
Block a user