diff --git a/frontend/src/routes/account/+page.svelte b/frontend/src/routes/account/+page.svelte
index 5ff5054..4452ad7 100644
--- a/frontend/src/routes/account/+page.svelte
+++ b/frontend/src/routes/account/+page.svelte
@@ -4,6 +4,7 @@
import { SvelteDate } from 'svelte/reactivity';
import { browser } from '$app/environment';
import { toast } from 'svelte-sonner';
+ import { sanitizeText } from '$lib/utils/toast-safe';
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';
@@ -33,6 +34,7 @@
import { Separator } from '$lib/components/ui/separator';
import * as AlertDialog from '$lib/components/ui/alert-dialog';
import { Skeleton } from '$lib/components/ui/skeleton';
+ import PolicyPopover from '$lib/components/ui/policyPopover.svelte';
import * as Dialog from '$lib/components/ui/dialog';
import Cropper from 'svelte-easy-crop';
@@ -778,7 +780,7 @@
await fetchUserData();
} else {
const text = await response.text();
- toast.error(text || 'Failed to update phone number', { id: loadingToast });
+ toast.error(sanitizeText(text) || 'Failed to update phone number', { id: loadingToast });
}
} catch (err) {
console.error('Error updating phone:', err);
@@ -846,7 +848,7 @@
if (!response.ok) {
const text = await response.text();
- toast.error('Failed to load upcoming bookings: ' + text);
+ toast.error('Failed to load upcoming bookings: ' + sanitizeText(text));
return;
}
@@ -888,7 +890,7 @@
if (!response.ok) {
const text = await response.text();
- toast.error('Failed to load past bookings: ' + text);
+ toast.error('Failed to load past bookings: ' + sanitizeText(text));
return;
}
@@ -1003,7 +1005,7 @@
passwordData = { current: '', new: '', confirm: '' };
} else {
const text = await response.text();
- toast.error(text || 'Failed to change password', { id: loadingToast });
+ toast.error(sanitizeText(text) || 'Failed to change password', { id: loadingToast });
}
} catch (err) {
console.error('Error changing password:', err);
@@ -1051,7 +1053,7 @@
goto('/');
} else {
const text = await response.text();
- toast.error(text || 'Failed to delete account', { id: loadingToast });
+ toast.error(sanitizeText(text) || 'Failed to delete account', { id: loadingToast });
}
} catch (err) {
console.error('Error deleting account:', err);
@@ -2303,6 +2305,23 @@
+
+
Policies
+
+ View our cancellation, deposit, and no-show policies
+
+
+ {#snippet trigger()}
+
+ {/snippet}
+
+
+
+
+
{#if authStore.currentUser?.role !== 'admin'}
diff --git a/frontend/src/routes/admin/+page.svelte b/frontend/src/routes/admin/+page.svelte
index 7083d05..5bb4ef3 100644
--- a/frontend/src/routes/admin/+page.svelte
+++ b/frontend/src/routes/admin/+page.svelte
@@ -292,12 +292,7 @@
-
+
diff --git a/frontend/src/routes/admin/notifications/+page.svelte b/frontend/src/routes/admin/notifications/+page.svelte
index 4c44c7c..3e78aba 100644
--- a/frontend/src/routes/admin/notifications/+page.svelte
+++ b/frontend/src/routes/admin/notifications/+page.svelte
@@ -76,6 +76,11 @@
let showEditRequestModal = $state(false);
let selectedEditRequest = $state(null);
+ function openBookingModal(bookingId: string) {
+ selectedBooking = { id: bookingId };
+ showBookingModal = true;
+ }
+
let pageState = $state<'loading' | 'authorized' | 'unauthorized'>('loading');
$effect(() => {
@@ -94,12 +99,12 @@
const reasonLabels: Record = {
pending_booking: 'Booking Pending Approval',
edit_request: 'Customer Requested Booking Change',
- edit_requested: 'Booking Edit Requested',
+ edit_requested: 'Booking Edit/Reschedule Requested',
new_booking: 'New Booking Received',
cancelled_booking: 'Booking Cancelled',
late_cancellation: 'Late Cancellation (< 24h)',
- no_deposit: 'Deposit Issue',
deposit_paid: 'Deposit Payment Received',
+ deposit_not_paid_by_deadline: 'Deposit Deadline Passed',
affiliate_claim: 'Affiliate Referral Claimed',
'1_month_no_pay': 'No Payments in 1 Month',
'1_week_no_pay': 'No Payments in 1 Week'
@@ -115,7 +120,7 @@
case 'edit_requested':
return 'edit_approve';
case 'late_cancellation':
- case 'no_deposit':
+ case 'deposit_not_paid_by_deadline':
case '1_week_no_pay':
case '1_month_no_pay':
return 'see_user';
@@ -198,13 +203,13 @@
selectedEditRequest = data.edit_request;
showEditRequestModal = true;
} else if (response.status === 404) {
- toast.error('This edit request has already been processed');
+ toast.error('This edit/reschedule request has already been processed');
} else {
- toast.error('Could not load edit request details');
+ toast.error('Could not load edit/reschedule request details');
}
} catch (err) {
console.error('Error fetching edit request:', err);
- toast.error('Network error loading edit request');
+ toast.error('Network error loading edit/reschedule request');
}
} else if (action === 'see_user' && notification.user_id) {
selectedUserId = notification.user_id;
@@ -504,7 +509,7 @@
-
+
{#if showEditRequestModal && selectedEditRequest}
{:else}
+
{service.service_name}
- {service.override_duration_minutes ?? service.duration_minutes} mins
+ {service.duration_minutes} mins
- {formatPounds(service.override_price ?? service.price)}
+ {formatPounds(service.price)}
{/each}
diff --git a/frontend/src/routes/gdpr/+page.svelte b/frontend/src/routes/gdpr/+page.svelte
index 088ffe3..bf707ed 100644
--- a/frontend/src/routes/gdpr/+page.svelte
+++ b/frontend/src/routes/gdpr/+page.svelte
@@ -29,6 +29,8 @@
data_consent_updated_at: string;
created_at: string;
updated_at: string;
+ failed_attempts?: number;
+ locked_until?: string;
};
bookings: Array<{
booking_id: string;
@@ -159,6 +161,18 @@
booking_id: string;
created_at: string;
}>;
+ login_audit?: Array<{
+ attempt_type: string;
+ ip_address: string;
+ success: boolean;
+ created_at: string;
+ }>;
+ refresh_tokens?: Array<{
+ role: string;
+ revoked: boolean;
+ created_at: string;
+ expires_at: string;
+ }>;
export_metadata?: {
exported_at: string;
exported_by: string;
@@ -538,9 +552,9 @@
- {#if computeBookingStats(gdprData)}
- {@const stats = computeBookingStats(gdprData)}
- Summary
+ {#if computeBookingStats(gdprData)}
+ {@const stats = computeBookingStats(gdprData)!}
+ Summary
Member Since
@@ -678,6 +692,18 @@
Referral Code
{gdprData.user_profile.referral_code || '—'}
+ {#if gdprData.user_profile.failed_attempts != null && gdprData.user_profile.failed_attempts > 0}
+
+
Failed Login Attempts
+
{gdprData.user_profile.failed_attempts}
+
+ {/if}
+ {#if gdprData.user_profile.locked_until}
+
+
Account Locked Until
+
{fmtDateTime(gdprData.user_profile.locked_until)}
+
+ {/if}
Privacy Policy Consent
@@ -885,6 +911,76 @@
{/if}
+
+ {#if gdprData.login_audit && gdprData.login_audit.length > 0}
+
+ Login Activity
+
+
+
+ | Date | Type | Success |
+
+ {#each gdprData.login_audit as la (la.created_at)}
+
+ | {fmtDateTime(la.created_at)} |
+ {la.attempt_type.replace(/_/g, ' ')} |
+
+ {la.success ? 'Yes' : 'No'}
+ |
+
+ {/each}
+
+
+
+
+
+ {/if}
+
+
+ {#if gdprData.refresh_tokens && gdprData.refresh_tokens.length > 0}
+
+ Session Tokens
+
+
+
+ | Issued | Expires | Status |
+
+ {#each gdprData.refresh_tokens as rt (rt.created_at)}
+
+ | {fmtDateTime(rt.created_at)} |
+ {fmtDateTime(rt.expires_at)} |
+
+ {rt.revoked ? 'Revoked' : 'Active'}
+ |
+
+ {/each}
+
+
+
+
+
+ {/if}
+
{#if gdprData.patch_tests && gdprData.patch_tests.length > 0}
@@ -1240,7 +1336,7 @@