2026-03-03 11:27:07 -06:00
|
|
|
<script>
|
|
|
|
|
import { liveQuery } from 'dexie'
|
|
|
|
|
import { db } from '../db.js'
|
|
|
|
|
import { api } from '../api.js'
|
|
|
|
|
|
|
|
|
|
let { session } = $props()
|
|
|
|
|
|
|
|
|
|
let error = $state('')
|
|
|
|
|
let showAdd = $state(false)
|
|
|
|
|
let adding = $state(false)
|
|
|
|
|
let newName = $state('')
|
|
|
|
|
let newColor = $state('#6366f1')
|
|
|
|
|
let newDesc = $state('')
|
|
|
|
|
|
|
|
|
|
let editID = $state(null)
|
|
|
|
|
let editName = $state('')
|
|
|
|
|
let editColor = $state('#6366f1')
|
|
|
|
|
let editDesc = $state('')
|
|
|
|
|
let saving = $state(false)
|
|
|
|
|
|
|
|
|
|
const role = $derived(session?.user?.role ?? '')
|
2026-03-04 12:00:36 -06:00
|
|
|
const canCreate = $derived(['admin', 'ticketing', 'staffing'].includes(role))
|
|
|
|
|
const canDelete = $derived(['admin', 'ticketing'].includes(role))
|
2026-03-03 11:27:07 -06:00
|
|
|
|
|
|
|
|
const allDepts = liveQuery(() =>
|
|
|
|
|
db.departments.filter(d => !d.deleted_at).toArray()
|
|
|
|
|
.then(arr => arr.sort((a, b) => a.name.localeCompare(b.name)))
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async function addDept(e) {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
adding = true
|
|
|
|
|
error = ''
|
|
|
|
|
try {
|
|
|
|
|
const d = await api.departments.create({ name: newName, color: newColor, description: newDesc })
|
|
|
|
|
await db.departments.put(d)
|
|
|
|
|
showAdd = false
|
|
|
|
|
newName = newDesc = ''
|
|
|
|
|
newColor = '#6366f1'
|
|
|
|
|
} catch (err) {
|
|
|
|
|
error = err.message
|
|
|
|
|
} finally {
|
|
|
|
|
adding = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function startEdit(d) {
|
|
|
|
|
editID = d.id
|
|
|
|
|
editName = d.name
|
|
|
|
|
editColor = d.color || '#6366f1'
|
|
|
|
|
editDesc = d.description || ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function cancelEdit() {
|
|
|
|
|
editID = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveDept(d) {
|
|
|
|
|
if (!editName.trim()) return
|
|
|
|
|
saving = true
|
|
|
|
|
error = ''
|
|
|
|
|
try {
|
|
|
|
|
const updated = await api.departments.update(d.id, {
|
|
|
|
|
name: editName, color: editColor, description: editDesc,
|
|
|
|
|
})
|
|
|
|
|
await db.departments.put(updated)
|
|
|
|
|
editID = null
|
|
|
|
|
} catch (err) {
|
|
|
|
|
error = err.message
|
|
|
|
|
} finally {
|
|
|
|
|
saving = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function deleteDept(d) {
|
|
|
|
|
if (!confirm(`Delete department "${d.name}"? Volunteers in this department will be unassigned.`)) return
|
|
|
|
|
try {
|
|
|
|
|
await api.departments.delete(d.id)
|
|
|
|
|
await db.departments.delete(d.id)
|
|
|
|
|
} catch (err) {
|
|
|
|
|
error = err.message
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<div class="page">
|
|
|
|
|
<div class="page-header">
|
|
|
|
|
<h1 class="page-title">Departments</h1>
|
|
|
|
|
{#if canCreate}
|
|
|
|
|
<div class="actions">
|
|
|
|
|
<button class="btn btn-primary btn-sm" onclick={() => showAdd = !showAdd}>+ Add</button>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{#if error}
|
|
|
|
|
<div class="alert alert-error">{error}</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
{#if showAdd && canCreate}
|
|
|
|
|
<div class="card" style="margin-bottom:1.5rem">
|
|
|
|
|
<form onsubmit={addDept}>
|
|
|
|
|
<div style="display:grid;grid-template-columns:1fr 1fr auto;gap:1rem;align-items:end">
|
|
|
|
|
<div class="form-group" style="margin-bottom:0">
|
|
|
|
|
<label for="d-name">Name *</label>
|
|
|
|
|
<input id="d-name" bind:value={newName} required placeholder="e.g. Security" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group" style="margin-bottom:0">
|
|
|
|
|
<label for="d-desc">Description</label>
|
|
|
|
|
<input id="d-desc" bind:value={newDesc} placeholder="Optional" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group" style="margin-bottom:0">
|
|
|
|
|
<label for="d-color">Color</label>
|
|
|
|
|
<input id="d-color" type="color" bind:value={newColor} style="width:60px;padding:0.2rem;height:2.3rem;cursor:pointer" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="actions" style="margin-top:1rem">
|
|
|
|
|
<button type="submit" class="btn btn-primary" disabled={adding}>
|
|
|
|
|
{adding ? 'Adding…' : 'Add department'}
|
|
|
|
|
</button>
|
|
|
|
|
<button type="button" class="btn btn-ghost" onclick={() => showAdd = false}>Cancel</button>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
{#if ($allDepts ?? []).length === 0}
|
|
|
|
|
<div class="empty">
|
|
|
|
|
<strong>No departments yet</strong>
|
2026-03-04 20:52:12 -06:00
|
|
|
<p>Create departments to organize shifts and volunteer teams. Coleads are assigned to specific departments.</p>
|
2026-03-03 11:27:07 -06:00
|
|
|
</div>
|
|
|
|
|
{:else}
|
|
|
|
|
<div class="table-wrap">
|
|
|
|
|
<table>
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>Department</th>
|
|
|
|
|
<th>Description</th>
|
|
|
|
|
{#if canCreate}<th></th>{/if}
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{#each $allDepts ?? [] as d (d.id)}
|
|
|
|
|
{#if editID === d.id}
|
|
|
|
|
<tr>
|
|
|
|
|
<td>
|
|
|
|
|
<div style="display:flex;align-items:center;gap:0.5rem">
|
|
|
|
|
<input type="color" bind:value={editColor} style="width:36px;height:36px;padding:0.1rem;border-radius:4px;cursor:pointer;flex-shrink:0" />
|
|
|
|
|
<input bind:value={editName} required placeholder="Name" style="margin:0" />
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
<td>
|
|
|
|
|
<input bind:value={editDesc} placeholder="Description" style="margin:0" />
|
|
|
|
|
</td>
|
|
|
|
|
{#if canCreate}
|
|
|
|
|
<td>
|
|
|
|
|
<div class="actions">
|
|
|
|
|
<button class="btn btn-primary btn-sm" onclick={() => saveDept(d)} disabled={saving}>
|
|
|
|
|
{saving ? '…' : 'Save'}
|
|
|
|
|
</button>
|
|
|
|
|
<button class="btn btn-ghost btn-sm" onclick={cancelEdit}>Cancel</button>
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
{/if}
|
|
|
|
|
</tr>
|
|
|
|
|
{:else}
|
|
|
|
|
<tr>
|
|
|
|
|
<td>
|
|
|
|
|
<span class="dept-dot" style="background:{d.color};margin-right:0.5rem"></span>
|
|
|
|
|
<strong>{d.name}</strong>
|
|
|
|
|
</td>
|
|
|
|
|
<td class="text-muted">{d.description || '—'}</td>
|
|
|
|
|
{#if canCreate}
|
|
|
|
|
<td>
|
|
|
|
|
<div class="actions">
|
|
|
|
|
<button class="btn btn-ghost btn-sm" onclick={() => startEdit(d)}>Edit</button>
|
|
|
|
|
{#if canDelete}
|
|
|
|
|
<button class="btn btn-danger btn-sm" onclick={() => deleteDept(d)}>Delete</button>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
{/if}
|
|
|
|
|
</tr>
|
|
|
|
|
{/if}
|
|
|
|
|
{/each}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|