feat(frontend): add apiFetch wrapper for automatic auth token injection

Centralizes auth token management into a reusable apiFetch() helper and getAuthHeaders() utility, eliminating inline Bearer token logic across all frontend files.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-07-06 19:21:34 +01:00
co-authored by Sisyphus
parent e831953e5b
commit 92124158bf
48 changed files with 530 additions and 1190 deletions
@@ -1,5 +1,5 @@
<script lang="ts">
import { authStore } from '$lib/stores/auth.svelte';
import { apiFetch } from '$lib/utils/api';
import { SvelteDate, SvelteMap } from 'svelte/reactivity';
import { CalendarDate, getLocalTimeZone, type DateValue } from '@internationalized/date';
import { toast } from 'svelte-sonner';
@@ -502,16 +502,8 @@
const endStr = `${endYear}-${String(endMonth).padStart(2, '0')}-${String(daysInEndMonth).padStart(2, '0')}`;
const [whRes, ahRes] = await Promise.all([
fetch(`/api/scheduling/working-hours?start=${startStr}&end=${endStr}`, {
headers: authStore.currentToken
? { Authorization: `Bearer ${authStore.currentToken}` }
: {}
}),
fetch(`/api/scheduling/available-hours?start=${startStr}&end=${endStr}`, {
headers: authStore.currentToken
? { Authorization: `Bearer ${authStore.currentToken}` }
: {}
})
apiFetch(`/api/scheduling/working-hours?start=${startStr}&end=${endStr}`),
apiFetch(`/api/scheduling/available-hours?start=${startStr}&end=${endStr}`)
]);
if (whRes.ok && ahRes.ok) {
@@ -578,16 +570,8 @@
const endStr = endOfMonth.toString();
const [whRes, ahRes] = await Promise.all([
fetch(`/api/scheduling/working-hours?start=${startStr}&end=${endStr}`, {
headers: authStore.currentToken
? { Authorization: `Bearer ${authStore.currentToken}` }
: {}
}),
fetch(`/api/scheduling/available-hours?start=${startStr}&end=${endStr}`, {
headers: authStore.currentToken
? { Authorization: `Bearer ${authStore.currentToken}` }
: {}
})
apiFetch(`/api/scheduling/working-hours?start=${startStr}&end=${endStr}`),
apiFetch(`/api/scheduling/available-hours?start=${startStr}&end=${endStr}`)
]);
if (whRes.ok && ahRes.ok) {
@@ -623,9 +607,7 @@
async function fetchAvailableServices() {
loadingServices = true;
try {
const response = await fetch('/api/services', {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
});
const response = await apiFetch('/api/services');
if (response.ok) {
availableServices = (await response.json()) as Service[];
}
@@ -726,12 +708,9 @@
body.notes = notes.trim();
}
const response = await fetch(`/api/bookings/${booking.id}/edit-request`, {
const response = await apiFetch(`/api/bookings/${booking.id}/edit-request`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
@@ -2,6 +2,7 @@
import PolicyPopover from '$lib/components/ui/policyPopover.svelte';
import { POLICY } from '$lib/constants/policy';
import { authStore } from '$lib/stores/auth.svelte';
import { apiFetch } from '$lib/utils/api';
import { ensureBusinessInfo, getBusinessInfo } from '$lib/stores/businessInfo.svelte';
import { SvelteDate } from 'svelte/reactivity';
import { toast } from 'svelte-sonner';
@@ -180,12 +181,9 @@
}
tipProcessing = true;
try {
const response = await fetch(`/api/bookings/${selectedBooking.id}/tip`, {
const response = await apiFetch(`/api/bookings/${selectedBooking.id}/tip`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
amount: Math.round(tipAmount * 100),
card_token: 'placeholder'
@@ -215,12 +213,9 @@
if (!bookingId) return;
try {
const bookingResp = await fetch(`/api/bookings/${bookingId}`, {
const bookingResp = await apiFetch(`/api/bookings/${bookingId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
}
headers: { 'Content-Type': 'application/json' }
});
if (bookingResp.ok) {
@@ -230,9 +225,7 @@
// Ensure business info is loaded (cached by shared store)
ensureBusinessInfo();
const editResp = await fetch(`/api/bookings/${bookingId}/edit-request`, {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
});
const editResp = await apiFetch(`/api/bookings/${bookingId}/edit-request`);
const editData = await editResp.json();
hasPendingEditRequest = editData.edit_request != null;
pendingEditRequest = editData.edit_request || null;
@@ -356,12 +349,9 @@ ${hasVAT ? `<p class="warning">VAT is included at ${biz?.default_vat_rate ?? 20}
cancelling = true;
try {
const body = hasPayments ? { reason: 'client_cancelled' } : undefined;
const response = await fetch(`/api/bookings/${selectedBooking.id}`, {
const response = await apiFetch(`/api/bookings/${selectedBooking.id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
},
headers: { 'Content-Type': 'application/json' },
body: body ? JSON.stringify(body) : undefined
});
@@ -1,6 +1,6 @@
<script lang="ts">
import { SvelteDate } from 'svelte/reactivity';
import { authStore } from '$lib/stores/auth.svelte';
import { apiFetch } from '$lib/utils/api';
import { toast } from 'svelte-sonner';
import { sanitizeText } from '$lib/utils/toast-safe';
import { formatDateTime, formatDuration } from '$lib/utils/format';
@@ -140,11 +140,10 @@
if (!booking?.id) return;
loadingOverlaps = true;
try {
const response = await fetch(`/api/admin/bookings/${booking.id}/overlapping`, {
const response = await apiFetch(`/api/admin/bookings/${booking.id}/overlapping`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
}
});
if (response.ok) {
@@ -322,11 +321,10 @@
serviceOverrides: overrides.length > 0 ? overrides : undefined
};
const response = await fetch(`/api/admin/bookings/${booking.id}/confirm`, {
const response = await apiFetch(`/api/admin/bookings/${booking.id}/confirm`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
@@ -351,11 +349,10 @@
const loadingToast = toast.loading('Declining booking...');
try {
const response = await fetch(`/api/admin/bookings/${booking.id}/cancel`, {
const response = await apiFetch(`/api/admin/bookings/${booking.id}/cancel`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
}
});
@@ -1,5 +1,5 @@
<script lang="ts">
import { authStore } from '$lib/stores/auth.svelte';
import { apiFetch } from '$lib/utils/api';
import { toast } from 'svelte-sonner';
import { SvelteDate, SvelteSet, SvelteURLSearchParams } from 'svelte/reactivity';
import { CalendarDate, getLocalTimeZone, type DateValue } from '@internationalized/date';
@@ -473,11 +473,8 @@
async function fetchUsers() {
loadingUsers = true;
try {
const response = await fetch(
`/api/admin/users?page=1&per_page=4&q=${encodeURIComponent(userQuery)}`,
{
headers: { Authorization: `Bearer ${authStore.currentToken}` }
}
const response = await apiFetch(
`/api/admin/users?page=1&per_page=4&q=${encodeURIComponent(userQuery)}`
);
if (response.ok) {
const data = await response.json();
@@ -503,9 +500,7 @@
if (selectedUserId) {
url = `/api/services/eligible-for/${selectedUserId}`;
}
const response = await fetch(url, {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
});
const response = await apiFetch(url);
if (response.ok) {
services = await response.json();
}
@@ -538,17 +533,11 @@
try {
const [whRes, ahRes] = await Promise.all([
fetch(
`/api/scheduling/working-hours?start=${startStr}&end=${endStr}${outOfHours ? '&out_of_hours=true' : ''}`,
{
headers: { Authorization: `Bearer ${authStore.currentToken}` }
}
apiFetch(
`/api/scheduling/working-hours?start=${startStr}&end=${endStr}${outOfHours ? '&out_of_hours=true' : ''}`
),
fetch(
`/api/scheduling/available-hours?start=${startStr}&end=${endStr}${outOfHours ? '&out_of_hours=true' : ''}`,
{
headers: { Authorization: `Bearer ${authStore.currentToken}` }
}
apiFetch(
`/api/scheduling/available-hours?start=${startStr}&end=${endStr}${outOfHours ? '&out_of_hours=true' : ''}`
)
]);
@@ -620,17 +609,11 @@
);
const [whRes, ahRes] = await Promise.all([
fetch(
`/api/scheduling/working-hours?start=${startOfMonth}&end=${endOfMonth}${outOfHours ? '&out_of_hours=true' : ''}`,
{
headers: { Authorization: `Bearer ${authStore.currentToken}` }
}
apiFetch(
`/api/scheduling/working-hours?start=${startOfMonth}&end=${endOfMonth}${outOfHours ? '&out_of_hours=true' : ''}`
),
fetch(
`/api/scheduling/available-hours?start=${startOfMonth}&end=${endOfMonth}${outOfHours ? '&out_of_hours=true' : ''}`,
{
headers: { Authorization: `Bearer ${authStore.currentToken}` }
}
apiFetch(
`/api/scheduling/available-hours?start=${startOfMonth}&end=${endOfMonth}${outOfHours ? '&out_of_hours=true' : ''}`
)
]);
@@ -713,11 +696,10 @@
payload.custom_service_ids = customServiceIds;
}
const response = await fetch('/api/admin/bookings/reserve', {
const response = await apiFetch('/api/admin/bookings/reserve', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
@@ -818,9 +800,7 @@
} else {
params.set('popular', '3');
}
const response = await fetch(`/api/admin/custom-services?${params}`, {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
});
const response = await apiFetch(`/api/admin/custom-services?${params}`);
if (response.ok) {
const data = await response.json();
const list = data.services || data;
@@ -841,11 +821,10 @@
}
creatingCustomService = true;
try {
const response = await fetch('/api/admin/custom-services', {
const response = await apiFetch('/api/admin/custom-services', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: (newCustomService.name ?? '').trim(),
@@ -955,11 +934,10 @@
return;
}
const phone = toE164UK(guestPhone)!;
const createRes = await fetch('/api/users/guest', {
const createRes = await apiFetch('/api/users/guest', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify({
firstName: guestName.trim().split(' ')[0] || 'Guest',
@@ -1025,11 +1003,10 @@
out_of_hours: outOfHours
};
const res = await fetch('/api/admin/bookings', {
const res = await apiFetch('/api/admin/bookings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
@@ -1,5 +1,6 @@
<script lang="ts">
import { authStore } from '$lib/stores/auth.svelte';
import { apiFetch } from '$lib/utils/api';
import { toast } from 'svelte-sonner';
import { SvelteDate } from 'svelte/reactivity';
import * as Modal from '$lib/components/ui/dialog';
@@ -70,11 +71,10 @@
const body: Record<string, unknown> = {};
if (forgiveFeesCancel) body.forgive_fees = true;
if (forgiveNoShowCancel) body.forgive_noshow = true;
const response = await fetch(`/api/admin/bookings/${selectedBooking.id}/cancel`, {
const response = await apiFetch(`/api/admin/bookings/${selectedBooking.id}/cancel`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
@@ -108,11 +108,10 @@
if (!bookingId) return;
try {
const response = await fetch(`/api/admin/bookings/${bookingId}`, {
const response = await apiFetch(`/api/admin/bookings/${bookingId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
}
});
@@ -1,5 +1,5 @@
<script lang="ts">
import { authStore } from '$lib/stores/auth.svelte';
import { apiFetch } from '$lib/utils/api';
import { SvelteDate, SvelteURLSearchParams } from 'svelte/reactivity';
import { toast } from 'svelte-sonner';
@@ -44,11 +44,10 @@
url = '/api/admin/bookings/search';
}
const response = await fetch(`${url}?${params}`, {
const response = await apiFetch(`${url}?${params}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
}
});
if (response.ok) {
@@ -1,5 +1,6 @@
<script lang="ts">
import { authStore } from '$lib/stores/auth.svelte';
import { apiFetch } from '$lib/utils/api';
import { toast } from 'svelte-sonner';
import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card';
@@ -36,9 +37,7 @@
async function fetchSettings() {
loading = true;
try {
const res = await fetch('/api/admin/settings', {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
});
const res = await apiFetch('/api/admin/settings');
if (res.ok) {
settings = await res.json();
} else {
@@ -237,11 +236,10 @@
return;
}
const res = await fetch('/api/admin/settings', {
const res = await apiFetch('/api/admin/settings', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify(patch)
});
@@ -1,5 +1,5 @@
<script lang="ts">
import { authStore } from '$lib/stores/auth.svelte';
import { apiFetch } from '$lib/utils/api';
import { toast } from 'svelte-sonner';
import { sanitizeText } from '$lib/utils/toast-safe';
import { Button } from '$lib/components/ui/button';
@@ -112,9 +112,7 @@
per_page: perPage.toString()
});
if (searchQuery.trim()) params.set('q', searchQuery.trim());
const response = await fetch(`/api/admin/custom-services?${params}`, {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
});
const response = await apiFetch(`/api/admin/custom-services?${params}`);
if (response.ok) {
const data = await response.json();
services = data.services;
@@ -137,11 +135,10 @@
}
creating = true;
try {
const response = await fetch('/api/admin/custom-services', {
const response = await apiFetch('/api/admin/custom-services', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: (newService.name ?? '').trim(),
@@ -175,9 +172,8 @@
)
return;
try {
const response = await fetch(`/api/admin/custom-services/${id}/promote`, {
method: 'POST',
headers: { Authorization: `Bearer ${authStore.currentToken}` }
const response = await apiFetch(`/api/admin/custom-services/${id}/promote`, {
method: 'POST'
});
if (response.ok) {
const data = await response.json();
@@ -197,9 +193,8 @@
async function deleteService(id: string) {
if (!confirm('Delete this custom service?')) return;
try {
const response = await fetch(`/api/admin/custom-services/${id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${authStore.currentToken}` }
const response = await apiFetch(`/api/admin/custom-services/${id}`, {
method: 'DELETE'
});
if (response.ok) {
toast.success('Deleted');
@@ -1,6 +1,6 @@
<script lang="ts">
import { SvelteDate } from 'svelte/reactivity';
import { authStore } from '$lib/stores/auth.svelte';
import { apiFetch } from '$lib/utils/api';
import { toast } from 'svelte-sonner';
import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card';
@@ -101,9 +101,7 @@
async function loadCampaigns() {
loading = true;
try {
const res = await fetch('/api/admin/discount-campaigns', {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
});
const res = await apiFetch('/api/admin/discount-campaigns');
if (res.ok) campaigns = await res.json();
else toast.error('Failed to load campaigns');
} catch {
@@ -173,11 +171,10 @@
: '/api/admin/discount-campaigns';
const method = editingCampaign ? 'PUT' : 'POST';
const res = await fetch(url, {
const res = await apiFetch(url, {
method,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
@@ -200,11 +197,10 @@
async function updateStatus(c: Campaign, status: string) {
actionInProgress = c.id;
try {
const res = await fetch(`/api/admin/discount-campaigns/${c.id}`, {
const res = await apiFetch(`/api/admin/discount-campaigns/${c.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify({ status })
});
@@ -222,9 +218,8 @@
async function cancelCampaign(c: Campaign) {
actionInProgress = c.id;
try {
const res = await fetch(`/api/admin/discount-campaigns/${c.id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${authStore.currentToken}` }
const res = await apiFetch(`/api/admin/discount-campaigns/${c.id}`, {
method: 'DELETE'
});
if (res.ok) {
toast.success('Campaign cancelled');
@@ -241,9 +236,7 @@
statsLoading = true;
showStatsModal = true;
try {
const res = await fetch(`/api/admin/discount-campaigns/${c.id}/stats`, {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
});
const res = await apiFetch(`/api/admin/discount-campaigns/${c.id}/stats`);
if (res.ok) statsData = await res.json();
else toast.error('Failed to load stats');
} catch {
@@ -1,5 +1,5 @@
<script lang="ts">
import { authStore } from '$lib/stores/auth.svelte';
import { apiFetch } from '$lib/utils/api';
import { SvelteDate } from 'svelte/reactivity';
import { toast } from 'svelte-sonner';
import { formatDuration } from '$lib/utils/format';
@@ -59,11 +59,10 @@
async function fetchBooking() {
loading = true;
try {
const response = await fetch(`/api/admin/bookings/${bookingId}`, {
const response = await apiFetch(`/api/admin/bookings/${bookingId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
}
});
@@ -241,11 +240,7 @@
async function fetchAvailableServices() {
loadingServices = true;
try {
const response = await fetch('/api/services', {
headers: {
Authorization: `Bearer ${authStore.currentToken}`
}
});
const response = await apiFetch('/api/services');
if (response.ok) {
const data = await response.json();
@@ -322,11 +317,10 @@
payload.notes = notes || undefined;
}
const response = await fetch(`/api/admin/bookings/${bookingId}`, {
const response = await apiFetch(`/api/admin/bookings/${bookingId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
@@ -368,11 +362,10 @@
return;
}
const response = await fetch(`/api/admin/payments/${refundPaymentId}/refund`, {
const response = await apiFetch(`/api/admin/payments/${refundPaymentId}/refund`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify({
amount: amountPence,
@@ -1,5 +1,5 @@
<script lang="ts">
import { authStore } from '$lib/stores/auth.svelte';
import { apiFetch } from '$lib/utils/api';
import { toast } from 'svelte-sonner';
import * as Modal from '$lib/components/ui/dialog';
import { Button } from '$lib/components/ui/button';
@@ -112,13 +112,12 @@
const loadingToast = toast.loading('Approving change request...');
try {
const response = await fetch(
const response = await apiFetch(
`/api/admin/bookings/${editRequest.booking_id}/edit-requests/${editRequest.id}/approve`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
}
}
);
@@ -144,13 +143,12 @@
const loadingToast = toast.loading('Denying change request...');
try {
const response = await fetch(
const response = await apiFetch(
`/api/admin/bookings/${editRequest.booking_id}/edit-requests/${editRequest.id}/deny`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
}
}
);
@@ -1,5 +1,6 @@
<script lang="ts">
import { authStore } from '$lib/stores/auth.svelte';
import { apiFetch } from '$lib/utils/api';
import { toast } from 'svelte-sonner';
import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card';
@@ -259,11 +260,7 @@
});
if (search.trim()) params.append('q', search.trim());
const res = await fetch(`/api/admin/gift-cards?${params}`, {
headers: {
Authorization: `Bearer ${authStore.currentToken}`
}
});
const res = await apiFetch(`/api/admin/gift-cards?${params}`);
if (res.ok) {
const data = await res.json();
summary.total_unclaimed = data.total_unclaimed;
@@ -293,11 +290,7 @@
async function fetchExpiredBalances() {
loadingExpired = true;
try {
const res = await fetch('/api/admin/gift-cards/expired-balances', {
headers: {
Authorization: `Bearer ${authStore.currentToken}`
}
});
const res = await apiFetch('/api/admin/gift-cards/expired-balances');
if (res.ok) {
const data = await res.json();
expiredBalances = data.expired_balances || [];
@@ -314,11 +307,10 @@
async function claimExpiredBalance(balanceId: string) {
claimingId = balanceId;
try {
const res = await fetch('/api/admin/gift-cards/expired-balances/claim', {
const res = await apiFetch('/api/admin/gift-cards/expired-balances/claim', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify({ balance_id: balanceId })
});
@@ -352,9 +344,7 @@
loadingCurrentCustomer = true;
currentCustomerInfo = null;
try {
const res = await fetch('/api/admin/today/current-next', {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
});
const res = await apiFetch('/api/admin/today/current-next');
if (res.ok) {
const data = await res.json();
const appointment = data.current || data.next;
@@ -385,9 +375,8 @@
if (!generateUserQuery.trim()) return;
generateLoadingUsers = true;
try {
const res = await fetch(
`/api/admin/users?page=1&per_page=5&q=${encodeURIComponent(generateUserQuery)}`,
{ headers: { Authorization: `Bearer ${authStore.currentToken}` } }
const res = await apiFetch(
`/api/admin/users?page=1&per_page=5&q=${encodeURIComponent(generateUserQuery)}`
);
if (res.ok) {
const data = await res.json();
@@ -406,11 +395,10 @@
async function generateInventoryCard() {
creating = true;
try {
const res = await fetch('/api/admin/gift-cards', {
const res = await apiFetch('/api/admin/gift-cards', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify({
amount: 0,
@@ -449,11 +437,10 @@
if (!isTransferValid || !selectedCardId) return;
transferring = true;
try {
const res = await fetch(`/api/admin/gift-cards/${selectedCardId}/transfer`, {
const res = await apiFetch(`/api/admin/gift-cards/${selectedCardId}/transfer`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify({
to_card_id: transferToCode,
@@ -570,11 +557,10 @@
if (actionType === 'create' && generateType === 'account' && selectedCustomer)
body.redeem_to_user_id = selectedCustomer.id;
const res = await fetch('/api/admin/till/sale', {
const res = await apiFetch('/api/admin/till/sale', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
@@ -610,11 +596,10 @@
if (actionType === 'create' && generateType === 'account' && selectedCustomer)
body.redeem_to_user_id = selectedCustomer.id;
const res = await fetch('/api/admin/till/sale', {
const res = await apiFetch('/api/admin/till/sale', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
@@ -650,9 +635,7 @@
await new Promise((r) => setTimeout(r, 2000));
attempts++;
try {
const res = await fetch(`/api/admin/till/sale/checkout/${ckId}/status`, {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
});
const res = await apiFetch(`/api/admin/till/sale/checkout/${ckId}/status`);
if (res.ok) {
const data = await res.json();
if (data.status === 'COMPLETED') {
@@ -700,11 +683,10 @@
if (actionType === 'create' && generateType === 'account' && selectedCustomer)
body.redeem_to_user_id = selectedCustomer.id;
const res = await fetch('/api/admin/till/sale', {
const res = await apiFetch('/api/admin/till/sale', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
@@ -736,11 +718,10 @@
idempotency_key: 'till-on-the-house-' + gcId + '-' + Date.now()
};
const res = await fetch('/api/admin/till/sale', {
const res = await apiFetch('/api/admin/till/sale', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
@@ -1,8 +1,8 @@
<script lang="ts">
import { authStore } from '$lib/stores/auth.svelte';
import { SvelteDate } from 'svelte/reactivity';
import { toast } from 'svelte-sonner';
import { sanitizeText } from '$lib/utils/toast-safe';
import { apiFetch } from '$lib/utils/api';
// shadcn-svelte components
import { Button } from '$lib/components/ui/button';
@@ -138,11 +138,9 @@
async function fetchExceptionGroups() {
exceptionGroupsLoading = true;
try {
const response = await fetch('/api/scheduling/exceptional-groups', {
method: 'GET',
const response = await apiFetch('/api/scheduling/exceptional-groups', {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
}
});
@@ -210,11 +208,10 @@
}))
};
const response = await fetch('/api/scheduling/exceptional-groups', {
const response = await apiFetch('/api/scheduling/exceptional-groups', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
@@ -242,12 +239,12 @@
const loadingToast = toast.loading('Deleting exception group...');
try {
const response = await fetch(`/api/scheduling/exceptional-groups?id=${exceptionToDelete}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${authStore.currentToken}`
const response = await apiFetch(
`/api/scheduling/exceptional-groups?id=${exceptionToDelete}`,
{
method: 'DELETE'
}
});
);
if (response.ok || response.status === 204) {
toast.success('Exception group deleted successfully!', { id: loadingToast });
@@ -2,8 +2,8 @@
import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card';
import FileDropZone from '$lib/components/ui/file-drop-zone.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import * as AlertDialog from '$lib/components/ui/alert-dialog';
import { apiFetch } from '$lib/utils/api';
// =============== Image Upload ===============
let uploading = $state(false);
@@ -581,11 +581,8 @@
/* -------- 5. Call the API ------------------------------------- */
uploadStatus[fileKey] = 'Uploading...';
const response = await fetch('/api/portfolio/images', {
const response = await apiFetch('/api/portfolio/images', {
method: 'POST',
headers: {
Authorization: `Bearer ${authStore.currentToken}`
},
body: fd
});
@@ -1,8 +1,8 @@
<script lang="ts">
import { authStore } from '$lib/stores/auth.svelte';
import { toast } from 'svelte-sonner';
import * as Modal from '$lib/components/ui/dialog';
import { Button } from '$lib/components/ui/button';
import { apiFetch } from '$lib/utils/api';
interface Props {
open: boolean;
@@ -28,11 +28,9 @@
if (!userId) return;
loading = true;
try {
const response = await fetch(`/api/admin/users/${userId}/patch-tests/eligible`, {
method: 'GET',
const response = await apiFetch(`/api/admin/users/${userId}/patch-tests/eligible`, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
}
});
@@ -63,11 +61,10 @@
const loadingToast = toast.loading('Adding patch test record...');
try {
const response = await fetch(`/api/admin/users/${userId}/patch-tests`, {
const response = await apiFetch(`/api/admin/users/${userId}/patch-tests`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify({
service_id: selectedServiceId
@@ -1,7 +1,7 @@
<script lang="ts">
import { authStore } from '$lib/stores/auth.svelte';
import { toast } from 'svelte-sonner';
import { Button } from '$lib/components/ui/button';
import { apiFetch } from '$lib/utils/api';
import * as Card from '$lib/components/ui/card';
import { Input } from '$lib/components/ui/input';
import { Checkbox } from '$lib/components/ui/checkbox';
@@ -105,9 +105,7 @@
async function fetchData() {
loading = true;
try {
const ptRes = await fetch('/api/admin/patch-tests', {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
});
const ptRes = await apiFetch('/api/admin/patch-tests');
if (ptRes.ok) {
patchTests = await ptRes.json();
@@ -117,9 +115,7 @@
// Only fetch services if not provided via prop
if (!servicesProp) {
const svcRes = await fetch('/api/admin/services', {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
});
const svcRes = await apiFetch('/api/admin/services');
if (svcRes.ok) {
const svcData = await svcRes.json();
const uniqueSvc = new SvelteMap<string, Service>();
@@ -205,11 +201,10 @@
};
try {
const res = await fetch(url, {
const res = await apiFetch(url, {
method,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
@@ -234,9 +229,8 @@
if (!confirm('Are you sure you want to delete this patch test requirement?')) return;
try {
const res = await fetch(`/api/admin/patch-tests/${id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${authStore.currentToken}` }
const res = await apiFetch(`/api/admin/patch-tests/${id}`, {
method: 'DELETE'
});
if (res.ok) {
@@ -1,7 +1,7 @@
<script lang="ts">
import { authStore } from '$lib/stores/auth.svelte';
import { SvelteDate, SvelteSet } from 'svelte/reactivity';
import { toast } from 'svelte-sonner';
import { apiFetch } from '$lib/utils/api';
import { CalendarDate, getLocalTimeZone, type DateValue } from '@internationalized/date';
import { Checkbox } from '$lib/components/ui/checkbox';
import PolicyPopover from '$lib/components/ui/policyPopover.svelte';
@@ -163,12 +163,8 @@
try {
const [whRes, ahRes] = await Promise.all([
fetch(`/api/scheduling/working-hours?start=${startStr}&end=${endStr}`, {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
}),
fetch(`/api/scheduling/available-hours?start=${startStr}&end=${endStr}`, {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
})
apiFetch(`/api/scheduling/working-hours?start=${startStr}&end=${endStr}`),
apiFetch(`/api/scheduling/available-hours?start=${startStr}&end=${endStr}`)
]);
if (whRes.ok && ahRes.ok) {
@@ -249,12 +245,8 @@
date.calendar.getDaysInMonth(date)
);
const [whRes, ahRes] = await Promise.all([
fetch(`/api/scheduling/working-hours?start=${startOfMonth}&end=${endOfMonth}`, {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
}),
fetch(`/api/scheduling/available-hours?start=${startOfMonth}&end=${endOfMonth}`, {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
})
apiFetch(`/api/scheduling/working-hours?start=${startOfMonth}&end=${endOfMonth}`),
apiFetch(`/api/scheduling/available-hours?start=${startOfMonth}&end=${endOfMonth}`)
]);
if (whRes.ok && ahRes.ok) {
const whData: WorkingHoursDay[] = await whRes.json();
@@ -317,11 +309,10 @@
const body: Record<string, unknown> = { start_time: newStartTime };
if (forgiveFees) body.forgive_fees = true;
if (forgiveNoShow) body.forgive_noshow = true;
const response = await fetch(`/api/admin/bookings/${booking.id}/reschedule`, {
const response = await apiFetch(`/api/admin/bookings/${booking.id}/reschedule`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
@@ -1,6 +1,6 @@
<script lang="ts">
import { authStore } from '$lib/stores/auth.svelte';
import { toast } from 'svelte-sonner';
import { apiFetch } from '$lib/utils/api';
// shadcn-svelte components
import { Button } from '$lib/components/ui/button';
@@ -154,11 +154,9 @@
servicesLoading = true;
try {
const response = await fetch('/api/admin/services', {
method: 'GET',
const response = await apiFetch('/api/admin/services', {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
}
});
@@ -184,11 +182,8 @@
async function toggleService(serviceId: string) {
servicesUpdating[serviceId] = true;
try {
const response = await fetch(`/api/admin/services/${serviceId}/toggle`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${authStore.currentToken}`
}
const response = await apiFetch(`/api/admin/services/${serviceId}/toggle`, {
method: 'PUT'
});
if (response.ok) {
@@ -214,11 +209,8 @@
servicesUpdating[serviceId] = true;
try {
const response = await fetch(`/api/admin/services/${serviceId}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${authStore.currentToken}`
}
const response = await apiFetch(`/api/admin/services/${serviceId}`, {
method: 'DELETE'
});
if (response.ok) {
@@ -262,11 +254,10 @@
minimum_age_required: Number(newService.minimum_age_required)
};
const response = await fetch('/api/admin/services', {
const response = await apiFetch('/api/admin/services', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
@@ -1,6 +1,6 @@
<script lang="ts">
import { authStore } from '$lib/stores/auth.svelte';
import { SvelteDate } from 'svelte/reactivity';
import { apiFetch } from '$lib/utils/api';
import { CalendarDate } from '@internationalized/date';
import { toast } from 'svelte-sonner';
import { sanitizeText } from '$lib/utils/toast-safe';
@@ -222,11 +222,9 @@
async function fetchBlockers() {
loading = true;
try {
const response = await fetch('/api/admin/time-blockers', {
method: 'GET',
const response = await apiFetch('/api/admin/time-blockers', {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
}
});
@@ -247,11 +245,9 @@
async function fetchDefaultHours() {
hoursLoading = true;
try {
const response = await fetch('/api/scheduling/default-hours', {
method: 'GET',
const response = await apiFetch('/api/scheduling/default-hours', {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
}
});
@@ -312,13 +308,11 @@
checkingOverlap = true;
try {
const dateParam = newStartDate;
const response = await fetch(
const response = await apiFetch(
`/api/admin/bookings/by-date-range?start=${encodeURIComponent(dateParam)}&end=${encodeURIComponent(dateParam)}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
}
}
);
@@ -409,11 +403,10 @@
if (overlappingBookings.length > 0) {
for (const booking of overlappingBookings) {
try {
await fetch('/api/admin/time-blockers', {
await apiFetch('/api/admin/time-blockers', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify({
start_time: startIso,
@@ -428,11 +421,10 @@
}
// Create the actual time blocker
const response = await fetch('/api/admin/time-blockers', {
const response = await apiFetch('/api/admin/time-blockers', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify({
start_time: startIso,
@@ -464,11 +456,8 @@
const loadingToast = toast.loading('Deleting time blocker...');
try {
const response = await fetch(`/api/admin/time-blockers/${blockerToDelete.id}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${authStore.currentToken}`
}
const response = await apiFetch(`/api/admin/time-blockers/${blockerToDelete.id}`, {
method: 'DELETE'
});
if (response.ok || response.status === 204) {
@@ -1,7 +1,7 @@
<script lang="ts">
import { authStore } from '$lib/stores/auth.svelte';
import { SvelteDate, SvelteURLSearchParams } from 'svelte/reactivity';
import { toast } from 'svelte-sonner';
import { apiFetch } from '$lib/utils/api';
import * as Modal from '$lib/components/ui/dialog';
import { Button } from '$lib/components/ui/button';
import PatchTestModal from './PatchTestModal.svelte';
@@ -104,11 +104,9 @@
if (!userId) return;
try {
const response = await fetch(`/api/admin/users/${userId}`, {
method: 'GET',
const response = await apiFetch(`/api/admin/users/${userId}`, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
}
});
@@ -127,11 +125,9 @@
async function fetchEligiblePatchTests() {
if (!userId) return;
try {
const response = await fetch(`/api/admin/users/${userId}/patch-tests/eligible`, {
method: 'GET',
const response = await apiFetch(`/api/admin/users/${userId}/patch-tests/eligible`, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
}
});
if (response.ok) {
@@ -163,11 +159,9 @@
params.set('cursor', cursor);
}
const response = await fetch(`/api/admin/bookings/user/${userId}?${params}`, {
method: 'GET',
const response = await apiFetch(`/api/admin/bookings/user/${userId}?${params}`, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
}
});
@@ -221,11 +215,9 @@
loadingRelationship = true;
try {
const response = await fetch(`/api/admin/users/${userId}/relationship`, {
method: 'GET',
const response = await apiFetch(`/api/admin/users/${userId}/relationship`, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
}
});
@@ -245,9 +237,7 @@
async function fetchGiftCardBalance() {
if (!userId) return;
try {
const res = await fetch(`/api/admin/users/${userId}/giftcard-balance`, {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
});
const res = await apiFetch(`/api/admin/users/${userId}/giftcard-balance`);
if (res.ok) {
const data = await res.json();
giftCardBalance = data.balance;
@@ -1,6 +1,6 @@
<script lang="ts">
import { authStore } from '$lib/stores/auth.svelte';
import { toast } from 'svelte-sonner';
import { apiFetch } from '$lib/utils/api';
import * as Card from '$lib/components/ui/card';
import { Input } from '$lib/components/ui/input';
import { Button } from '$lib/components/ui/button';
@@ -60,11 +60,9 @@
params.append('q', search.trim());
}
const response = await fetch(`/api/admin/users?${params}`, {
method: 'GET',
const response = await apiFetch(`/api/admin/users?${params}`, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
}
});
@@ -1,8 +1,8 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import WalkInCreateModal from '$lib/components/admin/WalkInCreateModal.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { CalendarDate } from '@internationalized/date';
import { apiFetch } from '$lib/utils/api';
import { SvelteDate } from 'svelte/reactivity';
import { onMount, onDestroy } from 'svelte';
import { toast } from 'svelte-sonner';
@@ -40,9 +40,7 @@
async function fetchShortestService() {
try {
const response = await fetch('/api/services', {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
});
const response = await apiFetch('/api/services');
if (response.ok) {
const services: Service[] = await response.json();
const durations = services.map((s) => s.duration_minutes).filter((d) => d > 0);
@@ -91,9 +89,8 @@
clearInterval(window.__walkInCountdownInterval);
}
try {
const res = await fetch('/api/admin/bookings/reserve', {
method: 'DELETE',
headers: { Authorization: `Bearer ${authStore.currentToken}` }
const res = await apiFetch('/api/admin/bookings/reserve', {
method: 'DELETE'
});
if (!res.ok && res.status !== 404) {
console.warn('Failed to release walk-in reservation', idToRelease, res.status);
@@ -185,9 +182,9 @@
const [y, m, d] = londonDateStr.split('-').map(Number);
const today = new CalendarDate(y, m, d);
const response = await fetch(`/api/scheduling/available-hours?start=${today}&end=${today}`, {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
});
const response = await apiFetch(
`/api/scheduling/available-hours?start=${today}&end=${today}`
);
if (!response.ok) {
noSlotsToday = true;
@@ -285,11 +282,10 @@
const start = new SvelteDate(y, m - 1, d, hours, minutes, 0, 0);
const startTimeISO = formatLocalDateTime(start);
const response = await fetch('/api/admin/bookings/reserve', {
const response = await apiFetch('/api/admin/bookings/reserve', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify({
user_id: null,
@@ -1,6 +1,6 @@
<script lang="ts">
import { authStore } from '$lib/stores/auth.svelte';
import { generateUUID } from '$lib/utils/uuid';
import { apiFetch } from '$lib/utils/api';
import { toast } from 'svelte-sonner';
import { SvelteDate, SvelteURLSearchParams } from 'svelte/reactivity';
import { isValidUKPhone, toE164UK } from '$lib/utils/phone';
@@ -281,11 +281,8 @@
async function fetchUsers() {
loadingUsers = true;
try {
const response = await fetch(
`/api/admin/users?page=1&per_page=4&q=${encodeURIComponent(userQuery)}`,
{
headers: { Authorization: `Bearer ${authStore.currentToken}` }
}
const response = await apiFetch(
`/api/admin/users?page=1&per_page=4&q=${encodeURIComponent(userQuery)}`
);
if (response.ok) {
const data = await response.json();
@@ -312,9 +309,7 @@
if (selectedUserId) {
url = `/api/services/eligible-for/${selectedUserId}`;
}
const response = await fetch(url, {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
});
const response = await apiFetch(url);
if (response.ok) {
services = await response.json();
}
@@ -356,9 +351,7 @@
} else {
params.set('popular', '3');
}
const response = await fetch(`/api/admin/custom-services?${params}`, {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
});
const response = await apiFetch(`/api/admin/custom-services?${params}`);
if (response.ok) {
const data = await response.json();
const list = data.services || data;
@@ -379,11 +372,10 @@
}
creatingCustomService = true;
try {
const response = await fetch('/api/admin/custom-services', {
const response = await apiFetch('/api/admin/custom-services', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: (newCustomService.name ?? '').trim(),
@@ -454,11 +446,10 @@
return;
}
const phone = toE164UK(guestPhone)!;
const createRes = await fetch('/api/users/guest', {
const createRes = await apiFetch('/api/users/guest', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify({
firstName: guestName.trim().split(' ')[0] || 'Walk-in',
@@ -549,12 +540,11 @@
notes: notes.trim() || null
};
const res = await fetch('/api/admin/bookings', {
const res = await apiFetch('/api/admin/bookings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
Authorization: `Bearer ${authStore.currentToken}`
'Idempotency-Key': idempotencyKey
},
body: JSON.stringify(payload)
});
@@ -1,7 +1,7 @@
<script lang="ts">
import { authStore } from '$lib/stores/auth.svelte';
import { toast } from 'svelte-sonner';
import { browser } from '$app/environment';
import { apiFetch } from '$lib/utils/api';
// shadcn-svelte components
import { Button } from '$lib/components/ui/button';
@@ -109,11 +109,9 @@
let error = null;
try {
const response = await fetch('/api/scheduling/default-hours', {
method: 'GET',
const response = await apiFetch('/api/scheduling/default-hours', {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
}
});
@@ -168,11 +166,10 @@
isOpen: hour.is_open
}));
const response = await fetch('/api/scheduling/default-hours', {
const response = await apiFetch('/api/scheduling/default-hours', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
@@ -22,6 +22,7 @@
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import { authStore } from '$lib/stores/auth.svelte';
import { apiFetch, getAuthHeaders } from '$lib/utils/api';
import { ensureBusinessInfo, getBusinessInfo } from '$lib/stores/businessInfo.svelte';
import { toast } from 'svelte-sonner';
import { onDestroy } from 'svelte';
@@ -207,9 +208,7 @@
}
try {
const response = await fetch('/api/user/profile', {
headers: authStore.currentToken ? { Authorization: `Bearer ${authStore.currentToken}` } : {}
});
const response = await apiFetch('/api/user/profile');
if (response.ok) {
const user = await response.json();
userDepositsRequired = user.deposits_required ?? 0;
@@ -229,9 +228,7 @@
_activeBookingCheckDone = false;
try {
// Check for pending bookings
const pendingResp = await fetch('/api/bookings?status=pending&perPage=1', {
headers: authStore.currentToken ? { Authorization: `Bearer ${authStore.currentToken}` } : {}
});
const pendingResp = await apiFetch('/api/bookings?status=pending&perPage=1');
if (pendingResp.ok) {
const data = await pendingResp.json();
if (data.bookings && data.bookings.length > 0) {
@@ -242,9 +239,7 @@
}
// Check for confirmed bookings
const confirmedResp = await fetch('/api/bookings?status=confirmed&perPage=1', {
headers: authStore.currentToken ? { Authorization: `Bearer ${authStore.currentToken}` } : {}
});
const confirmedResp = await apiFetch('/api/bookings?status=confirmed&perPage=1');
if (confirmedResp.ok) {
const data = await confirmedResp.json();
hasActiveBooking = data.bookings && data.bookings.length > 0;
@@ -270,11 +265,7 @@
async function fetchDiscountPreview() {
if (!confirmedBooking?.id) return;
try {
const resp = await fetch(`/api/bookings/${confirmedBooking.id}/discount-preview`, {
headers: authStore.currentToken
? { Authorization: `Bearer ${authStore.currentToken}` }
: undefined
});
const resp = await apiFetch(`/api/bookings/${confirmedBooking.id}/discount-preview`);
if (resp.ok) {
const data = await resp.json();
// Only show time-based (auto-apply) discounts on the confirmation screen
@@ -321,11 +312,11 @@
paymentAttempted = true;
const response = await fetch(`/api/bookings/${bookingId}/payment`, {
const response = await apiFetch(`/api/bookings/${bookingId}/payment`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(authStore.currentToken ? { Authorization: `Bearer ${authStore.currentToken}` } : {})
...getAuthHeaders()
},
body: JSON.stringify(body)
});
@@ -397,11 +388,11 @@
const startTimeISO = formatLocalDateTime(bookingDate);
const serviceIds = selectedServices.map((s) => s.id);
const response = await fetch('/api/bookings/reserve', {
const response = await apiFetch('/api/bookings/reserve', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(authStore.currentToken ? { Authorization: `Bearer ${authStore.currentToken}` } : {})
...getAuthHeaders()
},
body: JSON.stringify({ start_time: startTimeISO, service_ids: serviceIds })
});
@@ -453,10 +444,7 @@
window.__bookingFlowCountdownInterval = null;
}
try {
const res = await fetch('/api/bookings/reserve', {
method: 'DELETE',
headers: authStore.currentToken ? { Authorization: `Bearer ${authStore.currentToken}` } : {}
});
const res = await apiFetch('/api/bookings/reserve', { method: 'DELETE' });
if (!res.ok) console.warn('Failed to release reservation', idToRelease, res.status);
} catch (e) {
console.warn('Error releasing reservation', idToRelease, e);
@@ -529,13 +517,7 @@
async function fetchServices() {
servicesLoading = true;
try {
const response = await fetch('/api/services', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
...(authStore.currentToken ? { Authorization: `Bearer ${authStore.currentToken}` } : {})
}
});
const response = await apiFetch('/api/services');
if (response.ok) {
const data: Service[] = await response.json();
@@ -729,16 +711,9 @@
loadingAvailableHours = true;
try {
const authHeaders: RequestInit['headers'] = authStore.currentToken
? { Authorization: `Bearer ${authStore.currentToken}` }
: undefined;
const [whRes, ahRes] = await Promise.all([
fetch(`/api/scheduling/working-hours?start=${startStr}&end=${endStr}`, {
headers: authHeaders
}),
fetch(`/api/scheduling/available-hours?start=${startStr}&end=${endStr}`, {
headers: authHeaders
})
apiFetch(`/api/scheduling/working-hours?start=${startStr}&end=${endStr}`),
apiFetch(`/api/scheduling/available-hours?start=${startStr}&end=${endStr}`)
]);
if (!whRes.ok || !ahRes.ok) {
throw new Error(`HTTP error! wh: ${whRes.status}, ah: ${ahRes.status}`);
@@ -829,14 +804,9 @@
const startStr = startOfMonth.toString();
const endStr = endOfMonth.toString();
const authHeaders: RequestInit['headers'] = authStore.currentToken
? { Authorization: `Bearer ${authStore.currentToken}` }
: undefined;
// Fetch working hours
const workingHoursResponse = await fetch(
`/api/scheduling/working-hours?start=${startStr}&end=${endStr}`,
{ headers: authHeaders }
const workingHoursResponse = await apiFetch(
`/api/scheduling/working-hours?start=${startStr}&end=${endStr}`
);
if (!workingHoursResponse.ok) {
throw new Error(`HTTP error! status: ${workingHoursResponse.status}`);
@@ -860,9 +830,8 @@
workingHours = { ...workingHours, ...workingHoursMap };
// Fetch available hours
const availableHoursResponse = await fetch(
`/api/scheduling/available-hours?start=${startStr}&end=${endStr}`,
{ headers: authHeaders }
const availableHoursResponse = await apiFetch(
`/api/scheduling/available-hours?start=${startStr}&end=${endStr}`
);
if (!availableHoursResponse.ok) {
throw new Error(`HTTP error! status: ${availableHoursResponse.status}`);
@@ -1484,17 +1453,13 @@
requestBody.user_id = guestUserId;
}
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey
};
if (authStore.currentToken) {
headers['Authorization'] = `Bearer ${authStore.currentToken}`;
}
const response = await fetch('/api/bookings', {
const response = await apiFetch('/api/bookings', {
method: 'POST',
headers,
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
...getAuthHeaders()
},
body: JSON.stringify(requestBody)
});
@@ -4,6 +4,7 @@
import { navigating, page } from '$app/stores';
import { resolve } from '$app/paths';
import { authStore } from '$lib/stores/auth.svelte';
import { apiFetch } from '$lib/utils/api';
import { Button } from '$lib/components/ui/button';
import { Skeleton } from '$lib/components/ui/skeleton';
@@ -62,9 +63,7 @@
async function fetchUnreadCount() {
if (!authStore.isAuthenticated || !authStore.currentToken) return;
try {
const res = await fetch('/api/admin/notifications/unread-count', {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
});
const res = await apiFetch('/api/admin/notifications/unread-count');
if (res.ok) {
const data = await res.json();
unreadCount = data.count;
@@ -5,7 +5,7 @@
import { Input } from '$lib/components/ui/input';
import { Checkbox } from '$lib/components/ui/checkbox';
import type { Booking, BookingService, BookingDiscount } from '$lib/types/booking';
import { authStore } from '$lib/stores/auth.svelte';
import { apiFetch } from '$lib/utils/api';
const LOYALTY_DISCOUNT_RATE = 0.1;
@@ -80,9 +80,7 @@
if (!targetUserId) return;
_loadingCustomerBalance = true;
try {
const res = await fetch(`/api/admin/users/${targetUserId}/giftcard-balance`, {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
});
const res = await apiFetch(`/api/admin/users/${targetUserId}/giftcard-balance`);
if (res.ok) {
const data = await res.json();
customerBalance = data.balance;
@@ -103,9 +101,7 @@
if (!targetUserId) return;
_loadingSavedCardList = true;
try {
const res = await fetch(`/api/admin/users/${targetUserId}/payment-methods`, {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
});
const res = await apiFetch(`/api/admin/users/${targetUserId}/payment-methods`);
if (res.ok) {
savedCardList = await res.json();
}
@@ -248,12 +244,9 @@
async function applyLoyaltyRedemption(): Promise<void> {
if (!useLoyalty) return;
const res = await fetch(`/api/admin/bookings/${booking.id}/apply-redemption`, {
const res = await apiFetch(`/api/admin/bookings/${booking.id}/apply-redemption`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
}
headers: { 'Content-Type': 'application/json' }
});
if (!res.ok) {
const errData = await res.text();
@@ -275,12 +268,9 @@
try {
await applyLoyaltyRedemption();
const response = await fetch(`/api/admin/bookings/${booking.id}/payment`, {
const response = await apiFetch(`/api/admin/bookings/${booking.id}/payment`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
amount: Math.round(finalAmount * 100) - loyaltyDiscount,
payment_type: 'full',
@@ -309,14 +299,10 @@
pollingInterval = setInterval(async () => {
try {
const response = await fetch(
const response = await apiFetch(
`/api/admin/payments/${checkoutId}/status?booking_id=${booking.id}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
}
headers: { 'Content-Type': 'application/json' }
}
);
@@ -427,12 +413,9 @@
body.tip_amount = Math.round(tipAmount * 100);
}
const response = await fetch(`/api/admin/bookings/${booking.id}/payment`, {
const response = await apiFetch(`/api/admin/bookings/${booking.id}/payment`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
@@ -557,12 +540,9 @@
body.gift_card_id = giftCardId.replace(/-/g, '');
}
const response = await fetch(`/api/admin/bookings/${booking.id}/payment`, {
const response = await apiFetch(`/api/admin/bookings/${booking.id}/payment`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
@@ -607,9 +587,7 @@
savedCards = [];
selectedSavedCardId = null;
try {
const res = await fetch(`/api/admin/users/${booking.user_id}/payment-methods`, {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
});
const res = await apiFetch(`/api/admin/users/${booking.user_id}/payment-methods`);
if (res.ok) {
savedCards = await res.json();
}
@@ -632,12 +610,9 @@
try {
await applyLoyaltyRedemption();
const response = await fetch(`/api/admin/bookings/${booking.id}/payment`, {
const response = await apiFetch(`/api/admin/bookings/${booking.id}/payment`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
amount: Math.round(totalDue * 100) - loyaltyDiscount,
payment_type: 'full',
@@ -4,7 +4,7 @@
import * as Dialog from '$lib/components/ui/dialog';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { authStore } from '$lib/stores/auth.svelte';
import { apiFetch } from '$lib/utils/api';
import { formatCardCode } from '$lib/utils/format';
import { formatUserName } from '$lib/utils/nameDisplay';
@@ -313,9 +313,7 @@
loadingCurrentCustomer = true;
currentCustomerInfo = null;
try {
const res = await fetch('/api/admin/today/current-next', {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
});
const res = await apiFetch('/api/admin/today/current-next');
if (res.ok) {
const data = await res.json();
const appointment = data.current || data.next;
@@ -359,11 +357,8 @@
loadingUsers = true;
users = [];
try {
const res = await fetch(
`/api/admin/users?page=${page}&per_page=5&q=${encodeURIComponent(userQuery)}`,
{
headers: { Authorization: `Bearer ${authStore.currentToken}` }
}
const res = await apiFetch(
`/api/admin/users?page=${page}&per_page=5&q=${encodeURIComponent(userQuery)}`
);
if (res.ok) {
const data = await res.json();
@@ -396,12 +391,9 @@
const email = guestEmail || `till-${timestamp}@guest.invalid`;
const phone = '07451234567';
const res = await fetch('/api/users/guest', {
const res = await apiFetch('/api/users/guest', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
firstName,
lastName,
@@ -461,12 +453,9 @@
if (selectedCustomer) body.user_id = selectedCustomer.id;
if (delivery === 'account' && selectedCustomer) body.redeem_to_user_id = selectedCustomer.id;
const res = await fetch('/api/admin/till/sale', {
const res = await apiFetch('/api/admin/till/sale', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (res.ok) {
@@ -497,12 +486,9 @@
if (selectedCustomer) body.user_id = selectedCustomer.id;
if (delivery === 'account' && selectedCustomer) body.redeem_to_user_id = selectedCustomer.id;
const res = await fetch('/api/admin/till/sale', {
const res = await apiFetch('/api/admin/till/sale', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (res.ok) {
@@ -534,9 +520,7 @@
await new Promise((r) => setTimeout(r, 2000));
attempts++;
try {
const res = await fetch(`/api/admin/till/sale/checkout/${ckId}/status`, {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
});
const res = await apiFetch(`/api/admin/till/sale/checkout/${ckId}/status`);
if (res.ok) {
const data = await res.json();
if (data.status === 'COMPLETED') {
@@ -587,12 +571,9 @@
if (selectedCustomer) body.user_id = selectedCustomer.id;
if (delivery === 'account' && selectedCustomer) body.redeem_to_user_id = selectedCustomer.id;
const res = await fetch('/api/admin/till/sale', {
const res = await apiFetch('/api/admin/till/sale', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (res.ok) {
@@ -618,9 +599,7 @@
loadingSavedCards = true;
savedCardList = [];
try {
const res = await fetch(`/api/admin/users/${selectedCustomer.id}/payment-methods`, {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
});
const res = await apiFetch(`/api/admin/users/${selectedCustomer.id}/payment-methods`);
if (res.ok) {
const data = await res.json();
savedCardList = data.payment_methods || data || [];
@@ -648,12 +627,9 @@
if (selectedCustomer) body.user_id = selectedCustomer.id;
if (delivery === 'account' && selectedCustomer) body.redeem_to_user_id = selectedCustomer.id;
const res = await fetch('/api/admin/till/sale', {
const res = await apiFetch('/api/admin/till/sale', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (res.ok) {
@@ -11,6 +11,7 @@
import CardInput from '$lib/components/payments/CardInput.svelte';
import PolicyPopover from '$lib/components/ui/policyPopover.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { apiFetch } from '$lib/utils/api';
const LOYALTY_DISCOUNT_RATE = 0.1;
@@ -351,11 +352,8 @@
async function acquireLock() {
try {
const response = await fetch(`/api/bookings/${booking.id}/payment-lock`, {
method: 'POST',
headers: {
Authorization: `Bearer ${authStore.currentToken}`
}
const response = await apiFetch(`/api/bookings/${booking.id}/payment-lock`, {
method: 'POST'
});
if (response.ok) {
lockAcquired = true;
@@ -370,11 +368,8 @@
lockAcquired = false;
clearLockIntervals();
try {
await fetch(`/api/bookings/${booking.id}/payment-lock`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${authStore.currentToken}`
}
await apiFetch(`/api/bookings/${booking.id}/payment-lock`, {
method: 'DELETE'
});
} catch (_err) {
console.error('Failed to release payment lock:', _err);
@@ -390,11 +385,8 @@
function startRenewal() {
lockInterval = setInterval(async () => {
try {
const response = await fetch(`/api/bookings/${booking.id}/payment-lock`, {
method: 'POST',
headers: {
Authorization: `Bearer ${authStore.currentToken}`
}
const response = await apiFetch(`/api/bookings/${booking.id}/payment-lock`, {
method: 'POST'
});
if (response.ok) {
lockTimer = 300;
@@ -428,11 +420,7 @@
if (!authStore.isAuthenticated) return;
paymentMethodsLoading = true;
try {
const response = await fetch('/api/user/payment-methods', {
headers: {
Authorization: `Bearer ${authStore.currentToken}`
}
});
const response = await apiFetch('/api/user/payment-methods');
if (response.ok) {
paymentMethods = await response.json();
}
@@ -446,11 +434,7 @@
async function fetchLoyaltyData() {
if (!authStore.isAuthenticated) return;
try {
const response = await fetch('/api/user/loyalty', {
headers: {
Authorization: `Bearer ${authStore.currentToken}`
}
});
const response = await apiFetch('/api/user/loyalty');
if (response.ok) {
const data = await response.json();
stamps = data.stamps ?? 0;
@@ -498,12 +482,9 @@
// Apply loyalty redemption before payment
if (useLoyalty) {
try {
const redemptionResponse = await fetch(`/api/bookings/${booking.id}/apply-redemption`, {
const redemptionResponse = await apiFetch(`/api/bookings/${booking.id}/apply-redemption`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
}
headers: { 'Content-Type': 'application/json' }
});
if (!redemptionResponse.ok) {
const errData = await redemptionResponse.text();
@@ -537,12 +518,9 @@
const idempotencyKey = generateIdempotencyKey();
try {
const response = await fetch(`/api/bookings/${booking.id}/payment`, {
const response = await apiFetch(`/api/bookings/${booking.id}/payment`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
amount: amountCents,
payment_type: paymentType,
@@ -640,9 +618,7 @@
// Fetch eligible campaign discounts
try {
const resp = await fetch(`/api/bookings/${booking.id}/discount-preview`, {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
});
const resp = await apiFetch(`/api/bookings/${booking.id}/discount-preview`);
if (resp.ok) {
discountPreview = await resp.json();
}
@@ -1,5 +1,5 @@
<script lang="ts">
import { authStore } from '$lib/stores/auth.svelte';
import { apiFetch } from '$lib/utils/api';
import { ensureBusinessInfo, getBusinessInfo } from '$lib/stores/businessInfo.svelte';
import { SvelteDate } from 'svelte/reactivity';
import { toast } from 'svelte-sonner';
@@ -179,12 +179,8 @@
async function fetchCurrentAndNext() {
loading = true;
try {
const response = await fetch('/api/admin/today/current-next', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
}
const response = await apiFetch('/api/admin/today/current-next', {
headers: { 'Content-Type': 'application/json' }
});
if (response.ok) {
@@ -1,5 +1,5 @@
<script lang="ts">
import { authStore } from '$lib/stores/auth.svelte';
import { apiFetch } from '$lib/utils/api';
import { SvelteDate } from 'svelte/reactivity';
import { toast } from 'svelte-sonner';
import * as Card from '$lib/components/ui/card';
@@ -178,12 +178,8 @@
async function fetchPendingApprovals() {
if (!initialized) loading = true;
try {
const response = await fetch('/api/admin/today/pending-approvals', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
}
const response = await apiFetch('/api/admin/today/pending-approvals', {
headers: { 'Content-Type': 'application/json' }
});
if (response.ok) {
@@ -213,12 +209,8 @@
async function fetchEditRequests() {
try {
const response = await fetch('/api/admin/bookings/edit-requests', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
}
const response = await apiFetch('/api/admin/bookings/edit-requests', {
headers: { 'Content-Type': 'application/json' }
});
if (response.ok) {
@@ -240,11 +232,7 @@
async function openApprovalModal(bookingId: string) {
try {
const response = await fetch(`/api/admin/bookings/${bookingId}`, {
headers: {
Authorization: `Bearer ${authStore.currentToken}`
}
});
const response = await apiFetch(`/api/admin/bookings/${bookingId}`);
if (!response.ok) throw new Error(await response.text());
const data = await response.json();
@@ -1,5 +1,5 @@
<script lang="ts">
import { authStore } from '$lib/stores/auth.svelte';
import { apiFetch } from '$lib/utils/api';
import { SvelteDate } from 'svelte/reactivity';
import { CalendarDate } from '@internationalized/date';
import { toast } from 'svelte-sonner';
@@ -345,12 +345,8 @@
async function fetchTodayAppointments() {
if (!apptsInitialized) loading = true;
try {
const response = await fetch('/api/admin/today/appointments', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
}
const response = await apiFetch('/api/admin/today/appointments', {
headers: { 'Content-Type': 'application/json' }
});
if (response.ok) {
@@ -383,31 +379,22 @@
try {
const dateParam = today;
const [blockersRes, whRes, ahRes] = await Promise.all([
fetch(
apiFetch(
`/api/admin/time-blockers?start=${encodeURIComponent(dateParam)}&end=${encodeURIComponent(dateParam)}`,
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
}
headers: { 'Content-Type': 'application/json' }
}
),
fetch(
apiFetch(
`/api/scheduling/working-hours?start=${encodeURIComponent(weekStartStr)}&end=${encodeURIComponent(weekEndStr)}`,
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
}
headers: { 'Content-Type': 'application/json' }
}
),
fetch(
apiFetch(
`/api/scheduling/available-hours?start=${encodeURIComponent(weekStartStr)}&end=${encodeURIComponent(weekEndStr)}`,
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
}
headers: { 'Content-Type': 'application/json' }
}
)
]);
@@ -464,13 +451,10 @@
}
checkingOverlap = true;
try {
const response = await fetch(
const response = await apiFetch(
`/api/admin/bookings/by-date-range?start=${encodeURIComponent(today)}&end=${encodeURIComponent(today)}`,
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
}
headers: { 'Content-Type': 'application/json' }
}
);
if (response.ok) {
@@ -525,12 +509,9 @@
creating = true;
const loadingToast = toast.loading('Creating time blocker...');
try {
const response = await fetch('/api/admin/time-blockers', {
const response = await apiFetch('/api/admin/time-blockers', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
start_time: startIso,
duration_minutes: durationMinutes,
@@ -558,9 +539,8 @@
if (!blockerToDelete) return;
const loadingToast = toast.loading('Deleting time blocker...');
try {
const response = await fetch(`/api/admin/time-blockers/${blockerToDelete.id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${authStore.currentToken}` }
const response = await apiFetch(`/api/admin/time-blockers/${blockerToDelete.id}`, {
method: 'DELETE'
});
if (response.ok || response.status === 204) {
toast.success('Time blocker deleted', { id: loadingToast });
@@ -1,6 +1,6 @@
<script lang="ts">
import { SvelteDate } from 'svelte/reactivity';
import { authStore } from '$lib/stores/auth.svelte';
import { apiFetch } from '$lib/utils/api';
import { Skeleton } from '$lib/components/ui/skeleton';
import * as Card from '$lib/components/ui/card';
import { formatLocalDateTime } from '$lib/utils/timeSlots';
@@ -58,13 +58,10 @@
d.setDate(d.getDate() - i);
const dateStr = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
const res = await fetch(
const res = await apiFetch(
`/api/scheduling/working-hours?start=${encodeURIComponent(dateStr)}&end=${encodeURIComponent(dateStr)}`,
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
}
headers: { 'Content-Type': 'application/json' }
}
);
@@ -94,19 +91,13 @@
try {
const dateParam = today;
const [apptsRes, whRes] = await Promise.all([
fetch(`/api/admin/today/appointments`, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
}
apiFetch(`/api/admin/today/appointments`, {
headers: { 'Content-Type': 'application/json' }
}),
fetch(
apiFetch(
`/api/scheduling/working-hours?start=${encodeURIComponent(dateParam)}&end=${encodeURIComponent(dateParam)}`,
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
}
headers: { 'Content-Type': 'application/json' }
}
)
]);
@@ -155,13 +146,10 @@
const nowISO = formatLocalDateTime(new SvelteDate());
let bookingsMade = 0;
try {
const bmRes = await fetch(
const bmRes = await apiFetch(
`/api/admin/bookings/by-created-range?start=${encodeURIComponent(cutoff)}&end=${encodeURIComponent(nowISO)}`,
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
}
headers: { 'Content-Type': 'application/json' }
}
);
if (bmRes.ok) {
+6 -7
View File
@@ -1,3 +1,5 @@
import { apiFetch } from '$lib/utils/api';
export type SavedCard = {
id: string;
brand: string;
@@ -13,14 +15,11 @@ function createSavedCardsStore() {
let loading = $state(false);
let loaded = $state(false);
async function fetch() {
async function load() {
if (loading) return;
loading = true;
try {
const headers: Record<string, string> = {};
const token = globalThis.localStorage?.getItem('authToken');
if (token) headers['Authorization'] = `Bearer ${token}`;
const res = await globalThis.fetch('/api/user/payment-methods', { headers });
const res = await apiFetch('/api/user/payment-methods');
if (res.ok) {
cards = await res.json();
} else {
@@ -36,7 +35,7 @@ function createSavedCardsStore() {
function invalidate() {
loaded = false;
return fetch();
return load();
}
return {
@@ -49,7 +48,7 @@ function createSavedCardsStore() {
get loaded() {
return loaded;
},
fetch,
fetch: load,
invalidate
};
}
+42
View File
@@ -0,0 +1,42 @@
import { authStore } from '$lib/stores/auth.svelte';
/**
* Returns an Authorization header object if the user is authenticated, or undefined otherwise.
*
* Can be spread directly into a fetch() headers object:
* fetch('/api/...', { headers: { 'Content-Type': 'application/json', ...getAuthHeaders() } })
*
* Or used standalone for auth-only headers:
* fetch('/api/...', { headers: getAuthHeaders() })
*
* Spreading `undefined` is a safe no-op in modern JS environments.
*/
export function getAuthHeaders(): { Authorization: string } | undefined {
const token = authStore.currentToken;
return token ? { Authorization: `Bearer ${token}` } : undefined;
}
/**
* Drop-in wrapper around fetch() that automatically includes the auth token
* when available. Preserves any headers, method, body, etc. passed in `init`.
*
* Use this for all authenticated API calls. Public endpoints should continue
* using bare fetch() to avoid sending an unnecessary Authorization header.
*
* @example
* const res = await apiFetch('/api/user/profile');
* const data = await apiFetch('/api/bookings/reserve', {
* method: 'POST',
* body: JSON.stringify({ start_time, service_ids }),
* });
*/
export async function apiFetch(url: string | URL | Request, init?: RequestInit): Promise<Response> {
const headers = new Headers(init?.headers);
const authHeaders = getAuthHeaders();
if (authHeaders) {
headers.set('Authorization', authHeaders.Authorization);
}
return fetch(url, { ...init, headers });
}