Turnpike/frontend/src/pages/Dashboard.svelte

183 lines
6.3 KiB
Svelte
Raw Normal View History

<script>
import { liveQuery } from 'dexie'
import { db } from '../db.js'
let { session } = $props()
const roles = $derived(session?.user?.roles ?? [])
function hasRole(...allowed) { return roles.some(r => allowed.includes(r)) }
const myDeptIDs = $derived(session?.user?.department_ids ?? [])
const isAdmin = $derived(hasRole('admin'))
const isStaffing = $derived(hasRole('admin', 'staffing'))
const isColead = $derived(hasRole('colead'))
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.ready).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">
<div class="page-header">
<h1 class="page-title">{$event?.name ?? 'Event'}</h1>
{#if $event?.venue}
<span class="text-muted">{$event.venue}</span>
{/if}
</div>
{#if $event?.start_date}
<p class="text-muted" style="margin-bottom:1.5rem">
{$event.start_date}{$event.end_date !== $event.start_date ? ` ${$event.end_date}` : ''}
{#if $event.timezone} · {$event.timezone}{/if}
</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) -->
{#if isAdmin}
<h2 class="dash-section">Ticket Check-in</h2>
<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 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/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}
<!-- Shift coverage (admin/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 isAdmin}
<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?.preferred_name}</strong>
· {#each roles as r}<span class="badge badge-role" style="margin-right:0.25rem">{r}</span>{/each}
</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>