updates for admin area

This commit is contained in:
Mark Gregory
2026-09-12 21:29:34 +01:00
parent 2964762c4b
commit 8b6d07b8f6
10 changed files with 241 additions and 10 deletions
+11
View File
@@ -0,0 +1,11 @@
# Admin appearance modes
The BeatBook administration backend now supports three appearance modes:
- Light
- System (follows the operating system appearance and updates live)
- Dark
The switcher is in the administration top bar. The preference is stored in browser localStorage under `beatbook-admin-theme` and is applied in the document head before Vite assets load to prevent a light/dark flash during navigation.
The theme affects the admin shell, sidebar, header, cards, CRUD tables, form controls, search fields, badges, flash/error messages, buttons and date/time inputs. It does not alter the public BeatBook site.
+32
View File
@@ -0,0 +1,32 @@
# BeatBook Admin CRUD
This revision replaces the read-only/placeholder administration sections with persistent CRUD management.
## CRUD resources
- Users
- Clients
- DJs
- Businesses
- Bookings
- Payments
- Reviews
- Messages
- Notifications
- Support tickets
- Saved reports
- Content pages
- Roles
- Permissions
- Privacy/legal documents
- Application settings
## New persistence
Migration `2026_09_12_000004_create_admin_management_tables.php` adds tables for payments, messages, admin notifications, support tickets, saved reports, content pages, legal documents and application settings.
Run `php artisan migrate` on an existing installation. Do not use `migrate:fresh` unless you intentionally want to erase existing data.
Published Privacy, Cookie and Terms records are used by the corresponding public legal pages. Published Content records are available at `/pages/{slug}`.
Notification records are currently managed content; creating a record does not yet dispatch email/push notifications. Saved reports are definitions; a later reporting engine can execute/export them.
+26
View File
@@ -0,0 +1,26 @@
# BeatBook UI / Backend Rework
## Completed
- Replaced the generic admin placeholder with real section views for users, DJs, clients, bookings, payments, reviews, messages, notifications, support, reports, content, roles, privacy and settings.
- Added admin actions for role changes, DJ publish/feature controls, review moderation and cache clearing.
- Replaced the non-clickable client dashboard with account routes for overview, enquiries, bookings and editable account details.
- Added responsive admin navigation with a mobile drawer.
- Added responsive public navigation and mobile menu.
- Rebuilt home, DJ directory and DJ profile layouts for desktop, tablet and mobile.
- Added sorting to the DJ directory.
- Added route coverage tests for account and admin pages.
- Made auth tests explicitly bypass request-forgery middleware during automated tests only.
- Restored a clean Laravel .gitignore and ensured composer.lock is not ignored.
- Updated Laravel 13 request-forgery middleware API usage.
## Notes
Messages, notifications and support now have proper destinations and admin presentation, but the project currently has no dedicated conversation/support database schema. Their pages therefore show deliberate empty states until those workflows/models are added.
After applying the files run:
composer install
npm install
npm run build
php artisan optimize:clear
php artisan test
./vendor/bin/pint
+12
View File
@@ -0,0 +1,12 @@
# Security baseline
- Enforce HTTPS/HSTS in production.
- Keep private/verification uploads outside public storage; authorize every download.
- Validate MIME and image decoding; strip metadata where appropriate.
- Use policies for every tenant-owned resource.
- Enable 2FA for admin/DJ accounts.
- Verify Stripe webhook signatures and make handlers idempotent.
- Rate-limit auth, enquiry, review and messaging endpoints.
- Audit privileged actions; hash IP addresses if retained.
- Never store card details.
- Run queues under Supervisor/systemd and keep dependencies patched.
- Replace placeholder legal policies before launch.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1,6 +1,6 @@
{
"resources/css/app.css": {
"file": "assets/app-DbTvU8vz.css",
"file": "assets/app-ByTEIgcV.css",
"src": "resources/css/app.css",
"isEntry": true,
"name": "app",
@@ -9,7 +9,7 @@
]
},
"resources/js/app.js": {
"file": "assets/app-fQMWg8Nx.js",
"file": "assets/app-Cl06EWgF.js",
"name": "app",
"src": "resources/js/app.js",
"isEntry": true
+109
View File
@@ -21,3 +21,112 @@
.crud-heading-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}@media(max-width:640px){.crud-heading-actions{width:100%;align-items:stretch;flex-direction:column}}
.legal-shell{max-width:850px}.legal-shell h1{font-size:clamp(38px,6vw,64px);margin:10px 0 8px}.legal-meta{color:var(--muted);font-size:12px;margin-bottom:30px}.legal-copy{color:#cbd5e1;font-size:17px;line-height:1.85;white-space:normal}
/* Admin appearance */
.admin-body{
--admin-surface:#fff;
--admin-surface-2:#fafbfe;
--admin-surface-3:#f0f3f8;
--admin-input:#fff;
--admin-input-border:#d5dce7;
--admin-sidebar:#0b1020;
--admin-sidebar-hover:#17203a;
--admin-sidebar-line:#202a42;
--admin-sidebar-text:#aab7ca;
--admin-accent:#6d28d9;
--admin-accent-soft:#ede9fe;
--admin-accent-text:#6d28d9;
--admin-success-bg:#dcfce7;
--admin-success-text:#166534;
--admin-success-line:#bbf7d0;
--admin-danger-bg:#fff1f2;
--admin-danger-text:#991b1b;
--admin-danger-line:#fecaca;
--admin-shadow:0 4px 18px rgba(31,45,75,.04);
background:var(--admin-bg);
color:var(--admin-ink);
color-scheme:light;
}
html[data-admin-theme="dark"] .admin-body{
--admin-bg:#0b1020;
--admin-ink:#e8edf6;
--admin-muted:#98a6ba;
--admin-line:#263149;
--admin-surface:#111827;
--admin-surface-2:#151e2f;
--admin-surface-3:#1b2639;
--admin-input:#0f1726;
--admin-input-border:#344159;
--admin-sidebar:#070b15;
--admin-sidebar-hover:#17213a;
--admin-sidebar-line:#202b43;
--admin-sidebar-text:#aebbd0;
--admin-accent:#8b5cf6;
--admin-accent-soft:#2b2145;
--admin-accent-text:#c4b5fd;
--admin-success-bg:#10291d;
--admin-success-text:#86efac;
--admin-success-line:#1f5138;
--admin-danger-bg:#35151d;
--admin-danger-text:#fda4af;
--admin-danger-line:#642334;
--admin-shadow:0 7px 24px rgba(0,0,0,.22);
color-scheme:dark;
}
.admin-sidebar{background:var(--admin-sidebar)}
.admin-nav a,.admin-sidebar-foot a,.admin-sidebar-foot button{color:var(--admin-sidebar-text)}
.admin-nav a:hover,.admin-nav a.active,.admin-sidebar-foot a:hover,.admin-sidebar-foot button:hover{background:var(--admin-sidebar-hover)}
.admin-sidebar-foot{border-color:var(--admin-sidebar-line)}
.admin-topbar,.admin-stat,.admin-card{background:var(--admin-surface);box-shadow:var(--admin-shadow)}
.admin-topbar{border-color:var(--admin-line)}
.admin-card .responsive-table th,.admin-card .responsive-table td{border-color:var(--admin-line);color:var(--admin-ink)}
.admin-card .responsive-table th{color:var(--admin-muted)}
.toggle-button{background:var(--admin-surface-3);color:var(--admin-ink)}
.toggle-button.on{background:var(--admin-success-bg);color:var(--admin-success-text)}
.inline-controls select,.crud-search input,.crud-field input,.crud-field select,.crud-field textarea{background:var(--admin-input);border-color:var(--admin-input-border);color:var(--admin-ink)}
.crud-field>span,.crud-check strong{color:var(--admin-ink)}
.crud-check{background:var(--admin-surface-2);border-color:var(--admin-line)}
.admin-flash{background:var(--admin-success-bg);color:var(--admin-success-text);border-color:var(--admin-success-line)}
.admin-flash.error,.admin-error{background:var(--admin-danger-bg);color:var(--admin-danger-text);border-color:var(--admin-danger-line)}
.danger-button{background:var(--admin-danger-bg);color:var(--admin-danger-text)}
.role-badge,.feature-icon{background:var(--admin-accent-soft);color:var(--admin-accent-text)}
.action-card:hover{border-color:var(--admin-accent)}
.action-card>span{color:var(--admin-accent-text)}
.admin-theme-switcher{display:inline-flex;align-items:center;padding:3px;border:1px solid var(--admin-line);border-radius:10px;background:var(--admin-surface-2)}
.admin-theme-switcher button{display:grid;place-items:center;width:32px;height:32px;padding:0;border:0;border-radius:7px;background:transparent;color:var(--admin-muted);font:inherit;font-size:15px;cursor:pointer;transition:background .15s ease,color .15s ease,box-shadow .15s ease}
.admin-theme-switcher button:hover{color:var(--admin-ink)}
.admin-theme-switcher button.active{background:var(--admin-surface);color:var(--admin-accent-text);box-shadow:0 1px 4px rgba(0,0,0,.12)}
.admin-topbar-actions{display:flex;align-items:center;gap:10px}
html[data-admin-theme="dark"] .admin-button{background:#29354a;color:#f8fafc}
html[data-admin-theme="dark"] .admin-button.primary{background:#7c3aed}
html[data-admin-theme="dark"] .admin-stat small{color:#c4b5fd}
html[data-admin-theme="dark"] .crud-field input:focus,html[data-admin-theme="dark"] .crud-field select:focus,html[data-admin-theme="dark"] .crud-field textarea:focus{border-color:#8b5cf6;box-shadow:0 0 0 3px rgba(139,92,246,.18)}
html[data-admin-theme="dark"] input[type="date"],html[data-admin-theme="dark"] input[type="datetime-local"],html[data-admin-theme="dark"] input[type="time"]{color-scheme:dark}
@media(max-width:640px){.admin-topbar-actions{margin-left:auto}.admin-theme-switcher button{width:29px;height:29px}.admin-topbar-actions .button{display:none}}
/* Keep admin appearance controls and Public Site action on one row. */
.admin-topbar > .admin-topbar-actions{
display:flex;
flex-direction:row;
flex-wrap:nowrap;
align-items:center;
justify-content:flex-end;
gap:10px;
min-width:max-content;
margin-left:auto;
}
.admin-topbar-actions .admin-theme-switcher,
.admin-topbar-actions .button{
flex:0 0 auto;
white-space:nowrap;
}
@media(max-width:640px){
.admin-topbar{padding:0 12px;gap:8px}
.admin-topbar > div:not(.admin-topbar-actions){min-width:0;overflow:hidden}
.admin-topbar > div:not(.admin-topbar-actions) strong,
.admin-topbar > div:not(.admin-topbar-actions) span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.admin-topbar > .admin-topbar-actions{display:flex;gap:6px;margin-left:auto}
.admin-theme-switcher{padding:2px}
.admin-theme-switcher button{width:28px;height:28px}
.admin-topbar-actions .button{display:inline-flex;min-height:32px;padding:0 10px;font-size:11px}
}
+26
View File
@@ -28,3 +28,29 @@ const setAdminMenu = (open) => {
document.querySelector('[data-admin-open]')?.addEventListener('click', () => setAdminMenu(true));
document.querySelector('[data-admin-close]')?.addEventListener('click', () => setAdminMenu(false));
adminOverlay?.addEventListener('click', () => setAdminMenu(false));
const adminThemeButtons = document.querySelectorAll('[data-admin-theme]');
const adminThemeMedia = window.matchMedia('(prefers-color-scheme: dark)');
const applyAdminTheme = (preference, persist = true) => {
if (!['light', 'dark', 'system'].includes(preference)) preference = 'system';
const resolved = preference === 'system' ? (adminThemeMedia.matches ? 'dark' : 'light') : preference;
document.documentElement.dataset.adminTheme = resolved;
document.documentElement.dataset.adminThemePreference = preference;
if (persist) localStorage.setItem('beatbook-admin-theme', preference);
adminThemeButtons.forEach((button) => {
const active = button.dataset.adminTheme === preference;
button.classList.toggle('active', active);
button.setAttribute('aria-pressed', String(active));
});
};
if (adminThemeButtons.length) {
applyAdminTheme(localStorage.getItem('beatbook-admin-theme') || 'system', false);
adminThemeButtons.forEach((button) => button.addEventListener('click', () => applyAdminTheme(button.dataset.adminTheme)));
adminThemeMedia.addEventListener?.('change', () => {
if ((localStorage.getItem('beatbook-admin-theme') || 'system') === 'system') applyAdminTheme('system', false);
});
}
+16 -1
View File
@@ -5,6 +5,14 @@
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>@yield('title', 'Administration') — BeatBook</title>
<script>
(() => {
const saved = localStorage.getItem('beatbook-admin-theme') || 'system';
const dark = saved === 'dark' || (saved === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.dataset.adminTheme = dark ? 'dark' : 'light';
document.documentElement.dataset.adminThemePreference = saved;
})();
</script>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="admin-body">
@@ -37,7 +45,14 @@
<header class="admin-topbar">
<button class="icon-button admin-menu" type="button" data-admin-open aria-label="Open menu">☰</button>
<div><strong>@yield('page-title', 'Administration')</strong><span>{{ auth()->user()->name }}</span></div>
<a class="button small" href="{{ route('home') }}">Public site</a>
<div class="admin-topbar-actions">
<div class="admin-theme-switcher" role="group" aria-label="Appearance">
<button type="button" data-admin-theme="light" title="Light mode" aria-label="Use light mode">☀</button>
<button type="button" data-admin-theme="system" title="System appearance" aria-label="Use system appearance">◐</button>
<button type="button" data-admin-theme="dark" title="Dark mode" aria-label="Use dark mode">☾</button>
</div>
<a class="button small" href="{{ route('home') }}">Public site</a>
</div>
</header>
@if(session('success'))<div class="admin-flash">{{ session('success') }}</div>@endif
@if(session('error'))<div class="admin-flash error">{{ session('error') }}</div>@endif