import { PrivacySettings } from './PrivacySettings'; import { type BankProfile, type ProfileAvailability } from './BankProfiles'; import { BookingRulesSettings } from './BookingRulesSettings'; import * as React from 'react'; import { __ } from '@wordpress/i18n'; import { BookingApi } from '../booking-ui/api'; import { CatalogApi } from './api'; import type { Config } from './types'; import { CalendarSettings } from './CalendarSettings'; import { ClubLocationForm } from './ClubLocationForm'; import { methodLabel } from '../booking-ui/credit'; interface SettingsOverview { sections: Record; memberships_enabled: boolean; } interface BankAccount { country: string; recipient_name: string | null; account_number: string | null; bank_code: string | null; iban: string | null; bic: string | null; currency: string; } interface Payments { bank_profiles: BankProfile[]; bank_profile_availability: Record; bank_profiles_warning: string | null; block_booking_with_outstanding_balance: boolean; outstanding_balance_grace_hours: number; qr_available?: boolean; qr_unavailable_reason?: string | null; enable_credit: boolean; enable_pay_on_site: boolean; enable_bank_transfer: boolean; enable_qr_payment: boolean; bank_account: BankAccount; } interface Integration { reservation_page_id: number | null; pages: { id: number; title: string; }[]; url: string | null; edit_url: string | null; shortcode_present: boolean; shortcode_status: 'canonical' | 'legacy' | 'missing'; } const tabs = () => [['general', __('General', 'abece-booking')], ['bookings', __('Bookings', 'abece-booking')], ['membership', __('Membership', 'abece-booking')], ['payments', __('Payments', 'abece-booking')], ['calendar', __('Calendar', 'abece-booking')], ['location', __('Location', 'abece-booking')], ['integrations', __('Integrations', 'abece-booking')], ['privacy', __('Privacy', 'abece-booking')]]; export function SettingsApp({ api, config }: { api: BookingApi; config: Config; }) { const [overview, setOverview] = React.useState(null), [tab, setTab] = React.useState(new URLSearchParams(location.search).get('tab') || 'general'), [error, setError] = React.useState(''); const locationApi = React.useMemo(() => new CatalogApi(config), [config]); React.useEffect(() => { let alive = true; api.request('settings').then(value => { if (alive) { setOverview(value); setTab(current => value.sections[current] ? current : 'general'); } }).catch(e => { if (alive) setError(e.message); }); return () => { alive = false; }; }, [api]); React.useEffect(() => { const pop = () => setTab(new URLSearchParams(location.search).get('tab') || 'general'); addEventListener('popstate', pop); return () => removeEventListener('popstate', pop); }, []); function open(key: string) { const url = new URL(location.href); url.searchParams.set('tab', key); history.pushState(null, '', url); setTab(key); } return

{__('Settings', 'abece-booking')}

{error &&

{error}

}{!overview && !error &&

{__('Loading…', 'abece-booking')}

}{overview && <>
key === tab)?.[1]}> {tab === 'privacy' && }{tab === 'general' && }{tab === 'bookings' && <>{overview.sections.booking_rules && } {overview.sections.cancellation &&

{__('Cancellation and penalties', 'abece-booking')}

{__('Manage cancellation deadlines, fees and no-show penalties.', 'abece-booking')}

{__('Manage cancellation policies', 'abece-booking')}
}}{false}{tab === 'payments' && }{tab === 'calendar' && }{tab === 'location' && }{tab === 'integrations' && }
}
; } function adminUrl(page: string) { const url = new URL(location.href); url.search = ''; url.searchParams.set('page', 'abece-booking-' + page); return url.toString(); } function GeneralSettings({ api }: { api: BookingApi; }) { const [data, setData] = React.useState<{ site_name: string; timezone: string; locale: string; } | null>(null), [error, setError] = React.useState(''); React.useEffect(() => { let alive = true; api.request('settings/general').then(d => { if (alive) setData(d); }).catch(e => { if (alive) setError(e.message); }); return () => { alive = false; }; }, [api]); return <>

{__('General', 'abece-booking')}

{error &&

{error}

}{data && <>
{__('Site name', 'abece-booking')}
{data.site_name}
{__('Timezone', 'abece-booking')}
{data.timezone}

{__('The site name and timezone follow the WordPress settings. Prices use the currency of their price list.', 'abece-booking')}

}; } function PaymentSettings({ api, canManageBookings }: { api: BookingApi; canManageBookings: boolean; }) { const validated = React.useRef(''); const [data, setData] = React.useState(null), [busy, setBusy] = React.useState(false), [error, setError] = React.useState(''), [saved, setSaved] = React.useState(false); React.useEffect(() => { let alive = true; api.request('settings/payments').then(d => { if (alive) { validated.current = JSON.stringify(d.bank_profiles); setData(d); } }).catch(e => { if (alive) setError(e.message); }); return () => { alive = false; }; }, [api]); async function save(e: React.FormEvent) { e.preventDefault(); if (!data) return; setBusy(true); setError(''); try { const { qr_available, qr_unavailable_reason, bank_profile_availability, bank_profiles_warning, bank_account, ...payload } = data; const result = await api.request('settings/payments', 'PATCH', payload); validated.current = JSON.stringify(result.bank_profiles); setData(result); setSaved(true); } catch (e) { setError((e as Error).message); } finally { setBusy(false); } } const descriptions: Record = { credit: __('Internal customer credit account. A manager can top it up manually.', 'abece-booking'), pay_on_site: __('The customer pays when they arrive. Record the actual payment medium when confirming receipt.', 'abece-booking'), bank_transfer: __('The customer receives payment details. The booking remains unpaid until a manager confirms receipt. International accounts are supported.', 'abece-booking'), qr_payment: __('Scan a supported bank QR code. Availability depends on the currency, bank profile and banking app.', 'abece-booking') }; const dirtyProfiles = !!data && validated.current !== JSON.stringify(data.bank_profiles); const qrReason = data && !dirtyProfiles && !data.qr_available ? data.qr_unavailable_reason : null; return <>

{__('Payments', 'abece-booking')}

{error &&

{error}

}{saved &&

{__('Changes saved.', 'abece-booking')}

}{data ?
{(['credit', 'pay_on_site', 'bank_transfer', 'qr_payment'] as const).filter(m => m === 'pay_on_site' || false).map(method =>

{descriptions[method]}

{method === 'qr_payment' && qrReason &&

{__('Unavailable', 'abece-booking')}: {qrReason}

}{method === 'credit' &&

{__('Requires a supported online payment integration.', 'abece-booking')}

}
)}

{__('Requires WooCommerce or a supported payment integration.', 'abece-booking')}

{__('Outstanding amounts', 'abece-booking')}

{__('Zero hours means the unpaid amount becomes overdue when the booking ends.', 'abece-booking')}

{false}
: !error &&

{__('Loading…', 'abece-booking')}

}

{canManageBookings && {__('Show outstanding amounts', 'abece-booking')}} {false}

; } function IntegrationSettings({ api }: { api: BookingApi; }) { const [data, setData] = React.useState(null), [page, setPage] = React.useState(''), [busy, setBusy] = React.useState(false), [error, setError] = React.useState(''), [notice, setNotice] = React.useState(''); React.useEffect(() => { let alive = true; api.request('settings/integrations').then(d => { if (alive) { setData(d); setPage(String(d.reservation_page_id ?? '')); } }).catch(e => { if (alive) setError(e.message); }); return () => { alive = false; }; }, [api]); async function save(e: React.FormEvent) { e.preventDefault(); setBusy(true); setError(''); setNotice(''); try { setData(await api.request('settings/integrations', 'PATCH', { reservation_page_id: page ? Number(page) : null })); setNotice(__('Changes saved.', 'abece-booking')); } catch (e) { setError((e as Error).message); } finally { setBusy(false); } } async function copy() { setError(''); try { try { if (!navigator.clipboard) throw new Error(); await navigator.clipboard.writeText('[abece_booking]'); } catch { const old = document.activeElement as HTMLElement | null, input = document.createElement('textarea'); input.value = '[abece_booking]'; input.readOnly = true; input.style.position = 'fixed'; input.style.opacity = '0'; document.body.append(input); input.select(); const copied = document.execCommand('copy'); input.remove(); old?.focus(); if (!copied) throw new Error(__('Copy the shortcode manually.', 'abece-booking')); } setNotice(__('Shortcode copied.', 'abece-booking')); } catch (e) { setError((e as Error).message); } } return <>

{__('Reservation page', 'abece-booking')}

{__('Insert ABeCe Booking into a WordPress page using this shortcode:', 'abece-booking')}

[abece_booking]

  1. {__('Create or open a page in WordPress.', 'abece-booking')}
  2. {__('Add a Shortcode block.', 'abece-booking')}
  3. {__('Insert [abece_booking].', 'abece-booking')}
  4. {__('Publish the page.', 'abece-booking')}

{__('Shortcode is the currently supported integration.', 'abece-booking')}

{error &&

{error}

}{notice &&

{notice}

}
{data?.reservation_page_id &&

{data.shortcode_status === 'legacy' ? __('This page uses the legacy shortcode [sports_booking]. Bookings will continue to work; we recommend using [abece_booking].', 'abece-booking') : data.shortcode_present ? __('Booking shortcode found.', 'abece-booking') : __('The selected page does not contain [abece_booking].', 'abece-booking')}

}{data?.url && <>

{__('Reservation URL', 'abece-booking')}: {data.url}

{__('Open page', 'abece-booking')}{data.edit_url && {__('Edit page', 'abece-booking')}}

}; }