359 lines
11 KiB
PHP
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();
|
|
}
|
|
}
|