import * as React from 'react'; import { __, sprintf } from '@wordpress/i18n'; import { BookingApi } from '../booking-ui/api'; interface Rule { min_minutes_before: number; fee_percentage: string | null; fee_amount: number | null; priority: number; } interface Policy { id?: number; name: string; sport_id: number | null; resource_id: number | null; no_show_fee_percentage: string; priority: number; is_active: boolean; rules: Rule[]; } interface Options { sports: { id: number; name: string; }[]; resources: { id: number; name: string; sport_id: number | null; }[]; } const blank = (): Policy => ({ name: '', sport_id: null, resource_id: null, no_show_fee_percentage: '100', priority: 0, is_active: true, rules: [{ min_minutes_before: 1440, fee_percentage: '0', fee_amount: null, priority: 0 }, { min_minutes_before: 720, fee_percentage: '20', fee_amount: null, priority: 0 }, { min_minutes_before: 360, fee_percentage: '50', fee_amount: null, priority: 0 }, { min_minutes_before: 0, fee_percentage: '100', fee_amount: null, priority: 0 }] }); export function CancellationAdminApp({ api }: { api: BookingApi; }) { const precedenceId = React.useId(); const [items, setItems] = React.useState([]); const [total, setTotal] = React.useState(0); const [page, setPage] = React.useState(1); const [options, setOptions] = React.useState({ sports: [], resources: [] }); const [edit, setEdit] = React.useState(null); const [error, setError] = React.useState(''); const [busy, setBusy] = React.useState(false); const [revision, setRevision] = React.useState(0); React.useEffect(() => { let live = true; setBusy(true); setError(''); Promise.all([api.list('cancellation-policies', { page: String(page) }), api.request('cancellation-policy-options')]).then(([list, o]) => { if (live) { setItems(list.items); setTotal(list.total); setOptions(o); } }).catch(e => { if (live) setError(e.message); }).finally(() => { if (live) setBusy(false); }); return () => { live = false; }; }, [api, page, revision]); async function save(e: React.FormEvent) { e.preventDefault(); if (!edit) return; setBusy(true); setError(''); try { const { id, ...data } = edit; await api.request(id ? `cancellation-policies/${id}` : 'cancellation-policies', id ? 'PATCH' : 'POST', data); setEdit(null); setRevision(v => v + 1); } catch (e) { setError((e as Error).message); } finally { setBusy(false); } } async function toggle(p: Policy) { setBusy(true); setError(''); try { await api.request(`cancellation-policies/${p.id}`, 'PATCH', { is_active: !p.is_active }); setRevision(v => v + 1); } catch (e) { setError((e as Error).message); } finally { setBusy(false); } } function change(fields: Partial) { setEdit(p => p ? { ...p, ...fields } : p); } function rule(index: number, fields: Partial) { if (edit) change({ rules: edit.rules.map((r, i) => i === index ? { ...r, ...fields } : r) }); } function open(p: Policy) { setError(''); setEdit({ id: p.id, name: p.name, sport_id: p.sport_id, resource_id: p.resource_id, priority: p.priority, is_active: p.is_active, no_show_fee_percentage: p.no_show_fee_percentage, rules: p.rules.map(r => ({ min_minutes_before: r.min_minutes_before, fee_percentage: r.fee_percentage, fee_amount: r.fee_amount, priority: r.priority })) }); } return

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

{__('Policies for a specific resource take precedence over policies for its sport, then venue-wide policies. For the same scope, the higher number wins.', 'abece-booking')}

{__('At each boundary, the highest threshold reached applies: exactly 12 hours uses the 12-hour rule. A zero-hour rule is required.', 'abece-booking')}

{error &&

{error}

} {items.map(p => )}
{__('Name', 'abece-booking')}{__('Scope', 'abece-booking')}{__('Precedence for the same scope', 'abece-booking')}{__('No-show percentage', 'abece-booking')}{__('Status', 'abece-booking')}{__('Actions', 'abece-booking')}
{p.name}{p.resource_id ? options.resources.find(r => r.id === p.resource_id)?.name : p.sport_id ? options.sports.find(s => s.id === p.sport_id)?.name : __('Global', 'abece-booking')}{p.priority}{p.no_show_fee_percentage} %{p.is_active ? __('Active', 'abece-booking') : __('Inactive', 'abece-booking')}

{sprintf(__('Total items: %d', 'abece-booking'), total)}

{page} {edit &&

{edit.id ? __('Edit cancellation policy', 'abece-booking') : __('Add cancellation policy', 'abece-booking')}

{edit.resource_id !== null ? : edit.sport_id !== null && }
change({ priority: Number(e.target.value) })}/>

{__('Higher numbers take precedence between policies for the same resource, sport or the whole venue. Keep 0 if there is only one policy for that scope.', 'abece-booking')}

{__('Cancellation rules', 'abece-booking')}

{edit.rules.map((r, i) => )}
{__('Cancel at least (hours before start)', 'abece-booking')}{__('Fee type', 'abece-booking')}{__('Fee', 'abece-booking')}{__('Actions', 'abece-booking')}
rule(i, { min_minutes_before: Math.round(Number(e.target.value) * 60) })}/>{r.fee_percentage !== null ? rule(i, { fee_percentage: e.target.value })}/> : rule(i, { fee_amount: Number(e.target.value) })}/>}

{__('Fixed fees use the booking currency in its smallest units (for example, 10000 means 100.00 for a two-decimal currency). Fees never exceed the original booking price.', 'abece-booking')}

}
; }