Turnpike/frontend/src/pages/Dashboard.svelte

181 lines
6.3 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script>
import { liveQuery } from 'dexie'
import { db } from '../db.js'
let { session } = $props()
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 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">
<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/ticketing) -->
{#if isTicketing}
<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/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}
<!-- 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>