['except' => ''], ]; protected array $rules = [ 'name' => 'required|string|max:255', 'slug' => 'required|string|max:255|alpha_dash', 'isActive' => 'boolean', ]; public function mount(): void { if (! auth()->user()?->isHades()) { abort(403, 'Hades tier required for workspace administration.'); } } public function updatingSearch(): void { $this->resetPage(); } #[Computed] public function workspaces() { return Workspace::query() ->withCount($this->getAvailableRelations()) ->when($this->search, function ($query) { $query->where(function ($q) { $q->where('name', 'like', "%{$this->search}%") ->orWhere('slug', 'like', "%{$this->search}%"); }); }) ->orderBy('name') ->paginate(20); } /** * Get relations that are available for counting. * Filters out relations whose models don't exist yet or have incompatible schemas. */ protected function getAvailableRelations(): array { $relations = []; // Check each relation's model exists and has workspace_id column $checks = [ 'bioPages' => ['model' => \Core\Mod\Web\Models\Page::class, 'table' => 'pages'], 'bioProjects' => ['model' => \Core\Mod\Web\Models\Project::class, 'table' => 'page_projects'], 'socialAccounts' => ['model' => \Core\Mod\Social\Models\Account::class, 'table' => 'social_accounts'], 'analyticsSites' => ['model' => \Core\Mod\Analytics\Models\Website::class, 'table' => 'analytics_websites'], 'trustWidgets' => ['model' => \Core\Mod\Trust\Models\Campaign::class, 'table' => 'trust_campaigns'], 'notificationSites' => ['model' => \Core\Mod\Notify\Models\PushWebsite::class, 'table' => 'push_websites'], ]; $schema = \Illuminate\Support\Facades\Schema::getFacadeRoot(); foreach ($checks as $relation => $info) { if (class_exists($info['model'])) { // Verify the table has workspace_id column try { if ($schema->hasColumn($info['table'], 'workspace_id')) { $relations[] = $relation; } } catch (\Exception $e) { // Table might not exist yet, skip } } } return $relations; } #[Computed] public function allWorkspaces() { return Workspace::orderBy('name')->get(['id', 'name', 'slug']); } #[Computed] public function resourceTypes(): array { $types = []; $schema = \Illuminate\Support\Facades\Schema::getFacadeRoot(); // Only include resource types for models that exist and have valid relations $checks = [ 'bio_pages' => ['model' => \Core\Mod\Web\Models\Page::class, 'table' => 'pages', 'label' => 'Bio Pages', 'relation' => 'bioPages', 'icon' => 'link'], 'bio_projects' => ['model' => \Core\Mod\Web\Models\Project::class, 'table' => 'page_projects', 'label' => 'Bio Projects', 'relation' => 'bioProjects', 'icon' => 'folder'], 'social_accounts' => ['model' => \Core\Mod\Social\Models\Account::class, 'table' => 'social_accounts', 'label' => 'Social Accounts', 'relation' => 'socialAccounts', 'icon' => 'share-nodes'], 'analytics_sites' => ['model' => \Core\Mod\Analytics\Models\Website::class, 'table' => 'analytics_websites', 'label' => 'Analytics Sites', 'relation' => 'analyticsSites', 'icon' => 'chart-line'], 'trust_widgets' => ['model' => \Core\Mod\Trust\Models\Campaign::class, 'table' => 'trust_campaigns', 'label' => 'Trust Campaigns', 'relation' => 'trustWidgets', 'icon' => 'shield-check'], 'notification_sites' => ['model' => \Core\Mod\Notify\Models\PushWebsite::class, 'table' => 'push_websites', 'label' => 'Notification Sites', 'relation' => 'notificationSites', 'icon' => 'bell'], ]; foreach ($checks as $key => $info) { if (class_exists($info['model'])) { try { if ($schema->hasColumn($info['table'], 'workspace_id')) { $types[$key] = [ 'label' => $info['label'], 'relation' => $info['relation'], 'icon' => $info['icon'], ]; } } catch (\Exception $e) { // Table might not exist yet, skip } } } return $types; } public function openEdit(int $id): void { $workspace = Workspace::findOrFail($id); $this->editingId = $id; $this->name = $workspace->name; $this->slug = $workspace->slug; $this->isActive = $workspace->is_active; } public function closeEdit(): void { $this->editingId = null; $this->reset(['name', 'slug', 'isActive']); $this->resetErrorBag(); } public function save(): void { $this->validate(); $workspace = Workspace::findOrFail($this->editingId); // Check if slug is unique (excluding current workspace) $slugExists = Workspace::where('slug', $this->slug) ->where('id', '!=', $this->editingId) ->exists(); if ($slugExists) { $this->addError('slug', 'This slug is already in use.'); return; } $workspace->update([ 'name' => $this->name, 'slug' => $this->slug, 'is_active' => $this->isActive, ]); $this->closeEdit(); $this->actionMessage = "Workspace '{$workspace->name}' updated successfully."; $this->actionType = 'success'; unset($this->workspaces); } public function delete(int $id): void { $workspace = Workspace::withCount($this->getAvailableRelations())->findOrFail($id); // Check for resources (safely get counts that might not exist) $totalResources = ($workspace->bio_pages_count ?? 0) + ($workspace->bio_projects_count ?? 0) + ($workspace->social_accounts_count ?? 0) + ($workspace->analytics_sites_count ?? 0) + ($workspace->trust_widgets_count ?? 0) + ($workspace->notification_sites_count ?? 0) + ($workspace->orders_count ?? 0); if ($totalResources > 0) { $this->actionMessage = "Cannot delete workspace '{$workspace->name}'. It has {$totalResources} resources. Transfer or delete them first."; $this->actionType = 'error'; return; } // Check for users if ($workspace->users()->count() > 0) { $this->actionMessage = "Cannot delete workspace '{$workspace->name}'. It still has users assigned."; $this->actionType = 'error'; return; } $workspaceName = $workspace->name; $workspace->delete(); $this->actionMessage = "Workspace '{$workspaceName}' deleted successfully."; $this->actionType = 'success'; unset($this->workspaces); } public function openTransfer(int $workspaceId): void { $this->sourceWorkspaceId = $workspaceId; $this->targetWorkspaceId = null; $this->selectedResourceTypes = []; $this->showTransferModal = true; } public function closeTransfer(): void { $this->showTransferModal = false; $this->reset(['sourceWorkspaceId', 'targetWorkspaceId', 'selectedResourceTypes']); } public function executeTransfer(): void { if (! $this->sourceWorkspaceId || ! $this->targetWorkspaceId) { $this->actionMessage = 'Please select both source and target workspaces.'; $this->actionType = 'error'; return; } if ($this->sourceWorkspaceId === $this->targetWorkspaceId) { $this->actionMessage = 'Source and target workspaces cannot be the same.'; $this->actionType = 'error'; return; } if (empty($this->selectedResourceTypes)) { $this->actionMessage = 'Please select at least one resource type to transfer.'; $this->actionType = 'error'; return; } $source = Workspace::findOrFail($this->sourceWorkspaceId); $target = Workspace::findOrFail($this->targetWorkspaceId); $resourceTypes = $this->resourceTypes; $transferred = []; DB::transaction(function () use ($source, $target, $resourceTypes, &$transferred) { foreach ($this->selectedResourceTypes as $type) { if (! isset($resourceTypes[$type])) { continue; } $relation = $resourceTypes[$type]['relation']; $count = $source->{$relation}()->count(); if ($count > 0) { $source->{$relation}()->update(['workspace_id' => $target->id]); $transferred[$resourceTypes[$type]['label']] = $count; } } }); $this->closeTransfer(); if (empty($transferred)) { $this->actionMessage = 'No resources were transferred (source had no resources of selected types).'; $this->actionType = 'warning'; } else { $summary = collect($transferred) ->map(fn ($count, $label) => "{$count} {$label}") ->join(', '); $this->actionMessage = "Transferred {$summary} from '{$source->name}' to '{$target->name}'."; $this->actionType = 'success'; } unset($this->workspaces); } #[Computed] public function allUsers() { return User::orderBy('name')->get(['id', 'name', 'email']); } public function openChangeOwner(int $workspaceId): void { $workspace = Workspace::findOrFail($workspaceId); $this->ownerWorkspaceId = $workspaceId; $this->newOwnerId = $workspace->owner()?->id; $this->showOwnerModal = true; } public function closeChangeOwner(): void { $this->showOwnerModal = false; $this->reset(['ownerWorkspaceId', 'newOwnerId']); } public function changeOwner(): void { if (! $this->ownerWorkspaceId || ! $this->newOwnerId) { $this->actionMessage = 'Please select a new owner.'; $this->actionType = 'error'; return; } $workspace = Workspace::findOrFail($this->ownerWorkspaceId); $newOwner = User::findOrFail($this->newOwnerId); $oldOwner = $workspace->owner(); DB::transaction(function () use ($workspace, $newOwner, $oldOwner) { // Remove owner role from current owner (if exists) if ($oldOwner) { $workspace->users()->updateExistingPivot($oldOwner->id, ['role' => 'member']); } // Check if new owner is already a member if ($workspace->users()->where('user_id', $newOwner->id)->exists()) { // Update existing membership to owner $workspace->users()->updateExistingPivot($newOwner->id, ['role' => 'owner']); } else { // Add new owner to workspace $workspace->users()->attach($newOwner->id, ['role' => 'owner']); } }); $this->closeChangeOwner(); $this->actionMessage = "Ownership of '{$workspace->name}' transferred to {$newOwner->name}."; $this->actionType = 'success'; unset($this->workspaces); } public function openResources(int $workspaceId, string $type): void { $this->resourcesWorkspaceId = $workspaceId; $this->resourcesType = $type; $this->selectedResources = []; $this->resourcesTargetWorkspaceId = null; $this->showResourcesModal = true; } public function closeResources(): void { $this->showResourcesModal = false; $this->reset(['resourcesWorkspaceId', 'resourcesType', 'selectedResources', 'resourcesTargetWorkspaceId']); } #[Computed] public function currentResources(): array { if (! $this->resourcesWorkspaceId || ! $this->resourcesType) { return []; } $resourceTypes = $this->resourceTypes; if (! isset($resourceTypes[$this->resourcesType])) { return []; } $workspace = Workspace::find($this->resourcesWorkspaceId); if (! $workspace) { return []; } $relation = $resourceTypes[$this->resourcesType]['relation']; return $workspace->{$relation}() ->get() ->map(function ($item) { return [ 'id' => $item->id, 'name' => $item->name ?? $item->title ?? "#{$item->id}", 'detail' => $item->url ?? $item->domain ?? $item->email ?? $item->slug ?? null, 'created_at' => $item->created_at?->format('d M Y'), ]; }) ->toArray(); } public function toggleResourceSelection(int $id): void { if (in_array($id, $this->selectedResources)) { $this->selectedResources = array_values(array_diff($this->selectedResources, [$id])); } else { $this->selectedResources[] = $id; } } public function selectAllResources(): void { $this->selectedResources = collect($this->currentResources)->pluck('id')->toArray(); } public function deselectAllResources(): void { $this->selectedResources = []; } public function transferSelectedResources(): void { if (empty($this->selectedResources)) { $this->actionMessage = 'Please select at least one resource to transfer.'; $this->actionType = 'error'; return; } if (! $this->resourcesTargetWorkspaceId) { $this->actionMessage = 'Please select a target workspace.'; $this->actionType = 'error'; return; } if ($this->resourcesWorkspaceId === $this->resourcesTargetWorkspaceId) { $this->actionMessage = 'Source and target workspaces cannot be the same.'; $this->actionType = 'error'; return; } $resourceTypes = $this->resourceTypes; if (! isset($resourceTypes[$this->resourcesType])) { $this->actionMessage = 'Invalid resource type.'; $this->actionType = 'error'; return; } $workspace = Workspace::findOrFail($this->resourcesWorkspaceId); $target = Workspace::findOrFail($this->resourcesTargetWorkspaceId); $relation = $resourceTypes[$this->resourcesType]['relation']; $label = $resourceTypes[$this->resourcesType]['label']; $count = $workspace->{$relation}() ->whereIn('id', $this->selectedResources) ->update(['workspace_id' => $target->id]); $this->closeResources(); $this->actionMessage = "Transferred {$count} {$label} from '{$workspace->name}' to '{$target->name}'."; $this->actionType = 'success'; unset($this->workspaces); } public function openProvision(int $workspaceId, string $type): void { $this->provisionWorkspaceId = $workspaceId; $this->provisionType = $type; $this->provisionName = ''; $this->provisionUrl = ''; $this->showProvisionModal = true; } public function closeProvision(): void { $this->showProvisionModal = false; $this->reset(['provisionWorkspaceId', 'provisionType', 'provisionName', 'provisionUrl', 'provisionSlug']); } #[Computed] public function provisionConfig(): array { return [ 'bio_pages' => [ 'label' => 'Bio Page', 'icon' => 'link', 'color' => 'blue', 'fields' => ['name', 'slug'], 'model' => \Core\Mod\Web\Models\Page::class, 'defaults' => ['type' => 'page', 'is_enabled' => true], ], 'social_accounts' => [ 'label' => 'Social Account', 'icon' => 'share-nodes', 'color' => 'purple', 'fields' => ['name'], 'model' => \Core\Mod\Social\Models\Account::class, 'defaults' => ['provider' => 'manual', 'status' => 'active'], ], 'analytics_sites' => [ 'label' => 'Analytics Site', 'icon' => 'chart-line', 'color' => 'cyan', 'fields' => ['name', 'url'], 'model' => \Core\Mod\Analytics\Models\Website::class, 'defaults' => ['tracking_enabled' => true, 'is_enabled' => true], ], 'trust_widgets' => [ 'label' => 'Trust Campaign', 'icon' => 'shield-check', 'color' => 'emerald', 'fields' => ['name'], 'model' => \Core\Mod\Trust\Models\Campaign::class, 'defaults' => ['status' => 'draft'], ], 'notification_sites' => [ 'label' => 'Notification Site', 'icon' => 'bell', 'color' => 'amber', 'fields' => ['name', 'url'], 'model' => \Core\Mod\Notify\Models\PushWebsite::class, 'defaults' => ['status' => 'active'], ], ]; } public function provisionResource(): void { $config = $this->provisionConfig[$this->provisionType] ?? null; if (! $config || ! class_exists($config['model'])) { $this->actionMessage = 'Invalid resource type or model not available.'; $this->actionType = 'error'; return; } if (empty($this->provisionName)) { $this->actionMessage = 'Please enter a name.'; $this->actionType = 'error'; return; } if (in_array('url', $config['fields']) && empty($this->provisionUrl)) { $this->actionMessage = 'Please enter a URL.'; $this->actionType = 'error'; return; } if (in_array('slug', $config['fields']) && empty($this->provisionSlug)) { $this->actionMessage = 'Please enter a slug.'; $this->actionType = 'error'; return; } $workspace = Workspace::findOrFail($this->provisionWorkspaceId); $data = array_merge($config['defaults'], [ 'workspace_id' => $workspace->id, ]); // Handle name - for bio pages it goes in settings if ($this->provisionType === 'bio_pages') { $data['settings'] = ['page_title' => $this->provisionName]; } else { $data['name'] = $this->provisionName; } // Add slug for bio pages if (in_array('slug', $config['fields']) && $this->provisionSlug) { $data['url'] = \Illuminate\Support\Str::slug($this->provisionSlug); } // Add URL-related fields if applicable if (in_array('url', $config['fields']) && $this->provisionUrl) { $url = $this->provisionUrl; if (! str_starts_with($url, 'http://') && ! str_starts_with($url, 'https://')) { $url = 'https://'.$url; } $parsed = parse_url($url); $data['url'] = $url; $data['host'] = $parsed['host'] ?? null; $data['scheme'] = $parsed['scheme'] ?? 'https'; } // Add user_id if the model expects it if (auth()->check()) { $data['user_id'] = auth()->id(); } try { $config['model']::create($data); $this->closeProvision(); $this->actionMessage = "{$config['label']} '{$this->provisionName}' created in '{$workspace->name}'."; $this->actionType = 'success'; unset($this->workspaces); } catch (\Exception $e) { $this->actionMessage = "Failed to create resource: {$e->getMessage()}"; $this->actionType = 'error'; } } public function getStats(): array { return [ 'total' => Workspace::count(), 'active' => Workspace::where('is_active', true)->count(), 'inactive' => Workspace::where('is_active', false)->count(), ]; } public function render() { return view('tenant::admin.workspace-manager', [ 'stats' => $this->getStats(), ])->layout('hub::admin.layouts.app', ['title' => 'Workspace Manager']); } }