Close pre-sale registration a configurable time before events start.
Events gain nullable presale_closure_enabled / presale_close_minutes_before overrides; null inherits the new site_settings defaults (enabled, 120 min). A shared resolver computes the effective cutoff, which the public event API exposes as presaleClosesAt and the public booking endpoint enforces. Door and admin ticket creation are not gated. Admin: toggle + duration picker in the event modal (pre-filled from the site default) and a matching default card on Settings › General. Public: the event page shows "Registration Closed" after the cutoff and the checkout page redirects back. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
745af4184f
commit
59acc32a46
@@ -8,8 +8,10 @@ import { parseDate } from '@/lib/utils';
|
||||
import Card from '@/components/ui/Card';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Input from '@/components/ui/Input';
|
||||
import DurationInput from '@/components/admin/DurationInput';
|
||||
import {
|
||||
Cog6ToothIcon,
|
||||
TicketIcon,
|
||||
GlobeAltIcon,
|
||||
ClockIcon,
|
||||
EnvelopeIcon,
|
||||
@@ -47,6 +49,8 @@ export default function AdminSettingsPage() {
|
||||
maintenanceMode: false,
|
||||
maintenanceMessage: null,
|
||||
maintenanceMessageEs: null,
|
||||
presaleClosureEnabled: true,
|
||||
presaleCloseMinutesBefore: 120,
|
||||
});
|
||||
|
||||
const [legalSettings, setLegalSettings] = useState<LegalSettingsData>({
|
||||
@@ -508,6 +512,69 @@ export default function AdminSettingsPage() {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Pre-sale Closure defaults */}
|
||||
<Card>
|
||||
<div className="p-6">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<TicketIcon className="w-5 h-5 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg">
|
||||
{locale === 'es' ? 'Cierre de Preventa' : 'Pre-sale Closure'}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500">
|
||||
{locale === 'es'
|
||||
? 'Valor por defecto para los eventos. Cada evento puede cambiarlo en su ventana de edición.'
|
||||
: 'Default for events. Each event can override this in its edit dialog.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg mb-4">
|
||||
<div>
|
||||
<p className="font-medium">
|
||||
{locale === 'es' ? 'Cerrar la preventa antes del evento' : 'Close pre-sale before the event starts'}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
{settings.presaleClosureEnabled
|
||||
? (locale === 'es' ? 'Las inscripciones en línea se cierran antes del inicio' : 'Online registration stops before the start time')
|
||||
: (locale === 'es' ? 'Las inscripciones siguen abiertas hasta el inicio' : 'Registration stays open until the event starts')}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateSetting('presaleClosureEnabled', !settings.presaleClosureEnabled)}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
settings.presaleClosureEnabled ? 'bg-green-500' : 'bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
settings.presaleClosureEnabled ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{settings.presaleClosureEnabled && (
|
||||
<div className="max-w-md">
|
||||
<DurationInput
|
||||
label={locale === 'es' ? 'Cerrar la preventa este tiempo antes del inicio' : 'Close pre-sale this long before the start'}
|
||||
valueMinutes={settings.presaleCloseMinutesBefore}
|
||||
onChange={(minutes) => updateSetting('presaleCloseMinutesBefore', minutes)}
|
||||
unitLabels={locale === 'es'
|
||||
? { minutes: 'minutos', hours: 'horas', days: 'días' }
|
||||
: { minutes: 'minutes', hours: 'hours', days: 'days' }}
|
||||
helper={locale === 'es'
|
||||
? 'Se aplica a los eventos que no tienen su propia configuración.'
|
||||
: 'Applies to events that have not set their own value.'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Maintenance Mode */}
|
||||
<Card>
|
||||
<div className="p-6">
|
||||
|
||||
Reference in New Issue
Block a user