Updated Dashboard and clarified default states.

This commit is contained in:
Pen Anderson 2026-03-04 20:52:12 -06:00
parent 260e017f79
commit e7b25ea0c6
5 changed files with 172 additions and 35 deletions

View file

@ -4,13 +4,54 @@
let { session } = $props()
const attendees = liveQuery(() => db.attendees.toArray())
const event = liveQuery(() => db.event.get(1))
const role = $derived(session?.user?.role ?? '')
const myDeptIDs = $derived(session?.user?.department_ids ?? [])
const isTicketing = $derived(['admin', 'ticketing'].includes(role))
const isStaffing = $derived(['admin', 'ticketing', 'staffing'].includes(role))
const isColead = $derived(role === 'colead')
const total = $derived(($attendees ?? []).length)
const checkedIn = $derived(($attendees ?? []).filter(a => a.checked_in).length)
const remaining = $derived(total - checkedIn)
const pct = $derived(total > 0 ? Math.round((checkedIn / total) * 100) : 0)
const event = liveQuery(() => db.event.get(1))
const allTickets = liveQuery(() => db.tickets.toArray())
const allVolunteers = liveQuery(() => db.volunteers.filter(v => !v.deleted_at).toArray())
const allShifts = liveQuery(() => db.shifts.filter(s => !s.deleted_at).toArray())
const allDepts = liveQuery(() => db.departments.filter(d => !d.deleted_at).toArray())
const allVS = liveQuery(() => db.volunteer_shifts.toArray())
// Ticket stats
const tickets = $derived($allTickets ?? [])
const ticketTotal = $derived(tickets.length)
const ticketCheckedIn = $derived(tickets.filter(t => t.checked_in_at).length)
const ticketRemaining = $derived(ticketTotal - ticketCheckedIn)
const ticketPct = $derived(ticketTotal > 0 ? Math.round((ticketCheckedIn / ticketTotal) * 100) : 0)
// Volunteer stats (scoped for colead)
const volunteers = $derived.by(() => {
const vols = $allVolunteers ?? []
if (isColead) return vols.filter(v => myDeptIDs.includes(v.department_id))
return vols
})
const volTotal = $derived(volunteers.length)
const volCheckedIn = $derived(volunteers.filter(v => v.checked_in).length)
const volLeads = $derived(volunteers.filter(v => v.is_lead).length)
// Shift stats (scoped for colead)
const shifts = $derived.by(() => {
const all = $allShifts ?? []
if (isColead) return all.filter(s => myDeptIDs.includes(s.department_id))
return all
})
const shiftTotal = $derived(shifts.length)
const shiftsFilled = $derived.by(() => {
const vs = $allVS ?? []
return shifts.filter(s => vs.some(a => a.shift_id === s.id)).length
})
const shiftFillPct = $derived(shiftTotal > 0 ? Math.round((shiftsFilled / shiftTotal) * 100) : 0)
// Department names for colead header
const myDeptNames = $derived.by(() => {
const depts = $allDepts ?? []
return myDeptIDs.map(id => depts.find(d => d.id === id)?.name).filter(Boolean)
})
</script>
<div class="page">
@ -28,35 +69,113 @@
</p>
{/if}
{#if isColead && myDeptNames.length > 0}
<p style="margin-bottom:1.5rem;font-size:0.9rem">
Your department{myDeptNames.length > 1 ? 's' : ''}:
<strong>{myDeptNames.join(', ')}</strong>
</p>
{/if}
<!-- Ticket check-in (admin/ticketing) -->
{#if isTicketing}
<h2 class="dash-section">Ticket Check-in</h2>
<div class="stats">
<div class="stat">
<div class="stat-label">Total</div>
<div class="stat-value">{total}</div>
<div class="stat-label">Total tickets</div>
<div class="stat-value">{ticketTotal}</div>
</div>
<div class="stat">
<div class="stat-label">Checked in</div>
<div class="stat-value" style="color:var(--c-success)">{checkedIn}</div>
<div class="stat-value" style="color:var(--c-success)">{ticketCheckedIn}</div>
</div>
<div class="stat">
<div class="stat-label">Remaining</div>
<div class="stat-value">{remaining}</div>
<div class="stat-value">{ticketRemaining}</div>
</div>
<div class="stat">
<div class="stat-label">Progress</div>
<div class="stat-value">{pct}%</div>
<div class="stat-value">{ticketPct}%</div>
</div>
</div>
{#if total > 0}
<div class="card" style="margin-bottom:1rem">
<div style="height:8px;background:var(--c-border);border-radius:99px;overflow:hidden">
<div style="height:100%;width:{pct}%;background:var(--c-success);border-radius:99px;transition:width 0.4s ease"></div>
{#if ticketTotal > 0}
<div style="height:8px;background:var(--c-border);border-radius:99px;overflow:hidden;margin-bottom:2rem">
<div style="height:100%;width:{ticketPct}%;background:var(--c-success);border-radius:99px;transition:width 0.4s ease"></div>
</div>
{/if}
{/if}
<!-- Volunteer stats (admin/ticketing/staffing/colead) -->
{#if isStaffing || isColead}
<h2 class="dash-section">{isColead ? 'My Volunteers' : 'Volunteers'}</h2>
<div class="stats">
<div class="stat">
<div class="stat-label">Total</div>
<div class="stat-value">{volTotal}</div>
</div>
<div class="stat">
<div class="stat-label">Checked in</div>
<div class="stat-value" style="color:var(--c-success)">{volCheckedIn}</div>
</div>
<div class="stat">
<div class="stat-label">Leads</div>
<div class="stat-value">{volLeads}</div>
</div>
</div>
{/if}
<p class="text-muted" style="font-size:0.85rem">
<!-- Shift coverage (admin/ticketing/staffing/colead) -->
{#if isStaffing || isColead}
<h2 class="dash-section">{isColead ? 'My Shifts' : 'Shift Coverage'}</h2>
<div class="stats">
<div class="stat">
<div class="stat-label">Total shifts</div>
<div class="stat-value">{shiftTotal}</div>
</div>
<div class="stat">
<div class="stat-label">With volunteers</div>
<div class="stat-value">{shiftsFilled}</div>
</div>
<div class="stat">
<div class="stat-label">Fill rate</div>
<div class="stat-value">{shiftFillPct}%</div>
</div>
</div>
{/if}
<!-- Quick actions -->
{#if isTicketing}
<div class="dash-actions">
<a href="/import" class="btn btn-ghost btn-sm">Import CSV</a>
<a href="/participants" class="btn btn-ghost btn-sm">Manage Participants</a>
<a href="/settings" class="btn btn-ghost btn-sm">Settings</a>
</div>
{:else if isStaffing || isColead}
<div class="dash-actions">
<a href="/schedule" class="btn btn-ghost btn-sm">View Schedule</a>
<a href="/volunteers" class="btn btn-ghost btn-sm">Manage Volunteers</a>
</div>
{/if}
<p class="text-muted" style="font-size:0.85rem;margin-top:2rem">
Welcome, <strong style="color:var(--c-text)">{session?.user?.username}</strong>
· <span class="badge badge-role">{session?.user?.role}</span>
</p>
</div>
<style>
.dash-section {
font-size: 0.82rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--c-muted);
margin-bottom: 0.75rem;
}
.dash-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-top: 0.5rem;
}
</style>

View file

@ -127,7 +127,7 @@
{#if ($allDepts ?? []).length === 0}
<div class="empty">
<strong>No departments yet</strong>
<p>Add departments to organize your volunteer teams.</p>
<p>Create departments to organize shifts and volunteer teams. Coleads are assigned to specific departments.</p>
</div>
{:else}
<div class="table-wrap">

View file

@ -315,8 +315,8 @@
{#if ($allShifts ?? []).length === 0 && !showAdd}
<div class="empty">
<strong>No shifts yet</strong>
<p>Add shifts to schedule your volunteers.</p>
<strong>No shifts scheduled yet</strong>
<p>Create departments first, then add shifts here. Volunteers can self-select shifts via the kiosk.</p>
</div>
{:else}
{#each board as { dept, days }}

View file

@ -117,7 +117,7 @@
}
function roleLabel(r) {
return { admin: 'Admin', coordinator: 'Coordinator', ticketing: 'Ticketing', gate: 'Gate', volunteer_lead: 'Vol. Lead' }[r] || r
return { admin: 'Admin', ticketing: 'Ticketing', staffing: 'Staffing', colead: 'Colead', gatekeeper: 'Gatekeeper' }[r] || r
}
</script>
@ -129,6 +129,15 @@
</div>
</div>
<p class="text-muted" style="font-size:0.82rem;margin-bottom:1.5rem;line-height:1.6">
<strong style="color:var(--c-text)">Roles:</strong>
admin — full access ·
ticketing — participants, tickets, import ·
staffing — volunteers, shifts, departments ·
colead — manage assigned departments only ·
gatekeeper — check-in only
</p>
{#if loadError}
<div class="alert alert-error">{loadError}</div>
{/if}
@ -187,7 +196,8 @@
<div class="text-muted" style="padding:2rem 0">Loading…</div>
{:else if users.length === 0}
<div class="empty">
<strong>No users yet</strong>
<strong>No additional users</strong>
<p>The admin account was created at setup. Add users above to delegate access.</p>
</div>
{:else}
<div class="table-wrap">

View file

@ -20,6 +20,14 @@
const role = $derived(session?.user?.role ?? '')
const canManage = $derived(['admin', 'ticketing', 'staffing', 'colead'].includes(role))
const myDeptIDs = $derived(session?.user?.department_ids ?? [])
// Auto-filter coleads to their department on mount
$effect(() => {
if (role === 'colead' && myDeptIDs.length > 0 && !filterDept) {
filterDept = String(myDeptIDs[0])
}
})
const allVolunteers = liveQuery(() =>
db.volunteers.filter(v => !v.deleted_at).toArray()
@ -177,7 +185,7 @@
{#if ($allVolunteers ?? []).length === 0}
<div class="empty">
<strong>No volunteers yet</strong>
<p>Add volunteers manually.</p>
<p>Add volunteers manually above, or enable public signup in Settings.</p>
</div>
{:else}
<div class="table-wrap">