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()) . ''; } /** 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) . ''; }