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:
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -30,6 +30,10 @@
|
||||
});
|
||||
|
||||
// Verify email address
|
||||
// TODO: `/api/verify-email` doesn't exist in the backend. The correct endpoints are
|
||||
// `POST /api/verify/generate` (send verification code) and `POST /api/verify/check` (verify code).
|
||||
// This call silently 404s. Fix required — either add the missing backend route or refactor
|
||||
// to use `/api/verify/generate` + `/api/verify/check`.
|
||||
async function verify_email() {
|
||||
const loadingToast = toast.loading('Verifying email address...');
|
||||
const response = await fetch('/api/verify-email', {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { sanitizeText } from '$lib/utils/toast-safe';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import UserBookingModal from '$lib/components/account/UserBookingModal.svelte';
|
||||
import { isValidUKPhone, formatPhoneDisplay, toE164UK } from '$lib/utils/phone';
|
||||
import { savedCardsStore, type SavedCard } from '$lib/stores/savedCards.svelte';
|
||||
@@ -134,9 +135,8 @@
|
||||
|
||||
async function deleteCard(card: SavedCard) {
|
||||
try {
|
||||
const res = await fetch(`/api/user/payment-methods/${card.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${authStore.currentToken}` }
|
||||
const res = await apiFetch(`/api/user/payment-methods/${card.id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (res.ok) {
|
||||
toast.success('Card removed');
|
||||
@@ -259,9 +259,7 @@
|
||||
async function fetchGiftCardBalance() {
|
||||
loadingBalance = true;
|
||||
try {
|
||||
const res = await fetch('/api/user/giftcards/balance', {
|
||||
headers: { Authorization: `Bearer ${authStore.currentToken}` }
|
||||
});
|
||||
const res = await apiFetch('/api/user/giftcards/balance');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
giftCardBalance = data.balance;
|
||||
@@ -280,12 +278,9 @@
|
||||
}
|
||||
redeemingGiftCard = true;
|
||||
try {
|
||||
const res = await fetch('/api/user/giftcards/redeem', {
|
||||
const res = await apiFetch('/api/user/giftcards/redeem', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ code: giftCardCode })
|
||||
});
|
||||
if (res.ok) {
|
||||
@@ -334,12 +329,9 @@
|
||||
|
||||
const idempotencyKey = generateIdempotencyKey();
|
||||
|
||||
const res = await fetch('/api/user/giftcards/buy', {
|
||||
const res = await apiFetch('/api/user/giftcards/buy', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
amount: buyAmount * 100, // cents
|
||||
recipient_type: buyRecipientType,
|
||||
@@ -565,12 +557,9 @@
|
||||
}
|
||||
addingCard = true;
|
||||
try {
|
||||
const res = await fetch('/api/user/payment-methods', {
|
||||
const res = await apiFetch('/api/user/payment-methods', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
card_number: cardNum,
|
||||
expiry: newCardExpiry,
|
||||
@@ -598,9 +587,7 @@
|
||||
|
||||
async function fetchNotifPrefs() {
|
||||
try {
|
||||
const res = await fetch('/api/user/notification-preferences', {
|
||||
headers: { Authorization: `Bearer ${authStore.currentToken}` }
|
||||
});
|
||||
const res = await apiFetch('/api/user/notification-preferences');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
notifPrefs.emailEnabled = data.emailEnabled ?? true;
|
||||
@@ -614,12 +601,9 @@
|
||||
|
||||
async function saveNotifPrefs() {
|
||||
try {
|
||||
await fetch('/api/user/notification-preferences', {
|
||||
await apiFetch('/api/user/notification-preferences', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(notifPrefs)
|
||||
});
|
||||
} catch {
|
||||
@@ -700,11 +684,8 @@
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', blob, 'profile.jpg');
|
||||
const uploadResponse = await fetch('/api/user/profile-picture', {
|
||||
const uploadResponse = await apiFetch('/api/user/profile-picture', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
if (uploadResponse.ok) {
|
||||
@@ -765,12 +746,9 @@
|
||||
const loadingToast = toast.loading('Updating phone number...');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/user/profile', {
|
||||
const response = await apiFetch('/api/user/profile', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
firstName: userData?.firstName,
|
||||
lastName: userData?.lastName,
|
||||
@@ -868,12 +846,9 @@
|
||||
const loadingToast = toast.loading('Updating name...');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/user/profile', {
|
||||
const response = await apiFetch('/api/user/profile', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
firstName: newFirstName,
|
||||
lastName: newLastName,
|
||||
@@ -947,9 +922,7 @@
|
||||
|
||||
async function fetchGdprExportStatus() {
|
||||
try {
|
||||
const res = await fetch('/api/user/gdpr-export', {
|
||||
headers: { Authorization: `Bearer ${authStore.currentToken}` }
|
||||
});
|
||||
const res = await apiFetch('/api/user/gdpr-export');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data.export_metadata?.exported_at) {
|
||||
@@ -975,12 +948,9 @@
|
||||
|
||||
loadingUser = true;
|
||||
try {
|
||||
const response = await fetch('/api/user/profile', {
|
||||
const response = await apiFetch('/api/user/profile', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
}
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
@@ -1017,12 +987,9 @@
|
||||
const today = new Date().toLocaleDateString('en-CA', { timeZone: 'Europe/London' });
|
||||
|
||||
// Fetch more items (e.g. 10) to ensure we find upcoming ones even if the first few are past
|
||||
const response = await fetch(`/api/bookings?start_date=${today}&per_page=10&page=1`, {
|
||||
const response = await apiFetch(`/api/bookings?start_date=${today}&per_page=10&page=1`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
}
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -1059,12 +1026,9 @@
|
||||
loadingPast = true;
|
||||
try {
|
||||
const today = new Date().toLocaleDateString('en-CA', { timeZone: 'Europe/London' });
|
||||
const response = await fetch(`/api/bookings?end_date=${today}&per_page=10&page=${page}`, {
|
||||
const response = await apiFetch(`/api/bookings?end_date=${today}&per_page=10&page=${page}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
}
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -1175,12 +1139,9 @@
|
||||
const loadingToast = toast.loading('Changing password...');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/user/change-password', {
|
||||
const response = await apiFetch('/api/user/change-password', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
current_password: passwordData.current,
|
||||
new_password: passwordData.new
|
||||
@@ -1228,11 +1189,8 @@
|
||||
const loadingToast = toast.loading('Deleting account...');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/user/account', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
}
|
||||
const response = await apiFetch('/api/user/account', {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
@@ -1291,6 +1249,10 @@
|
||||
<svelte:head>
|
||||
<script>
|
||||
(function () {
|
||||
// Pre-hydration auth guard: reads localStorage directly because the Svelte
|
||||
// authStore hasn't initialized yet at this point (async+$state). This runs
|
||||
// synchronously in <svelte:head> before any rendering, preventing a flash
|
||||
// of protected content. The authStore handles post-hydration auth.
|
||||
try {
|
||||
var token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { browser } from '$app/environment';
|
||||
import { Skeleton } from '$lib/components/ui/skeleton';
|
||||
|
||||
@@ -122,14 +123,9 @@
|
||||
async function loadSharedData() {
|
||||
try {
|
||||
const [hoursRes, servicesRes] = await Promise.all([
|
||||
fetch('/api/scheduling/default-hours', {
|
||||
headers: { Authorization: `Bearer ${authStore.currentToken}` }
|
||||
}),
|
||||
fetch('/api/admin/services', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
}
|
||||
apiFetch('/api/scheduling/default-hours'),
|
||||
apiFetch('/api/admin/services', {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -165,6 +161,10 @@
|
||||
<svelte:head>
|
||||
<script>
|
||||
(function () {
|
||||
// Pre-hydration auth guard: reads localStorage directly because the Svelte
|
||||
// authStore hasn't initialized yet at this point (async+$state). This runs
|
||||
// synchronously in <svelte:head> before any rendering, preventing a flash
|
||||
// of protected content. The authStore handles post-hydration auth.
|
||||
try {
|
||||
var token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
@@ -340,11 +340,8 @@
|
||||
onRefresh={async () => {
|
||||
if (!browser) return;
|
||||
try {
|
||||
const r = await fetch('/api/admin/services', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
}
|
||||
const r = await apiFetch('/api/admin/services', {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
if (r.ok) {
|
||||
services = await r.json();
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { browser } from '$app/environment';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { Skeleton } from '$lib/components/ui/skeleton';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||
@@ -138,9 +139,7 @@
|
||||
per_page: perPage.toString(),
|
||||
include_acknowledged: includeAcknowledged.toString()
|
||||
});
|
||||
const response = await fetch(`/api/admin/notifications?${params}`, {
|
||||
headers: { Authorization: `Bearer ${authStore.currentToken}` }
|
||||
});
|
||||
const response = await apiFetch(`/api/admin/notifications?${params}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed: ${response.status}`);
|
||||
}
|
||||
@@ -155,21 +154,16 @@
|
||||
}
|
||||
|
||||
async function fetchBookingDetails(bookingId: string): Promise<Booking | null> {
|
||||
const response = await fetch(`/api/admin/bookings/${bookingId}`, {
|
||||
headers: { Authorization: `Bearer ${authStore.currentToken}` }
|
||||
});
|
||||
const response = await apiFetch(`/api/admin/bookings/${bookingId}`);
|
||||
if (!response.ok) return null;
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function handleAction(notification: Notification) {
|
||||
if (!notification.acknowledged_at) {
|
||||
await fetch(`/api/admin/notifications/${notification.id}/acknowledge`, {
|
||||
await apiFetch(`/api/admin/notifications/${notification.id}/acknowledge`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
}
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -192,11 +186,8 @@
|
||||
}
|
||||
} else if (action === 'edit_approve' && notification.booking_id) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/admin/bookings/${notification.booking_id}/edit-request`,
|
||||
{
|
||||
headers: { Authorization: `Bearer ${authStore.currentToken}` }
|
||||
}
|
||||
const response = await apiFetch(
|
||||
`/api/admin/bookings/${notification.booking_id}/edit-request`
|
||||
);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
@@ -219,12 +210,9 @@
|
||||
|
||||
async function handleAcknowledge(notification: Notification) {
|
||||
try {
|
||||
const response = await fetch(`/api/admin/notifications/${notification.id}/acknowledge`, {
|
||||
const response = await apiFetch(`/api/admin/notifications/${notification.id}/acknowledge`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
}
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
if (!response.ok) {
|
||||
toast.error('Failed to acknowledge notification');
|
||||
@@ -351,6 +339,10 @@
|
||||
<svelte:head>
|
||||
<script>
|
||||
(function () {
|
||||
// Pre-hydration auth guard: reads localStorage directly because the Svelte
|
||||
// authStore hasn't initialized yet at this point (async+$state). This runs
|
||||
// synchronously in <svelte:head> before any rendering, preventing a flash
|
||||
// of protected content. The authStore handles post-hydration auth.
|
||||
try {
|
||||
var token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// ============================================================
|
||||
import { goto } from '$app/navigation';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { browser } from '$app/environment';
|
||||
import { Skeleton } from '$lib/components/ui/skeleton';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
@@ -230,15 +231,9 @@
|
||||
const endStr = formatDate(end);
|
||||
|
||||
const [bookingsRes, whRes, blockersRes] = await Promise.all([
|
||||
fetch(`/api/admin/bookings?start_date=${startStr}&end_date=${endStr}&per_page=500`, {
|
||||
headers: { Authorization: `Bearer ${authStore.currentToken}` }
|
||||
}),
|
||||
fetch(`/api/scheduling/working-hours?start=${startStr}&end=${endStr}`, {
|
||||
headers: { Authorization: `Bearer ${authStore.currentToken}` }
|
||||
}),
|
||||
fetch(`/api/admin/time-blockers?start=${startStr}&end=${endStr}`, {
|
||||
headers: { Authorization: `Bearer ${authStore.currentToken}` }
|
||||
})
|
||||
apiFetch(`/api/admin/bookings?start_date=${startStr}&end_date=${endStr}&per_page=500`),
|
||||
apiFetch(`/api/scheduling/working-hours?start=${startStr}&end=${endStr}`),
|
||||
apiFetch(`/api/admin/time-blockers?start=${startStr}&end=${endStr}`)
|
||||
]);
|
||||
|
||||
if (bookingsRes.ok) {
|
||||
@@ -445,6 +440,10 @@
|
||||
<svelte:head>
|
||||
<script>
|
||||
(function () {
|
||||
// Pre-hydration auth guard: reads localStorage directly because the Svelte
|
||||
// authStore hasn't initialized yet at this point (async+$state). This runs
|
||||
// synchronously in <svelte:head> before any rendering, preventing a flash
|
||||
// of protected content. The authStore handles post-hydration auth.
|
||||
try {
|
||||
var token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
|
||||
@@ -1,397 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Skeleton } from '$lib/components/ui/skeleton';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
|
||||
// Types
|
||||
type Service = {
|
||||
id: string;
|
||||
service_name: string;
|
||||
price: number;
|
||||
duration_minutes: number;
|
||||
override_price?: number;
|
||||
};
|
||||
|
||||
type Booking = {
|
||||
id: string;
|
||||
start_time: string;
|
||||
status: string;
|
||||
services: Service[];
|
||||
deposit_required: boolean;
|
||||
deposit_amount?: number;
|
||||
deposit_deadline?: string;
|
||||
duration_minutes: number;
|
||||
};
|
||||
|
||||
type PaymentSummary = {
|
||||
total_amount: number;
|
||||
paid_amount: number;
|
||||
refunded_amount: number;
|
||||
remaining_amount: number;
|
||||
total_vat_amount: number;
|
||||
total_net_amount: number;
|
||||
payments: Array<{
|
||||
id: string;
|
||||
status: string;
|
||||
amount: number;
|
||||
payment_type: string;
|
||||
}>;
|
||||
refunds: Array<{
|
||||
id: string;
|
||||
status: string;
|
||||
amount: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
// State
|
||||
let booking = $state<Booking | null>(null);
|
||||
let paymentSummary = $state<PaymentSummary | null>(null);
|
||||
let loading = $state(true);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Get booking ID from URL
|
||||
const bookingId = $derived($page.params.id);
|
||||
|
||||
// Derived values
|
||||
const totalDuration = $derived(
|
||||
booking?.services?.reduce((sum, s) => sum + s.duration_minutes, 0) ?? 0
|
||||
);
|
||||
|
||||
const totalPrice = $derived(booking?.services?.reduce((sum, s) => sum + s.price, 0) ?? 0);
|
||||
|
||||
const hasPaidDeposit = $derived(() => {
|
||||
if (!paymentSummary?.payments) return false;
|
||||
return paymentSummary.payments.some(
|
||||
(p) => p.status === 'completed' && (p.payment_type === 'deposit' || p.payment_type === 'full')
|
||||
);
|
||||
});
|
||||
|
||||
const hasPaidAnything = $derived(() => {
|
||||
if (!paymentSummary?.payments) return false;
|
||||
return paymentSummary.payments.some((p) => p.status === 'completed');
|
||||
});
|
||||
|
||||
// Format functions
|
||||
function formatDate(dateStr: string): string {
|
||||
const date = new SvelteDate(dateStr);
|
||||
return date.toLocaleDateString('en-GB', {
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
function formatTime(dateStr: string): string {
|
||||
const date = new SvelteDate(dateStr);
|
||||
return date.toLocaleTimeString('en-GB', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
function formatDuration(minutes: number): string {
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const remainingMinutes = minutes % 60;
|
||||
|
||||
if (hours === 0) {
|
||||
return `${remainingMinutes} minutes`;
|
||||
} else if (remainingMinutes === 0) {
|
||||
return `${hours} ${hours === 1 ? 'hour' : 'hours'}`;
|
||||
} else {
|
||||
return `${hours} ${hours === 1 ? 'hour' : 'hours'} ${remainingMinutes} minutes`;
|
||||
}
|
||||
}
|
||||
|
||||
function formatPounds(pounds: number): string {
|
||||
return `£${pounds.toFixed(2)}`;
|
||||
}
|
||||
|
||||
function formatPence(pence: number): string {
|
||||
return `£${(pence / 100).toFixed(2)}`;
|
||||
}
|
||||
|
||||
function formatDeadline(dateStr: string): string {
|
||||
const date = new SvelteDate(dateStr);
|
||||
return date.toLocaleDateString('en-GB', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch data
|
||||
async function fetchBookingData() {
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
// Fetch booking details
|
||||
const bookingResponse = await fetch(`/api/bookings/${bookingId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!bookingResponse.ok) {
|
||||
if (bookingResponse.status === 404) {
|
||||
throw new Error('Booking not found');
|
||||
}
|
||||
throw new Error('Failed to load booking');
|
||||
}
|
||||
|
||||
booking = await bookingResponse.json();
|
||||
|
||||
// Fetch payment summary
|
||||
const paymentResponse = await fetch(`/api/bookings/${bookingId}/payment-summary`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
}
|
||||
});
|
||||
|
||||
if (paymentResponse.ok) {
|
||||
paymentSummary = await paymentResponse.json();
|
||||
}
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'An error occurred';
|
||||
toast.error(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
$effect(() => {
|
||||
if (bookingId) {
|
||||
fetchBookingData();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Booking Confirmed - Crussell</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="mx-auto max-w-2xl p-6">
|
||||
{#if loading}
|
||||
<div class="space-y-6">
|
||||
<div class="text-center">
|
||||
<Skeleton class="mx-auto h-12 w-64" />
|
||||
<Skeleton class="mx-auto mt-2 h-6 w-48" />
|
||||
</div>
|
||||
<Card.Root>
|
||||
<Card.Content class="space-y-4 pt-6">
|
||||
<Skeleton class="h-8 w-full" />
|
||||
<Skeleton class="h-20 w-full" />
|
||||
<Skeleton class="h-16 w-full" />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
{:else if error}
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title class="text-red-600">Error</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<p class="text-gray-600">{error}</p>
|
||||
<Button class="mt-4" onclick={() => (window.location.href = '/')}>Return Home</Button>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
{:else if booking}
|
||||
<div class="mb-8 text-center">
|
||||
<div
|
||||
class="mx-auto mb-4 flex h-20 w-20 items-center justify-center rounded-full bg-green-100"
|
||||
>
|
||||
<svg
|
||||
class="h-10 w-10 text-green-600"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M20 6L9 17l-5-5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-gray-900">Booking Confirmed!</h1>
|
||||
<p class="mt-2 text-gray-600">Your appointment has been successfully booked</p>
|
||||
</div>
|
||||
|
||||
<Card.Root class="mb-6">
|
||||
<Card.Header>
|
||||
<Card.Title>Appointment Details</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content class="space-y-4">
|
||||
<div class="flex items-center justify-between border-b pb-4">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-500">Date</div>
|
||||
<div class="text-lg font-semibold">{formatDate(booking.start_time)}</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-sm font-medium text-gray-500">Time</div>
|
||||
<div class="text-lg font-semibold">{formatTime(booking.start_time)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between border-b pb-4">
|
||||
<div class="text-sm font-medium text-gray-500">Estimated Duration</div>
|
||||
<div class="font-semibold">{formatDuration(totalDuration)}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb-3 text-sm font-medium text-gray-500">Services</div>
|
||||
<div class="space-y-2">
|
||||
{#each booking.services as service (service.id)}
|
||||
<div class="flex justify-between rounded bg-gray-50 p-3">
|
||||
<div>
|
||||
<div class="font-medium">{service.service_name}</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
{service.duration_minutes} mins
|
||||
</div>
|
||||
</div>
|
||||
<div class="font-semibold">
|
||||
{formatPounds(service.price)}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between border-t pt-4">
|
||||
<div class="text-lg font-semibold">Total</div>
|
||||
<div class="text-lg font-bold">{formatPounds(totalPrice)}</div>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Payment</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
{#if booking.deposit_required}
|
||||
{#if hasPaidDeposit()}
|
||||
<div class="flex items-center gap-3 rounded-lg border border-green-200 bg-green-50 p-4">
|
||||
<svg
|
||||
class="h-6 w-6 text-green-600"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M20 6L9 17l-5-5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<div>
|
||||
<div class="font-semibold text-green-800">Deposit Paid</div>
|
||||
<div class="text-sm text-green-700">
|
||||
Your deposit of {formatPounds(booking.deposit_amount ?? 0)} has been paid
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if paymentSummary && paymentSummary.remaining_amount > 0}
|
||||
<div class="mt-4 rounded-lg bg-gray-50 p-4">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">Remaining Balance</span>
|
||||
<span class="font-semibold">{formatPence(paymentSummary.remaining_amount)}</span>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500">
|
||||
You can pay the remaining balance on the day of your appointment
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-lg border border-amber-200 bg-amber-50 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="font-semibold text-amber-800">Deposit Required</div>
|
||||
<div class="text-sm text-amber-700">
|
||||
To secure your booking, please pay a deposit of {formatPounds(
|
||||
booking.deposit_amount ?? 0
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-amber-800">
|
||||
{formatPounds(booking.deposit_amount ?? 0)}
|
||||
</div>
|
||||
</div>
|
||||
{#if booking.deposit_deadline}
|
||||
<div class="mt-2 text-sm text-amber-600">
|
||||
Please pay before {formatDeadline(booking.deposit_deadline)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Button class="w-full" onclick={() => toast.info('Payment integration coming soon')}>
|
||||
Pay Deposit Now
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-lg bg-gray-50 p-4">
|
||||
<p class="text-gray-600">
|
||||
You can pay on the day, but if you'd prefer you can pay ahead here
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if hasPaidAnything()}
|
||||
<div
|
||||
class="flex items-center gap-3 rounded-lg border border-green-200 bg-green-50 p-4"
|
||||
>
|
||||
<svg
|
||||
class="h-6 w-6 text-green-600"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M20 6L9 17l-5-5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<div>
|
||||
<div class="font-semibold text-green-800">Paid</div>
|
||||
<div class="text-sm text-green-700">
|
||||
{formatPence(paymentSummary?.paid_amount ?? 0)} paid
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if paymentSummary && paymentSummary.total_vat_amount > 0}
|
||||
<div class="mt-2 rounded-lg bg-gray-50 p-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">Net amount</span>
|
||||
<span class="font-medium">{formatPence(paymentSummary.total_net_amount)}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">VAT</span>
|
||||
<span class="font-medium">{formatPence(paymentSummary.total_vat_amount)}</span>
|
||||
</div>
|
||||
<div
|
||||
class="mt-1 flex justify-between border-t border-gray-200 pt-1 text-sm font-semibold"
|
||||
>
|
||||
<span class="text-gray-800">Total paid</span>
|
||||
<span class="text-gray-800">{formatPence(paymentSummary.paid_amount)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<Button class="w-full" onclick={() => toast.info('Payment integration coming soon')}>
|
||||
Pay Now
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="mt-6 flex flex-col gap-4 sm:flex-row sm:justify-center">
|
||||
<Button variant="outline" onclick={() => (window.location.href = '/schedule')}>
|
||||
View My Bookings
|
||||
</Button>
|
||||
<Button variant="ghost" onclick={() => (window.location.href = '/')}>Return Home</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { browser } from '$app/environment';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
@@ -228,9 +229,7 @@
|
||||
async function fetchGdprData() {
|
||||
stopPolling();
|
||||
try {
|
||||
const res = await fetch('/api/user/gdpr-export', {
|
||||
headers: { Authorization: `Bearer ${authStore.currentToken}` }
|
||||
});
|
||||
const res = await apiFetch('/api/user/gdpr-export');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data.status === 'generating') {
|
||||
@@ -481,6 +480,27 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<script>
|
||||
(function () {
|
||||
// Pre-hydration auth guard: reads localStorage directly because the Svelte
|
||||
// authStore hasn't initialized yet at this point (async+$state). This runs
|
||||
// synchronously before any rendering, preventing a flash of protected content.
|
||||
// The authStore handles post-hydration auth.
|
||||
try {
|
||||
var token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
window.location.replace('/login');
|
||||
return;
|
||||
}
|
||||
var payload = JSON.parse(atob(token.split('.')[1]));
|
||||
if (payload.exp * 1000 <= Date.now()) {
|
||||
window.location.replace('/login');
|
||||
}
|
||||
} catch (e) {
|
||||
window.location.replace('/login');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<title>Your Personal Data — Crussell</title>
|
||||
</svelte:head>
|
||||
|
||||
|
||||
@@ -270,6 +270,10 @@
|
||||
<svelte:head>
|
||||
<script>
|
||||
(function () {
|
||||
// Pre-hydration auth guard: reads localStorage directly because the Svelte
|
||||
// authStore hasn't initialized yet at this point (async+$state). This runs
|
||||
// synchronously in <svelte:head> before any rendering, preventing a flash
|
||||
// of protected content. The authStore handles post-hydration auth.
|
||||
try {
|
||||
var token = localStorage.getItem('authToken');
|
||||
if (token) {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
|
||||
// Types
|
||||
type Service = {
|
||||
@@ -103,11 +104,7 @@
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/bookings/${bookingId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
}
|
||||
});
|
||||
const response = await apiFetch(`/api/bookings/${bookingId}`);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
@@ -164,12 +161,9 @@
|
||||
try {
|
||||
const amountInPence = Math.round(tipAmount * 100);
|
||||
|
||||
const response = await fetch(`/api/bookings/${bookingId}/tip`, {
|
||||
const response = await apiFetch(`/api/bookings/${bookingId}/tip`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
amount: amountInPence,
|
||||
card_token: 'placeholder'
|
||||
@@ -226,6 +220,27 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<script>
|
||||
(function () {
|
||||
// Pre-hydration auth guard: reads localStorage directly because the Svelte
|
||||
// authStore hasn't initialized yet at this point (async+$state). This runs
|
||||
// synchronously before any rendering, preventing a flash of protected content.
|
||||
// The authStore handles post-hydration auth.
|
||||
try {
|
||||
var token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
window.location.replace('/login');
|
||||
return;
|
||||
}
|
||||
var payload = JSON.parse(atob(token.split('.')[1]));
|
||||
if (payload.exp * 1000 <= Date.now()) {
|
||||
window.location.replace('/login');
|
||||
}
|
||||
} catch (e) {
|
||||
window.location.replace('/login');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<title>Leave a Tip - Crussell</title>
|
||||
</svelte:head>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { Skeleton } from '$lib/components/ui/skeleton/index.js';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { formatDuration } from '$lib/utils/format';
|
||||
|
||||
type Service = {
|
||||
@@ -23,12 +23,9 @@
|
||||
async function fetchServices() {
|
||||
servicesLoading = true;
|
||||
try {
|
||||
const response = await fetch('/api/services', {
|
||||
const response = await apiFetch('/api/services', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
}
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { ensureBusinessInfo, getBusinessInfo } from '$lib/stores/businessInfo.svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import { toast } from 'svelte-sonner';
|
||||
@@ -39,11 +40,8 @@
|
||||
loading = true;
|
||||
try {
|
||||
const today = new Date().toLocaleDateString('en-CA', { timeZone: 'Europe/London' });
|
||||
const response = await fetch(`/api/bookings?start_date=${today}&per_page=50&page=1`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
}
|
||||
const response = await apiFetch(`/api/bookings?start_date=${today}&per_page=50&page=1`, {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
if (!response.ok) {
|
||||
toast.error('Failed to load bookings');
|
||||
@@ -180,6 +178,30 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<script>
|
||||
(function () {
|
||||
// Pre-hydration auth guard: reads localStorage directly because the Svelte
|
||||
// authStore hasn't initialized yet at this point (async+$state). This runs
|
||||
// synchronously before any rendering, preventing a flash of protected content.
|
||||
// The authStore handles post-hydration auth.
|
||||
try {
|
||||
var token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
window.location.replace('/login');
|
||||
return;
|
||||
}
|
||||
var payload = JSON.parse(atob(token.split('.')[1]));
|
||||
if (payload.exp * 1000 <= Date.now()) {
|
||||
window.location.replace('/login');
|
||||
}
|
||||
} catch (e) {
|
||||
window.location.replace('/login');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</svelte:head>
|
||||
|
||||
{#if pageState === 'loading'}
|
||||
<div class="mx-auto max-w-4xl px-4 py-8 md:px-8 md:py-12">
|
||||
<div class="animate-pulse space-y-6">
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
@@ -131,12 +132,9 @@
|
||||
try {
|
||||
const amountInPence = Math.round(tipAmount * 100);
|
||||
|
||||
const response = await fetch(`/api/bookings/${booking.id}/tip`, {
|
||||
const response = await apiFetch(`/api/bookings/${booking.id}/tip`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
amount: amountInPence,
|
||||
card_token: 'placeholder'
|
||||
@@ -192,11 +190,7 @@
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/bookings', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
}
|
||||
});
|
||||
const response = await apiFetch('/api/bookings');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load your bookings');
|
||||
@@ -231,11 +225,7 @@
|
||||
}
|
||||
|
||||
// Fetch full booking details (includes payments for tips tracking)
|
||||
const detailsResp = await fetch(`/api/bookings/${pastBooking.id}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
}
|
||||
});
|
||||
const detailsResp = await apiFetch(`/api/bookings/${pastBooking.id}`);
|
||||
|
||||
if (!detailsResp.ok) {
|
||||
throw new Error('Failed to load booking details');
|
||||
@@ -251,6 +241,27 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<script>
|
||||
(function () {
|
||||
// Pre-hydration auth guard: reads localStorage directly because the Svelte
|
||||
// authStore hasn't initialized yet at this point (async+$state). This runs
|
||||
// synchronously before any rendering, preventing a flash of protected content.
|
||||
// The authStore handles post-hydration auth.
|
||||
try {
|
||||
var token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
window.location.replace('/login');
|
||||
return;
|
||||
}
|
||||
var payload = JSON.parse(atob(token.split('.')[1]));
|
||||
if (payload.exp * 1000 <= Date.now()) {
|
||||
window.location.replace('/login');
|
||||
}
|
||||
} catch (e) {
|
||||
window.location.replace('/login');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<title>Leave a Tip - Crussell</title>
|
||||
</svelte:head>
|
||||
|
||||
|
||||
@@ -83,6 +83,10 @@
|
||||
<svelte:head>
|
||||
<script>
|
||||
(function () {
|
||||
// Pre-hydration auth guard: reads localStorage directly because the Svelte
|
||||
// authStore hasn't initialized yet at this point (async+$state). This runs
|
||||
// synchronously in <svelte:head> before any rendering, preventing a flash
|
||||
// of protected content. The authStore handles post-hydration auth.
|
||||
try {
|
||||
var token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
|
||||
Reference in New Issue
Block a user