array_merge($r, [['POST', '/admin/events/{id}/publish', 'a_event_publish', 'events']]));
/** Publish a draft from the "Before you publish" steps, once it has something to sell. */
function a_event_publish(int $id): void
{
$ev = q1('SELECT * FROM {events} WHERE id = ?', [$id]) ?? abort(404);
set_back("/admin/events/$id");
if ($ev['status'] !== 'draft') fail('Only a draft can be published here.');
if (!(int)qv('SELECT COUNT(*) FROM {ticket_categories} WHERE event_id = ? AND is_active = 1', [$id])) fail('Add at least one ticket category first.');
if ($ev['starts_at'] <= now()) fail('This event has already started. Change the date first.');
q("UPDATE {events} SET status = 'published', updated_at = ? WHERE id = ?", [now(), $id]);
do_action('gatecraft_event_saved', $id, $ev);
audit('event.publish', 'event', $id, ['title' => $ev['title']]);
flash('success', 'Published. The event page is live and tickets are on sale.');
redirect("/admin/events/$id");
}
/** Things waiting for staff, per admin page slug, for the badges in the menu. */
function admin_nav_counts(): array
{
static $c = null;
if ($c !== null) return $c;
$c = [];
try {
if (can('orders')) {
$c['gatecraft-orders'] = (int)qv("SELECT COUNT(*) FROM {orders} WHERE status = 'pending_payment' AND payment_method = 'offline' AND expires_at > ?", [now()]);
if (feature('approvals')) $c['gatecraft-approvals'] = (int)qv("SELECT COUNT(*) FROM {orders} WHERE status = 'awaiting_approval' AND expires_at > ?", [now()]);
}
} catch (\Throwable $e) {
// Counts are a convenience; a page must never fail because of them.
}
return $c = (array)apply_filters('gatecraft_nav_counts', $c);
}
function admin_cap_ok(string $cap): bool
{
return current_user_can($cap);
}
/** Links for the "New" menu, filtered by what the person may do. */
function admin_quick_create(): array
{
$items = [
['events', 'New event', '/admin/events/new', 'calendar'],
['orders', 'New booking or guest', '/admin/orders/new', 'ticket'],
['venues', 'New venue', '/admin/venues?new=1', 'building'],
['discounts', 'New discount code', '/admin/discounts', 'tag'],
['users', 'Invite staff', '/admin/users#f_email', 'users'],
];
$items = (array)apply_filters('gatecraft_quick_create', $items); // add-ons insert their own actions
return array_values(array_filter($items, fn($i) => can($i[0])));
}
/** Everything the quick finder can jump to, added before the admin script as window.gcPalette: menu pages, actions and recent events. */
function admin_palette_attach(string $handle): void
{
$out = [];
foreach (admin_pages() as $slug => [$label, $cap, $route]) {
if (!admin_cap_ok($cap)) continue;
$out[] = ['t' => t($label), 'g' => t('Go to'), 'u' => map_url('/admin' . $route)];
}
foreach (admin_quick_create() as [, $label, $href]) $out[] = ['t' => t($label), 'g' => t('Create'), 'u' => map_url($href)];
if (can('settings')) {
foreach (setting_tabs() as $k => $label) $out[] = ['t' => t('Settings') . ': ' . t($label), 'g' => t('Settings'), 'u' => map_url('/admin/settings?tab=' . $k)];
}
if (can('events')) {
try {
$evs = q("SELECT id, title, starts_at FROM {events} WHERE status <> 'archived' ORDER BY ABS(TIMESTAMPDIFF(DAY, starts_at, ?)) LIMIT 40", [now()])->fetchAll();
foreach ($evs as $e) $out[] = ['t' => $e['title'], 'd' => fmt_date($e['starts_at']), 'g' => t('Events'), 'u' => map_url('/admin/events/' . (int)$e['id']), 'raw' => 1];
} catch (\Throwable $e) {
// ignore
}
}
$search = can('orders') ? map_url('/admin/orders') : '';
$json = wp_json_encode(['items' => $out, 'search' => $search, 'searchLabel' => t('Search bookings for'), 'placeholder' => t('Jump to a page, event or setting, or search bookings'), 'empty' => t('Nothing found. Press Enter to search bookings.')], JSON_HEX_TAG | JSON_HEX_AMP);
if ($json) wp_add_inline_script($handle, 'window.gcPalette=' . $json . ';', 'before');
}
/*
* Wayfinding: every admin area has its own colour, used in the menu, the top bar and the page header, so staff can
* tell where they are at a glance. The colours sit next to the brand's black, red and steel teal.
*/
const ADMIN_SECTIONS = [
'overview' => ['Overview', 'home', ['/admin', '/admin/notifications']],
'events' => ['Events', 'calendar', ['/admin/events', '/admin/venues', '/admin/discounts', '/admin/categories', '/admin/tiers', '/admin/questions', '/admin/series']],
'sales' => ['Sales', 'cart', ['/admin/orders', '/admin/tickets', '/admin/approvals', '/admin/audience', '/admin/reports', '/admin/export', '/admin/waitlist']],
'door' => ['Door', 'scan', ['/admin/checkin']],
'admin' => ['Admin', 'settings', ['/admin/settings', '/admin/users', '/admin/emails', '/admin/audit', '/admin/backup', '/admin/privacy', '/admin/updates', '/admin/license', '/admin/event-design', '/admin/pro-settings', '/admin/import']],
'gatecraft' => ['Gatecraft', 'info', ['/admin/about', '/admin/release-notes']],
];
/** The admin area a path belongs to (a key of ADMIN_SECTIONS). */
function admin_section(?string $path = null): string
{
$path ??= gatecraft_current_path();
if (!str_starts_with($path, '/admin') && function_exists(__NAMESPACE__ . '\\gatecraft_admin_route')) $path = gatecraft_admin_route();
$best = 'overview'; $len = 0;
foreach (ADMIN_SECTIONS as $key => [, , $prefixes]) {
foreach ($prefixes as $p) {
if (($path === $p || str_starts_with($path, $p . '/')) && strlen($p) > $len) { $best = $key; $len = strlen($p); }
}
}
return $best;
}
/** Short edition name: Free, Pro or Business Edition. */
function edition_short(): string
{
if (is_business_edition()) return 'Business Edition';
return defined('GATECRAFT_PRO_VERSION') ? 'Pro' : 'Free';
}
/**
* The version as people see it: WF.YEAR.RELEASE.FEATURE.PATCH for the free edition, WP.… for Gatecraft Pro. Only the
* label carries the prefix. The file headers, readme and update checks keep the plain number so versions compare.
*/
function version_label(bool $free = false): string
{
if (is_business_edition()) return GATECRAFT_VERSION;
return !$free && defined('GATECRAFT_PRO_VERSION') ? 'WP.' . GATECRAFT_PRO_VERSION : 'WF.' . GATECRAFT_VERSION;
}
/** The running version, always in view in the admin. Links to the release notes where they are listed. */
function version_pill(string $class = ''): string
{
$notes = release_notes_on();
return '<' . ($notes ? 'a href="/admin/release-notes" title="' . e('Release notes for this version') . '"' : 'span') . ' class="ver-pill' . ($class !== '' ? ' ' . e($class) : '') . '">'
. '' . e('Version') . '' . e(version_label()) . '' . e(edition_short()) . '' . ($notes ? 'a' : 'span') . '>';
}
/** The current area as a coloured chip, shown at the top of every admin page. */
function admin_section_chip(): string
{
[$label, $icon] = ADMIN_SECTIONS[admin_section()];
return '' . icon($icon) . '' . e($label) . '';
}