Updated Dashboard and clarified default states.
This commit is contained in:
parent
260e017f79
commit
2ba4d4407d
5 changed files with 174 additions and 35 deletions
|
|
@ -4,13 +4,54 @@
|
||||||
|
|
||||||
let { session } = $props()
|
let { session } = $props()
|
||||||
|
|
||||||
const attendees = liveQuery(() => db.attendees.toArray())
|
const role = $derived(session?.user?.role ?? '')
|
||||||
const event = liveQuery(() => db.event.get(1))
|
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 event = liveQuery(() => db.event.get(1))
|
||||||
const checkedIn = $derived(($attendees ?? []).filter(a => a.checked_in).length)
|
const allTickets = liveQuery(() => db.tickets.toArray())
|
||||||
const remaining = $derived(total - checkedIn)
|
const allVolunteers = liveQuery(() => db.volunteers.filter(v => !v.deleted_at).toArray())
|
||||||
const pct = $derived(total > 0 ? Math.round((checkedIn / total) * 100) : 0)
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="page">
|
<div class="page">
|
||||||
|
|
@ -28,35 +69,115 @@
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="stats">
|
{#if isColead && myDeptNames.length > 0}
|
||||||
<div class="stat">
|
<p style="margin-bottom:1.5rem;font-size:0.9rem">
|
||||||
<div class="stat-label">Total</div>
|
Your department{myDeptNames.length > 1 ? 's' : ''}:
|
||||||
<div class="stat-value">{total}</div>
|
<strong>{myDeptNames.join(', ')}</strong>
|
||||||
</div>
|
</p>
|
||||||
<div class="stat">
|
{/if}
|
||||||
<div class="stat-label">Checked in</div>
|
|
||||||
<div class="stat-value" style="color:var(--c-success)">{checkedIn}</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat">
|
|
||||||
<div class="stat-label">Remaining</div>
|
|
||||||
<div class="stat-value">{remaining}</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat">
|
|
||||||
<div class="stat-label">Progress</div>
|
|
||||||
<div class="stat-value">{pct}%</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if total > 0}
|
<!-- Ticket check-in (admin/ticketing) -->
|
||||||
<div class="card" style="margin-bottom:1rem">
|
{#if isTicketing}
|
||||||
<div style="height:8px;background:var(--c-border);border-radius:99px;overflow:hidden">
|
<h2 class="dash-section">Ticket Check-in</h2>
|
||||||
<div style="height:100%;width:{pct}%;background:var(--c-success);border-radius:99px;transition:width 0.4s ease"></div>
|
<div class="stats">
|
||||||
|
<div class="stat">
|
||||||
|
<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)">{ticketCheckedIn}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-label">Remaining</div>
|
||||||
|
<div class="stat-value">{ticketRemaining}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-label">Progress</div>
|
||||||
|
<div class="stat-value">{ticketPct}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if ticketTotal > 0}
|
||||||
|
<div class="card" style="margin-bottom:2rem">
|
||||||
|
<div style="height:8px;background:var(--c-border);border-radius:99px;overflow:hidden">
|
||||||
|
<div style="height:100%;width:{ticketPct}%;background:var(--c-success);border-radius:99px;transition:width 0.4s ease"></div>
|
||||||
|
</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>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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>
|
Welcome, <strong style="color:var(--c-text)">{session?.user?.username}</strong>
|
||||||
· <span class="badge badge-role">{session?.user?.role}</span>
|
· <span class="badge badge-role">{session?.user?.role}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,7 @@
|
||||||
{#if ($allDepts ?? []).length === 0}
|
{#if ($allDepts ?? []).length === 0}
|
||||||
<div class="empty">
|
<div class="empty">
|
||||||
<strong>No departments yet</strong>
|
<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>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
|
|
|
||||||
|
|
@ -315,8 +315,8 @@
|
||||||
|
|
||||||
{#if ($allShifts ?? []).length === 0 && !showAdd}
|
{#if ($allShifts ?? []).length === 0 && !showAdd}
|
||||||
<div class="empty">
|
<div class="empty">
|
||||||
<strong>No shifts yet</strong>
|
<strong>No shifts scheduled yet</strong>
|
||||||
<p>Add shifts to schedule your volunteers.</p>
|
<p>Create departments first, then add shifts here. Volunteers can self-select shifts via the kiosk.</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#each board as { dept, days }}
|
{#each board as { dept, days }}
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function roleLabel(r) {
|
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>
|
</script>
|
||||||
|
|
||||||
|
|
@ -129,6 +129,15 @@
|
||||||
</div>
|
</div>
|
||||||
</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}
|
{#if loadError}
|
||||||
<div class="alert alert-error">{loadError}</div>
|
<div class="alert alert-error">{loadError}</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -187,7 +196,8 @@
|
||||||
<div class="text-muted" style="padding:2rem 0">Loading…</div>
|
<div class="text-muted" style="padding:2rem 0">Loading…</div>
|
||||||
{:else if users.length === 0}
|
{:else if users.length === 0}
|
||||||
<div class="empty">
|
<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>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,14 @@
|
||||||
|
|
||||||
const role = $derived(session?.user?.role ?? '')
|
const role = $derived(session?.user?.role ?? '')
|
||||||
const canManage = $derived(['admin', 'ticketing', 'staffing', 'colead'].includes(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(() =>
|
const allVolunteers = liveQuery(() =>
|
||||||
db.volunteers.filter(v => !v.deleted_at).toArray()
|
db.volunteers.filter(v => !v.deleted_at).toArray()
|
||||||
|
|
@ -177,7 +185,7 @@
|
||||||
{#if ($allVolunteers ?? []).length === 0}
|
{#if ($allVolunteers ?? []).length === 0}
|
||||||
<div class="empty">
|
<div class="empty">
|
||||||
<strong>No volunteers yet</strong>
|
<strong>No volunteers yet</strong>
|
||||||
<p>Add volunteers manually.</p>
|
<p>Add volunteers manually above, or enable public signup in Settings.</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue