TenantAtlas/app/Filament/Pages/Onboarding/TenantOnboardingTaskBoard.php
2026-02-01 12:20:18 +01:00

359 lines
11 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Filament\Pages\Onboarding;
use App\Jobs\Onboarding\OnboardingConnectionDiagnosticsJob;
use App\Jobs\Onboarding\OnboardingConsentStatusJob;
use App\Jobs\Onboarding\OnboardingInitialSyncJob;
use App\Jobs\Onboarding\OnboardingVerifyPermissionsJob;
use App\Models\OnboardingEvidence;
use App\Models\OnboardingSession;
use App\Models\ProviderConnection;
use App\Models\Tenant;
use App\Models\User;
use App\Services\Auth\CapabilityResolver;
use App\Services\OperationRunService;
use App\Support\Auth\Capabilities;
use App\Support\Badges\BadgeCatalog;
use App\Support\Badges\BadgeDomain;
use App\Support\Onboarding\OnboardingFixHints;
use App\Support\Onboarding\OnboardingTaskCatalog;
use App\Support\Onboarding\OnboardingTaskType;
use App\Support\OperationRunLinks;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
class TenantOnboardingTaskBoard extends Page
{
protected static bool $shouldRegisterNavigation = false;
protected static ?string $slug = 'onboarding/tasks';
protected static ?string $title = 'Onboarding task board';
protected string $view = 'filament.pages.onboarding.tenant-onboarding-task-board';
public ?OnboardingSession $session = null;
public bool $canStartProviderTasks = false;
/**
* @var array<string, string>
*/
public array $runUrls = [];
public function mount(): void
{
$tenant = Tenant::current();
$user = auth()->user();
if (! $user instanceof User) {
abort(403, 'Not allowed');
}
/** @var CapabilityResolver $resolver */
$resolver = app(CapabilityResolver::class);
$this->canStartProviderTasks = $resolver->can($user, $tenant, Capabilities::PROVIDER_RUN);
$activeSession = OnboardingSession::query()
->where('tenant_id', $tenant->getKey())
->whereIn('status', ['draft', 'in_progress'])
->latest('id')
->first();
if (! $activeSession instanceof OnboardingSession) {
$this->redirect(TenantOnboardingWizard::getUrl(tenant: $tenant));
return;
}
$this->session = $activeSession;
if ($activeSession->current_step < 4) {
$this->redirect(TenantOnboardingWizard::getUrl(tenant: $tenant));
}
}
/**
* @return array<string, string>
*/
public function latestEvidenceStatusByTaskType(): array
{
$tenant = Tenant::current();
$evidence = OnboardingEvidence::query()
->where('tenant_id', $tenant->getKey())
->whereIn('task_type', OnboardingTaskType::all())
->orderByDesc('recorded_at')
->get();
$byTask = [];
foreach ($evidence as $row) {
if (! isset($byTask[$row->task_type])) {
$byTask[$row->task_type] = $row->status;
}
}
return $byTask;
}
/**
* @return array<string, OnboardingEvidence>
*/
public function latestEvidenceByTaskType(): array
{
$tenant = Tenant::current();
$evidence = OnboardingEvidence::query()
->where('tenant_id', $tenant->getKey())
->whereIn('task_type', OnboardingTaskType::all())
->orderByDesc('recorded_at')
->get();
$byTask = [];
foreach ($evidence as $row) {
if (! isset($byTask[$row->task_type])) {
$byTask[$row->task_type] = $row;
}
}
return $byTask;
}
/**
* @return array<int, array{
* task_type: string,
* title: string,
* step: int,
* prerequisites: array<int, string>,
* status: string,
* badge: \App\Support\Badges\BadgeSpec,
* evidence: OnboardingEvidence|null,
* prerequisites_met: bool,
* unmet_prerequisites: array<int, string>,
* }>
*/
public function taskRows(): array
{
$statuses = $this->latestEvidenceStatusByTaskType();
$evidenceByTask = $this->latestEvidenceByTaskType();
return collect(OnboardingTaskCatalog::all())
->map(function (array $task) use ($statuses, $evidenceByTask): array {
$taskType = $task['task_type'];
$status = $statuses[$taskType] ?? 'unknown';
$unmet = OnboardingTaskCatalog::unmetPrerequisites($taskType, $statuses);
return [
'task_type' => $taskType,
'title' => $task['title'],
'step' => $task['step'],
'prerequisites' => $task['prerequisites'],
'status' => $status,
'badge' => BadgeCatalog::spec(BadgeDomain::OnboardingTaskStatus, $status),
'evidence' => $evidenceByTask[$taskType] ?? null,
'prerequisites_met' => count($unmet) === 0,
'unmet_prerequisites' => $unmet,
];
})
->values()
->all();
}
/**
* @return array<int, string>
*/
public function fixHintsFor(?string $reasonCode): array
{
return OnboardingFixHints::forReason($reasonCode);
}
/**
* @return array<int, array{
* recorded_at: string,
* task_type: string,
* status: string,
* badge: \App\Support\Badges\BadgeSpec,
* reason_code: string|null,
* message: string|null,
* run_url: string|null,
* }>
*/
public function recentEvidenceRows(int $limit = 20): array
{
$tenant = Tenant::current();
$evidence = OnboardingEvidence::query()
->where('tenant_id', $tenant->getKey())
->orderByDesc('recorded_at')
->limit($limit)
->with('operationRun')
->get();
return $evidence
->map(function (OnboardingEvidence $row) use ($tenant): array {
$runUrl = null;
if ($row->operationRun) {
$runUrl = OperationRunLinks::view($row->operationRun, $tenant);
}
return [
'recorded_at' => $row->recorded_at?->toDateTimeString() ?? '',
'task_type' => $row->task_type,
'status' => $row->status,
'badge' => BadgeCatalog::spec(BadgeDomain::OnboardingTaskStatus, $row->status),
'reason_code' => $row->reason_code,
'message' => $row->message,
'run_url' => $runUrl,
];
})
->values()
->all();
}
public function startTask(string $taskType): void
{
if (! $this->canStartProviderTasks) {
abort(403);
}
$tenant = Tenant::current();
$user = auth()->user();
if (! $user instanceof User) {
abort(403, 'Not allowed');
}
if (! $this->session instanceof OnboardingSession) {
Notification::make()
->title('No onboarding session')
->danger()
->send();
return;
}
if ($this->session->current_step < 4) {
$this->redirect(TenantOnboardingWizard::getUrl(tenant: $tenant));
return;
}
if (! in_array($taskType, OnboardingTaskType::all(), true)) {
Notification::make()
->title('Unknown task')
->danger()
->send();
return;
}
$latestStatuses = $this->latestEvidenceStatusByTaskType();
if (! OnboardingTaskCatalog::prerequisitesMet($taskType, $latestStatuses)) {
Notification::make()
->title('Prerequisites not met')
->body('Complete required tasks first.')
->warning()
->send();
return;
}
$connectionId = $this->session->provider_connection_id;
if (! is_int($connectionId)) {
Notification::make()
->title('Select a provider connection first')
->warning()
->send();
return;
}
$connection = ProviderConnection::query()
->where('tenant_id', $tenant->getKey())
->whereKey($connectionId)
->first();
if (! $connection instanceof ProviderConnection) {
Notification::make()
->title('Selected provider connection not found')
->danger()
->send();
return;
}
/** @var OperationRunService $runs */
$runs = app(OperationRunService::class);
$run = $runs->ensureRunWithIdentity(
tenant: $tenant,
type: $taskType,
identityInputs: ['task_type' => $taskType],
context: [
'task_type' => $taskType,
'onboarding_session_id' => (int) $this->session->getKey(),
'provider_connection_id' => (int) $connection->getKey(),
],
initiator: $user,
);
$this->runUrls[$taskType] = OperationRunLinks::view($run, $tenant);
if (! $run->wasRecentlyCreated) {
Notification::make()
->title('Task already queued')
->body('A run is already queued or running. Use the link to monitor progress.')
->warning()
->send();
return;
}
match ($taskType) {
OnboardingTaskType::VerifyPermissions => OnboardingVerifyPermissionsJob::dispatch(
tenantId: (int) $tenant->getKey(),
userId: (int) $user->getKey(),
providerConnectionId: (int) $connection->getKey(),
onboardingSessionId: (int) $this->session->getKey(),
operationRun: $run,
),
OnboardingTaskType::ConsentStatus => OnboardingConsentStatusJob::dispatch(
tenantId: (int) $tenant->getKey(),
userId: (int) $user->getKey(),
providerConnectionId: (int) $connection->getKey(),
onboardingSessionId: (int) $this->session->getKey(),
operationRun: $run,
),
OnboardingTaskType::ConnectionDiagnostics => OnboardingConnectionDiagnosticsJob::dispatch(
tenantId: (int) $tenant->getKey(),
userId: (int) $user->getKey(),
providerConnectionId: (int) $connection->getKey(),
onboardingSessionId: (int) $this->session->getKey(),
operationRun: $run,
),
OnboardingTaskType::InitialSync => OnboardingInitialSyncJob::dispatch(
tenantId: (int) $tenant->getKey(),
userId: (int) $user->getKey(),
providerConnectionId: (int) $connection->getKey(),
onboardingSessionId: (int) $this->session->getKey(),
operationRun: $run,
),
default => null,
};
Notification::make()
->title('Task queued')
->success()
->send();
}
}