Turnpike/frontend/src/api.js

153 lines
7.3 KiB
JavaScript
Raw Normal View History

import { db } from './db.js'
async function getToken() {
const session = await db.session.get(1)
return session?.token ?? null
}
export async function apiFetch(path, options = {}) {
const token = await getToken()
const headers = {}
// Don't set Content-Type for FormData — browser sets it with correct boundary
if (!(options.body instanceof FormData)) {
headers['Content-Type'] = 'application/json'
}
Object.assign(headers, options.headers)
if (token) headers['Authorization'] = `Bearer ${token}`
const res = await fetch(path, { ...options, headers })
if (res.status === 401) {
await db.session.clear()
window.location.pathname = '/login'
throw new Error('unauthorized')
}
return res
}
export async function apiJSON(path, options = {}) {
const res = await apiFetch(path, options)
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText }))
throw new Error(err.error || res.statusText)
}
return res.json()
}
// Unauthenticated fetch for the kiosk (no JWT, no redirect on 401)
async function kioskFetch(path, options = {}) {
const headers = { 'Content-Type': 'application/json', ...options.headers }
const res = await fetch(path, { ...options, headers })
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText }))
const e = new Error(err.error || res.statusText)
e.status = res.status
e.body = err
throw e
}
return res.json()
}
export const api = {
login: (username, password) =>
apiJSON('/api/login', { method: 'POST', body: JSON.stringify({ username, password }) }),
logout: () => apiFetch('/api/logout', { method: 'POST' }),
me: () => apiJSON('/api/me'),
event: {
get: () => apiJSON('/api/event'),
update: (data) => apiJSON('/api/event', { method: 'PUT', body: JSON.stringify(data) }),
},
participants: {
list: (params = {}) => apiJSON('/api/participants?' + new URLSearchParams(params)),
get: (id) => apiJSON(`/api/participants/${id}`),
create: (data) => apiJSON('/api/participants', { method: 'POST', body: JSON.stringify(data) }),
update: (id, data) => apiJSON(`/api/participants/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
delete: (id) => apiFetch(`/api/participants/${id}`, { method: 'DELETE' }),
merge: (id, otherId) => apiJSON(`/api/participants/${id}/merge/${otherId}`, { method: 'POST' }),
},
tickets: {
list: () => apiJSON('/api/tickets'),
create: (data) => apiJSON('/api/tickets', { method: 'POST', body: JSON.stringify(data) }),
checkIn: (id) => apiJSON(`/api/tickets/${id}/checkin`, { method: 'POST' }),
generateCodes: () => apiJSON('/api/tickets/generate-codes', { method: 'POST' }),
emailCode: (id) => apiJSON(`/api/tickets/${id}/email-code`, { method: 'POST' }),
emailAllCodes: () => apiJSON('/api/tickets/email-codes', { method: 'POST' }),
},
volunteers: {
list: (params = {}) => apiJSON('/api/volunteers?' + new URLSearchParams(params)),
get: (id) => apiJSON(`/api/volunteers/${id}`),
create: (data) => apiJSON('/api/volunteers', { method: 'POST', body: JSON.stringify(data) }),
update: (id, data) => apiJSON(`/api/volunteers/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
delete: (id) => apiFetch(`/api/volunteers/${id}`, { method: 'DELETE' }),
checkIn: (id) => apiJSON(`/api/volunteers/${id}/checkin`, { method: 'POST' }),
assignShift: (id, shiftId) => apiFetch(`/api/volunteers/${id}/shifts`, { method: 'POST', body: JSON.stringify({ shift_id: shiftId }) }),
unassignShift: (id, shiftId) => apiFetch(`/api/volunteers/${id}/shifts/${shiftId}`, { method: 'DELETE' }),
},
departments: {
list: () => apiJSON('/api/departments'),
create: (data) => apiJSON('/api/departments', { method: 'POST', body: JSON.stringify(data) }),
update: (id, data) => apiJSON(`/api/departments/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
delete: (id) => apiFetch(`/api/departments/${id}`, { method: 'DELETE' }),
},
shifts: {
list: (params = {}) => apiJSON('/api/shifts?' + new URLSearchParams(params)),
create: (data) => apiJSON('/api/shifts', { method: 'POST', body: JSON.stringify(data) }),
update: (id, data) => apiJSON(`/api/shifts/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
delete: (id) => apiFetch(`/api/shifts/${id}`, { method: 'DELETE' }),
assignVolunteer: (shiftId, volunteerId, force = false) =>
apiFetch(`/api/shifts/${shiftId}/volunteers`, { method: 'POST', body: JSON.stringify({ volunteer_id: volunteerId, force }) }),
unassignVolunteer: (shiftId, volunteerId) =>
apiFetch(`/api/shifts/${shiftId}/volunteers/${volunteerId}`, { method: 'DELETE' }),
reorder: (positions) =>
apiFetch('/api/shifts/reorder', { method: 'POST', body: JSON.stringify(positions) }),
},
users: {
list: () => apiJSON('/api/users'),
create: (data) => apiJSON('/api/users', { method: 'POST', body: JSON.stringify(data) }),
update: (id, data) => apiJSON(`/api/users/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
delete: (id) => apiFetch(`/api/users/${id}`, { method: 'DELETE' }),
},
settings: {
get: () => apiJSON('/api/settings'),
update: (data) => apiJSON('/api/settings', { method: 'PUT', body: JSON.stringify(data) }),
testEmail: (to) => apiJSON('/api/settings/test-email', { method: 'POST', body: JSON.stringify({ to }) }),
2026-03-03 17:59:35 -06:00
toggleShiftSignups: (open) => apiJSON('/api/settings/shift-signups', { method: 'POST', body: JSON.stringify({ open }) }),
resetTickets: () => apiJSON('/api/settings/reset-tickets', { method: 'POST' }),
resetVolunteers: () => apiJSON('/api/settings/reset-volunteers', { method: 'POST' }),
resetShifts: () => apiJSON('/api/settings/reset-shifts', { method: 'POST' }),
resetDepartments: () => apiJSON('/api/settings/reset-departments', { method: 'POST' }),
resetVolunteerShifts: () => apiJSON('/api/settings/reset-volunteer-shifts', { method: 'POST' }),
2026-03-03 17:59:35 -06:00
},
signup: {
config: () => kioskFetch('/api/public/signup-config'),
submit: (data) => kioskFetch('/api/public/signup', { method: 'POST', body: JSON.stringify(data) }),
confirm: (token) => kioskFetch('/api/public/confirm', { method: 'POST', body: JSON.stringify({ token }) }),
},
import: async (formData) => {
const res = await apiFetch('/api/import', { method: 'POST', body: formData })
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText }))
throw new Error(err.error || res.statusText)
}
return res.json()
},
sync: {
pull: (since) => apiJSON('/api/sync/pull' + (since ? `?since=${encodeURIComponent(since)}` : '')),
},
kiosk: {
get: (token) => kioskFetch(`/api/v/${token}`),
// claim returns {conflict: true, conflicting_shifts: [...]} on 409, or the updated kiosk state on success.
claim: async (token, shiftId, force = false) => {
const res = await fetch(`/api/v/${token}/shifts/${shiftId}${force ? '?force=true' : ''}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
})
const body = await res.json().catch(() => ({}))
if (res.status === 409) return { conflict: true, ...body }
if (!res.ok) throw new Error(body.error || res.statusText)
return body
},
unclaim: (token, shiftId) =>
kioskFetch(`/api/v/${token}/shifts/${shiftId}`, { method: 'DELETE' }),
},
}