Files
beatbook/app/Http/Controllers/Admin/CrudController.php
T
2026-09-12 21:18:19 +01:00

311 lines
26 KiB
PHP

<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\AdminNotification;
use App\Models\AppSetting;
use App\Models\Booking;
use App\Models\Business;
use App\Models\ContentPage;
use App\Models\DjProfile;
use App\Models\LegalDocument;
use App\Models\Message;
use App\Models\Payment;
use App\Models\Review;
use App\Models\SavedReport;
use App\Models\SupportTicket;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\View\View;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Spatie\Permission\PermissionRegistrar;
class CrudController extends Controller
{
public function index(Request $request, string $resource): View
{
$definition = $this->definition($resource);
$query = $this->query($resource, $definition);
if ($search = trim((string) $request->query('q'))) {
$query->where(function (Builder $query) use ($definition, $search) {
foreach ($definition['search'] ?? [] as $column) {
$query->orWhere($column, 'like', '%'.$search.'%');
}
});
}
$records = $query->latest($definition['order_by'] ?? 'created_at')->paginate(20)->withQueryString();
return view('admin.crud.index', compact('resource', 'definition', 'records'));
}
public function create(string $resource): View
{
$definition = $this->definition($resource);
$record = null;
$options = $this->options($resource);
return view('admin.crud.form', compact('resource', 'definition', 'record', 'options'));
}
public function store(Request $request, string $resource): RedirectResponse
{
$definition = $this->definition($resource);
$data = $request->validate($this->rules($resource, null));
$record = $this->persist($resource, new $definition['model'], $data, true);
return redirect()->route('admin.resources.index', $resource)
->with('success', $definition['singular'].' created successfully.');
}
public function edit(string $resource, string $id): View
{
$definition = $this->definition($resource);
$record = $this->find($resource, $definition, $id);
$options = $this->options($resource);
return view('admin.crud.form', compact('resource', 'definition', 'record', 'options'));
}
public function update(Request $request, string $resource, string $id): RedirectResponse
{
$definition = $this->definition($resource);
$record = $this->find($resource, $definition, $id);
$data = $request->validate($this->rules($resource, $record));
$this->persist($resource, $record, $data, false);
return redirect()->route('admin.resources.index', $resource)
->with('success', $definition['singular'].' updated successfully.');
}
public function destroy(Request $request, string $resource, string $id): RedirectResponse
{
$definition = $this->definition($resource);
$record = $this->find($resource, $definition, $id);
if ($resource === 'users' && $record->is($request->user())) {
return back()->with('error', 'You cannot delete your own administrator account.');
}
if ($resource === 'roles' && in_array($record->name, ['admin', 'super-admin'], true)) {
return back()->with('error', 'The core administrator roles cannot be deleted.');
}
$record->delete();
return redirect()->route('admin.resources.index', $resource)
->with('success', $definition['singular'].' deleted.');
}
private function definition(string $resource): array
{
$definitions = [
'users' => [
'title' => 'Users', 'singular' => 'User', 'model' => User::class, 'search' => ['name', 'email'], 'with' => ['roles'],
'columns' => ['name' => 'Name', 'email' => 'Email', 'roles_text' => 'Role', 'created_at' => 'Joined'],
'fields' => [
'name' => ['label' => 'Name', 'type' => 'text'], 'email' => ['label' => 'Email', 'type' => 'email'], 'phone' => ['label' => 'Phone', 'type' => 'text'],
'role' => ['label' => 'Role', 'type' => 'select', 'options' => 'roles', 'nullable' => true], 'password' => ['label' => 'Password', 'type' => 'password', 'help' => 'Leave blank when editing to keep the current password.'],
'marketing_opt_in' => ['label' => 'Marketing opt-in', 'type' => 'checkbox'],
],
],
'clients' => [
'title' => 'Clients', 'singular' => 'Client', 'model' => User::class, 'search' => ['name', 'email'], 'with' => ['roles'], 'scope' => 'client',
'columns' => ['name' => 'Name', 'email' => 'Email', 'phone' => 'Phone', 'created_at' => 'Joined'],
'fields' => ['name' => ['label' => 'Name', 'type' => 'text'], 'email' => ['label' => 'Email', 'type' => 'email'], 'phone' => ['label' => 'Phone', 'type' => 'text'], 'password' => ['label' => 'Password', 'type' => 'password'], 'marketing_opt_in' => ['label' => 'Marketing opt-in', 'type' => 'checkbox']],
],
'djs' => [
'title' => 'DJs', 'singular' => 'DJ profile', 'model' => DjProfile::class, 'search' => ['stage_name', 'city', 'headline'], 'with' => ['user', 'business'],
'columns' => ['stage_name' => 'Stage name', 'user.name' => 'Account', 'city' => 'City', 'price_from' => 'From', 'is_published' => 'Published', 'is_featured' => 'Featured'],
'fields' => ['user_id' => ['label' => 'User', 'type' => 'select', 'options' => 'users'], 'business_id' => ['label' => 'Business', 'type' => 'select', 'options' => 'businesses', 'nullable' => true], 'stage_name' => ['label' => 'Stage name', 'type' => 'text'], 'slug' => ['label' => 'Slug', 'type' => 'text'], 'headline' => ['label' => 'Headline', 'type' => 'text'], 'bio' => ['label' => 'Biography', 'type' => 'textarea'], 'city' => ['label' => 'City', 'type' => 'text'], 'service_radius_miles' => ['label' => 'Service radius (miles)', 'type' => 'number'], 'price_from' => ['label' => 'Price from (£)', 'type' => 'number', 'step' => '0.01'], 'is_published' => ['label' => 'Published', 'type' => 'checkbox'], 'is_featured' => ['label' => 'Featured', 'type' => 'checkbox']],
],
'businesses' => [
'title' => 'Businesses', 'singular' => 'Business', 'model' => Business::class, 'search' => ['name', 'email', 'status'],
'columns' => ['name' => 'Name', 'email' => 'Email', 'phone' => 'Phone', 'status' => 'Status', 'verified_at' => 'Verified'],
'fields' => ['name' => ['label' => 'Name', 'type' => 'text'], 'slug' => ['label' => 'Slug', 'type' => 'text'], 'description' => ['label' => 'Description', 'type' => 'textarea'], 'email' => ['label' => 'Email', 'type' => 'email'], 'phone' => ['label' => 'Phone', 'type' => 'text'], 'website' => ['label' => 'Website', 'type' => 'url'], 'status' => ['label' => 'Status', 'type' => 'select', 'values' => ['active' => 'Active', 'suspended' => 'Suspended', 'pending' => 'Pending']], 'verified_at' => ['label' => 'Verified at', 'type' => 'datetime-local']],
],
'bookings' => [
'title' => 'Bookings', 'singular' => 'Booking', 'model' => Booking::class, 'search' => ['status'], 'with' => ['client', 'djProfile'], 'order_by' => 'event_date',
'columns' => ['client.name' => 'Client', 'djProfile.stage_name' => 'DJ', 'event_date' => 'Event date', 'total' => 'Total', 'deposit' => 'Deposit', 'status' => 'Status'],
'fields' => ['client_id' => ['label' => 'Client', 'type' => 'select', 'options' => 'users'], 'dj_profile_id' => ['label' => 'DJ', 'type' => 'select', 'options' => 'djs'], 'event_date' => ['label' => 'Event date', 'type' => 'date'], 'total' => ['label' => 'Total (£)', 'type' => 'number', 'step' => '0.01'], 'deposit' => ['label' => 'Deposit (£)', 'type' => 'number', 'step' => '0.01'], 'status' => ['label' => 'Status', 'type' => 'select', 'values' => ['pending' => 'Pending', 'confirmed' => 'Confirmed', 'completed' => 'Completed', 'cancelled' => 'Cancelled']], 'paid_at' => ['label' => 'Paid at', 'type' => 'datetime-local']],
],
'payments' => [
'title' => 'Payments', 'singular' => 'Payment', 'model' => Payment::class, 'search' => ['reference', 'provider', 'status'], 'with' => ['user', 'booking'],
'columns' => ['reference' => 'Reference', 'user.name' => 'User', 'amount' => 'Amount', 'provider' => 'Provider', 'status' => 'Status', 'paid_at' => 'Paid'],
'fields' => ['booking_id' => ['label' => 'Booking', 'type' => 'select', 'options' => 'bookings', 'nullable' => true], 'user_id' => ['label' => 'User', 'type' => 'select', 'options' => 'users', 'nullable' => true], 'amount' => ['label' => 'Amount (£)', 'type' => 'number', 'step' => '0.01'], 'provider' => ['label' => 'Provider', 'type' => 'select', 'values' => ['manual' => 'Manual', 'stripe' => 'Stripe', 'bank' => 'Bank transfer']], 'reference' => ['label' => 'Reference', 'type' => 'text'], 'status' => ['label' => 'Status', 'type' => 'select', 'values' => ['pending' => 'Pending', 'paid' => 'Paid', 'failed' => 'Failed', 'refunded' => 'Refunded']], 'paid_at' => ['label' => 'Paid at', 'type' => 'datetime-local'], 'notes' => ['label' => 'Notes', 'type' => 'textarea']],
],
'reviews' => [
'title' => 'Reviews', 'singular' => 'Review', 'model' => Review::class, 'search' => ['title', 'body', 'status'], 'with' => ['user', 'djProfile'],
'columns' => ['user.name' => 'User', 'djProfile.stage_name' => 'DJ', 'rating' => 'Rating', 'title' => 'Title', 'status' => 'Status'],
'fields' => ['booking_id' => ['label' => 'Booking', 'type' => 'select', 'options' => 'bookings'], 'user_id' => ['label' => 'User', 'type' => 'select', 'options' => 'users'], 'dj_profile_id' => ['label' => 'DJ', 'type' => 'select', 'options' => 'djs'], 'rating' => ['label' => 'Rating', 'type' => 'number', 'min' => 1, 'max' => 5], 'title' => ['label' => 'Title', 'type' => 'text'], 'body' => ['label' => 'Review', 'type' => 'textarea'], 'status' => ['label' => 'Status', 'type' => 'select', 'values' => ['pending' => 'Pending', 'published' => 'Published', 'rejected' => 'Rejected']], 'dj_reply' => ['label' => 'DJ reply', 'type' => 'textarea']],
],
'messages' => [
'title' => 'Messages', 'singular' => 'Message', 'model' => Message::class, 'search' => ['subject', 'body', 'status'], 'with' => ['fromUser', 'toUser'],
'columns' => ['subject' => 'Subject', 'fromUser.name' => 'From', 'toUser.name' => 'To', 'status' => 'Status', 'created_at' => 'Created'],
'fields' => ['from_user_id' => ['label' => 'From user', 'type' => 'select', 'options' => 'users', 'nullable' => true], 'to_user_id' => ['label' => 'To user', 'type' => 'select', 'options' => 'users', 'nullable' => true], 'subject' => ['label' => 'Subject', 'type' => 'text'], 'body' => ['label' => 'Message', 'type' => 'textarea'], 'status' => ['label' => 'Status', 'type' => 'select', 'values' => ['unread' => 'Unread', 'read' => 'Read', 'archived' => 'Archived']], 'read_at' => ['label' => 'Read at', 'type' => 'datetime-local']],
],
'notifications' => [
'title' => 'Notifications', 'singular' => 'Notification', 'model' => AdminNotification::class, 'search' => ['title', 'body', 'audience', 'status'],
'columns' => ['title' => 'Title', 'audience' => 'Audience', 'status' => 'Status', 'published_at' => 'Published'],
'fields' => ['title' => ['label' => 'Title', 'type' => 'text'], 'body' => ['label' => 'Message', 'type' => 'textarea'], 'audience' => ['label' => 'Audience', 'type' => 'select', 'values' => ['all' => 'Everyone', 'clients' => 'Clients', 'djs' => 'DJs', 'admins' => 'Admins']], 'status' => ['label' => 'Status', 'type' => 'select', 'values' => ['draft' => 'Draft', 'published' => 'Published', 'archived' => 'Archived']], 'published_at' => ['label' => 'Published at', 'type' => 'datetime-local']],
],
'support' => [
'title' => 'Support tickets', 'singular' => 'Support ticket', 'model' => SupportTicket::class, 'search' => ['subject', 'category', 'status', 'priority'], 'with' => ['user'],
'columns' => ['subject' => 'Subject', 'user.name' => 'User', 'category' => 'Category', 'priority' => 'Priority', 'status' => 'Status'],
'fields' => ['user_id' => ['label' => 'User', 'type' => 'select', 'options' => 'users', 'nullable' => true], 'subject' => ['label' => 'Subject', 'type' => 'text'], 'category' => ['label' => 'Category', 'type' => 'select', 'values' => ['general' => 'General', 'account' => 'Account', 'booking' => 'Booking', 'payment' => 'Payment', 'technical' => 'Technical']], 'priority' => ['label' => 'Priority', 'type' => 'select', 'values' => ['low' => 'Low', 'normal' => 'Normal', 'high' => 'High', 'urgent' => 'Urgent']], 'status' => ['label' => 'Status', 'type' => 'select', 'values' => ['open' => 'Open', 'in-progress' => 'In progress', 'resolved' => 'Resolved', 'closed' => 'Closed']], 'description' => ['label' => 'Description', 'type' => 'textarea'], 'resolution' => ['label' => 'Resolution', 'type' => 'textarea']],
],
'reports' => [
'title' => 'Saved reports', 'singular' => 'Report', 'model' => SavedReport::class, 'search' => ['name', 'type', 'description'],
'columns' => ['name' => 'Name', 'type' => 'Type', 'is_active' => 'Active', 'updated_at' => 'Updated'],
'fields' => ['name' => ['label' => 'Name', 'type' => 'text'], 'type' => ['label' => 'Type', 'type' => 'select', 'values' => ['overview' => 'Overview', 'bookings' => 'Bookings', 'revenue' => 'Revenue', 'users' => 'Users', 'djs' => 'DJs', 'reviews' => 'Reviews']], 'description' => ['label' => 'Description', 'type' => 'textarea'], 'filters' => ['label' => 'Filters (JSON)', 'type' => 'textarea'], 'is_active' => ['label' => 'Active', 'type' => 'checkbox']],
],
'content' => [
'title' => 'Content', 'singular' => 'Content page', 'model' => ContentPage::class, 'search' => ['title', 'slug', 'status'],
'columns' => ['title' => 'Title', 'slug' => 'Slug', 'status' => 'Status', 'published_at' => 'Published'],
'fields' => ['title' => ['label' => 'Title', 'type' => 'text'], 'slug' => ['label' => 'Slug', 'type' => 'text'], 'excerpt' => ['label' => 'Excerpt', 'type' => 'textarea'], 'body' => ['label' => 'Body', 'type' => 'textarea', 'large' => true], 'status' => ['label' => 'Status', 'type' => 'select', 'values' => ['draft' => 'Draft', 'published' => 'Published', 'archived' => 'Archived']], 'published_at' => ['label' => 'Published at', 'type' => 'datetime-local']],
],
'roles' => [
'title' => 'Roles & permissions', 'singular' => 'Role', 'model' => Role::class, 'search' => ['name'], 'with_count' => ['users'],
'columns' => ['name' => 'Role', 'users_count' => 'Users', 'created_at' => 'Created'],
'fields' => ['name' => ['label' => 'Role name', 'type' => 'text']],
],
'permissions' => [
'title' => 'Permissions', 'singular' => 'Permission', 'model' => Permission::class, 'search' => ['name'],
'columns' => ['name' => 'Permission', 'created_at' => 'Created', 'updated_at' => 'Updated'],
'fields' => ['name' => ['label' => 'Permission name', 'type' => 'text']],
],
'privacy' => [
'title' => 'Privacy & legal', 'singular' => 'Legal document', 'model' => LegalDocument::class, 'search' => ['type', 'title', 'version', 'status'],
'columns' => ['type' => 'Type', 'title' => 'Title', 'version' => 'Version', 'status' => 'Status', 'published_at' => 'Published'],
'fields' => ['type' => ['label' => 'Document type', 'type' => 'select', 'values' => ['privacy' => 'Privacy policy', 'cookies' => 'Cookie policy', 'terms' => 'Terms & conditions', 'dpa' => 'Data processing']], 'title' => ['label' => 'Title', 'type' => 'text'], 'version' => ['label' => 'Version', 'type' => 'text'], 'body' => ['label' => 'Document', 'type' => 'textarea', 'large' => true], 'status' => ['label' => 'Status', 'type' => 'select', 'values' => ['draft' => 'Draft', 'published' => 'Published', 'archived' => 'Archived']], 'published_at' => ['label' => 'Published at', 'type' => 'datetime-local']],
],
'settings' => [
'title' => 'Settings', 'singular' => 'Setting', 'model' => AppSetting::class, 'search' => ['key', 'label', 'group'],
'columns' => ['label' => 'Setting', 'key' => 'Key', 'value' => 'Value', 'group' => 'Group', 'type' => 'Type'],
'fields' => ['label' => ['label' => 'Label', 'type' => 'text'], 'key' => ['label' => 'Key', 'type' => 'text'], 'value' => ['label' => 'Value', 'type' => 'textarea'], 'type' => ['label' => 'Type', 'type' => 'select', 'values' => ['text' => 'Text', 'number' => 'Number', 'boolean' => 'Boolean', 'email' => 'Email', 'url' => 'URL', 'json' => 'JSON']], 'group' => ['label' => 'Group', 'type' => 'text'], 'description' => ['label' => 'Description', 'type' => 'textarea']],
],
];
abort_unless(isset($definitions[$resource]), 404);
return $definitions[$resource];
}
private function query(string $resource, array $definition): Builder
{
/** @var Builder $query */
$query = $definition['model']::query();
if (! empty($definition['with'])) {
$query->with($definition['with']);
}
if (! empty($definition['with_count'])) {
$query->withCount($definition['with_count']);
}
if (($definition['scope'] ?? null) === 'client') {
$query->whereHas('roles', fn (Builder $q) => $q->where('name', 'client'));
}
return $query;
}
private function find(string $resource, array $definition, string $id): Model
{
$query = $this->query($resource, $definition);
return $query->findOrFail($id);
}
private function rules(string $resource, ?Model $record): array
{
$id = $record?->getKey();
return match ($resource) {
'users', 'clients' => ['name' => ['required', 'string', 'max:255'], 'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($id)], 'phone' => ['nullable', 'string', 'max:50'], 'password' => [$record ? 'nullable' : 'required', 'string', 'min:8'], 'role' => ['nullable', 'string', 'max:50'], 'marketing_opt_in' => ['nullable', 'boolean']],
'djs' => ['user_id' => ['required', 'exists:users,id'], 'business_id' => ['nullable', 'exists:businesses,id'], 'stage_name' => ['required', 'string', 'max:255'], 'slug' => ['nullable', 'string', 'max:255', Rule::unique('dj_profiles')->ignore($id)], 'headline' => ['nullable', 'string', 'max:255'], 'bio' => ['nullable', 'string'], 'city' => ['required', 'string', 'max:255'], 'service_radius_miles' => ['required', 'integer', 'min:0', 'max:500'], 'price_from' => ['nullable', 'numeric', 'min:0'], 'is_published' => ['nullable', 'boolean'], 'is_featured' => ['nullable', 'boolean']],
'businesses' => ['name' => ['required', 'string', 'max:255'], 'slug' => ['nullable', 'string', 'max:255', Rule::unique('businesses')->ignore($id)], 'description' => ['nullable', 'string'], 'email' => ['nullable', 'email'], 'phone' => ['nullable', 'string', 'max:50'], 'website' => ['nullable', 'url'], 'status' => ['required', Rule::in(['active', 'suspended', 'pending'])], 'verified_at' => ['nullable', 'date']],
'bookings' => ['client_id' => ['required', 'exists:users,id'], 'dj_profile_id' => ['required', 'exists:dj_profiles,id'], 'event_date' => ['required', 'date'], 'total' => ['required', 'numeric', 'min:0'], 'deposit' => ['required', 'numeric', 'min:0'], 'status' => ['required', Rule::in(['pending', 'confirmed', 'completed', 'cancelled'])], 'paid_at' => ['nullable', 'date']],
'payments' => ['booking_id' => ['nullable', 'exists:bookings,id'], 'user_id' => ['nullable', 'exists:users,id'], 'amount' => ['required', 'numeric', 'min:0'], 'provider' => ['required', Rule::in(['manual', 'stripe', 'bank'])], 'reference' => ['nullable', 'string', 'max:255'], 'status' => ['required', Rule::in(['pending', 'paid', 'failed', 'refunded'])], 'paid_at' => ['nullable', 'date'], 'notes' => ['nullable', 'string']],
'reviews' => ['booking_id' => ['required', 'exists:bookings,id', Rule::unique('reviews')->ignore($id)], 'user_id' => ['required', 'exists:users,id'], 'dj_profile_id' => ['required', 'exists:dj_profiles,id'], 'rating' => ['required', 'integer', 'between:1,5'], 'title' => ['nullable', 'string', 'max:255'], 'body' => ['required', 'string'], 'status' => ['required', Rule::in(['pending', 'published', 'rejected'])], 'dj_reply' => ['nullable', 'string']],
'messages' => ['from_user_id' => ['nullable', 'exists:users,id'], 'to_user_id' => ['nullable', 'exists:users,id'], 'subject' => ['required', 'string', 'max:255'], 'body' => ['required', 'string'], 'status' => ['required', Rule::in(['unread', 'read', 'archived'])], 'read_at' => ['nullable', 'date']],
'notifications' => ['title' => ['required', 'string', 'max:255'], 'body' => ['required', 'string'], 'audience' => ['required', Rule::in(['all', 'clients', 'djs', 'admins'])], 'status' => ['required', Rule::in(['draft', 'published', 'archived'])], 'published_at' => ['nullable', 'date']],
'support' => ['user_id' => ['nullable', 'exists:users,id'], 'subject' => ['required', 'string', 'max:255'], 'category' => ['required', 'string', 'max:50'], 'priority' => ['required', Rule::in(['low', 'normal', 'high', 'urgent'])], 'status' => ['required', Rule::in(['open', 'in-progress', 'resolved', 'closed'])], 'description' => ['required', 'string'], 'resolution' => ['nullable', 'string']],
'reports' => ['name' => ['required', 'string', 'max:255'], 'type' => ['required', 'string', 'max:50'], 'description' => ['nullable', 'string'], 'filters' => ['nullable', 'json'], 'is_active' => ['nullable', 'boolean']],
'content' => ['title' => ['required', 'string', 'max:255'], 'slug' => ['nullable', 'string', 'max:255', Rule::unique('content_pages')->ignore($id)], 'excerpt' => ['nullable', 'string'], 'body' => ['nullable', 'string'], 'status' => ['required', Rule::in(['draft', 'published', 'archived'])], 'published_at' => ['nullable', 'date']],
'roles' => ['name' => ['required', 'string', 'max:125', Rule::unique('roles')->where('guard_name', 'web')->ignore($id)]],
'permissions' => ['name' => ['required', 'string', 'max:125', Rule::unique('permissions')->where('guard_name', 'web')->ignore($id)]],
'privacy' => ['type' => ['required', 'string', 'max:50'], 'title' => ['required', 'string', 'max:255'], 'version' => ['required', 'string', 'max:50'], 'body' => ['required', 'string'], 'status' => ['required', Rule::in(['draft', 'published', 'archived'])], 'published_at' => ['nullable', 'date']],
'settings' => ['label' => ['required', 'string', 'max:255'], 'key' => ['required', 'string', 'max:255', Rule::unique('app_settings')->ignore($id)], 'value' => ['nullable', 'string'], 'type' => ['required', 'string', 'max:30'], 'group' => ['required', 'string', 'max:100'], 'description' => ['nullable', 'string']],
default => abort(404),
};
}
private function persist(string $resource, Model $record, array $data, bool $creating): Model
{
foreach (['marketing_opt_in', 'is_published', 'is_featured', 'is_active'] as $boolean) {
if (array_key_exists($boolean, $data)) {
$data[$boolean] = (bool) $data[$boolean];
}
}
if (isset($data['slug']) && blank($data['slug'])) {
$data['slug'] = Str::slug($data['stage_name'] ?? $data['name'] ?? $data['title'] ?? Str::random(8));
}
if ($resource === 'reports' && isset($data['filters'])) {
$data['filters'] = blank($data['filters']) ? null : json_decode($data['filters'], true, flags: JSON_THROW_ON_ERROR);
}
if (in_array($resource, ['users', 'clients'], true)) {
$role = $resource === 'clients' ? 'client' : ($data['role'] ?? null);
unset($data['role']);
if (blank($data['password'] ?? null)) {
unset($data['password']);
} else {
$data['password'] = Hash::make($data['password']);
}
$record->fill($data)->save();
if ($role) {
$roleModel = Role::firstOrCreate(['name' => $role, 'guard_name' => 'web']);
$record->syncRoles([$roleModel]);
app(PermissionRegistrar::class)->forgetCachedPermissions();
}
return $record;
}
if (in_array($resource, ['roles', 'permissions'], true)) {
$record->fill(['name' => $data['name'], 'guard_name' => 'web'])->save();
app(PermissionRegistrar::class)->forgetCachedPermissions();
return $record;
}
$record->fill($data)->save();
return $record;
}
private function options(string $resource): array
{
$roles = Role::query()->orderBy('name')->pluck('name', 'name')->all();
foreach (['client', 'dj', 'support', 'moderator', 'admin', 'super-admin'] as $role) {
$roles[$role] ??= Str::headline($role);
}
return [
'roles' => $roles,
'users' => User::query()->orderBy('name')->pluck('name', 'id')->all(),
'businesses' => Business::query()->orderBy('name')->pluck('name', 'id')->all(),
'djs' => DjProfile::query()->orderBy('stage_name')->pluck('stage_name', 'id')->all(),
'bookings' => Booking::query()->with(['client', 'djProfile'])->latest('event_date')->get()->mapWithKeys(fn (Booking $b) => [$b->id => ($b->client?->name ?? 'Client').' · '.($b->djProfile?->stage_name ?? 'DJ').' · '.$b->event_date?->format('j M Y')])->all(),
];
}
}