php-tenant/View/Modal/Admin/WorkspaceManager.php
Snider 86dbf4e763 fix: namespace to Core\Mod\Tenant, restructure package
- Changed namespace from Core\Core\Tenant to Core\Mod\Tenant
- Moved src/ contents to root
- Removed Host UK extension files (admin.php, MemberManager, TeamManager)
- Fixed composer.json autoload paths
2026-01-27 00:58:42 +00:00

666 lines
22 KiB
PHP

<?php
namespace Core\Mod\Tenant\View\Modal\Admin;
use Core\Mod\Tenant\Models\User;
use Core\Mod\Tenant\Models\Workspace;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Computed;
use Livewire\Component;
use Livewire\WithPagination;
class WorkspaceManager extends Component
{
use WithPagination;
// Search and filtering
public string $search = '';
// Edit modal state
public ?int $editingId = null;
public string $name = '';
public string $slug = '';
public bool $isActive = true;
// Transfer modal state
public bool $showTransferModal = false;
public ?int $sourceWorkspaceId = null;
public ?int $targetWorkspaceId = null;
public array $selectedResourceTypes = [];
// Change owner modal state
public bool $showOwnerModal = false;
public ?int $ownerWorkspaceId = null;
public ?int $newOwnerId = null;
// Resource viewer modal state
public bool $showResourcesModal = false;
public ?int $resourcesWorkspaceId = null;
public ?string $resourcesType = null;
public array $selectedResources = [];
public ?int $resourcesTargetWorkspaceId = null;
// Provision modal state
public bool $showProvisionModal = false;
public ?int $provisionWorkspaceId = null;
public ?string $provisionType = null;
public string $provisionName = '';
public string $provisionUrl = '';
public string $provisionSlug = '';
// Action messages
public string $actionMessage = '';
public string $actionType = '';
protected $queryString = [
'search' => ['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']);
}
}