label('Run Inventory Sync') ->icon('heroicon-o-arrow-path') ->color('warning') ->form([ Select::make('policy_types') ->label('Policy types') ->multiple() ->searchable() ->preload() ->native(false) ->hintActions([ fn (Select $component): HintAction => HintAction::make('select_all_policy_types') ->label('Select all') ->link() ->size(Size::Small) ->action(function (InventorySyncService $inventorySyncService) use ($component): void { $component->state($inventorySyncService->defaultSelectionPayload()['policy_types']); }), fn (Select $component): HintAction => HintAction::make('clear_policy_types') ->label('Clear') ->link() ->size(Size::Small) ->action(function () use ($component): void { $component->state([]); }), ]) ->options(function (): array { return collect(config('tenantpilot.supported_policy_types', [])) ->filter(fn (array $meta): bool => filled($meta['type'] ?? null)) ->groupBy(fn (array $meta): string => (string) ($meta['category'] ?? 'Other')) ->mapWithKeys(function ($items, string $category): array { $options = collect($items) ->mapWithKeys(function (array $meta): array { $type = (string) $meta['type']; $label = (string) ($meta['label'] ?? $type); $platform = (string) ($meta['platform'] ?? 'all'); return [$type => "{$label} • {$platform}"]; }) ->all(); return [$category => $options]; }) ->all(); }) ->default([]) ->dehydrated() ->required() ->rules([ 'array', 'min:1', new \App\Rules\SupportedPolicyTypesRule, ]) ->columnSpanFull(), Toggle::make('include_foundations') ->label('Include foundation types') ->helperText('Include scope tags, assignment filters, and notification templates.') ->default(true) ->dehydrated() ->rules(['boolean']) ->columnSpanFull(), Toggle::make('include_dependencies') ->label('Include dependencies') ->helperText('Include dependency extraction where supported.') ->default(true) ->dehydrated() ->rules(['boolean']) ->columnSpanFull(), Hidden::make('tenant_id') ->default(fn (): ?string => Tenant::current()?->getKey()) ->dehydrated(), ]) ->visible(function (): bool { $user = auth()->user(); if (! $user instanceof User) { return false; } return $user->canSyncTenant(Tenant::current()); }) ->action(function (array $data, BulkOperationService $bulkOperationService, InventorySyncService $inventorySyncService, AuditLogger $auditLogger): void { $tenant = Tenant::current(); $user = auth()->user(); if (! $user instanceof User) { abort(403, 'Not allowed'); } if (! $user->canSyncTenant($tenant)) { abort(403, 'Not allowed'); } $requestedTenantId = $data['tenant_id'] ?? null; if ($requestedTenantId !== null && (int) $requestedTenantId !== (int) $tenant->getKey()) { Notification::make() ->title('Not allowed') ->danger() ->send(); abort(403, 'Not allowed'); } $selectionPayload = $inventorySyncService->defaultSelectionPayload(); if (array_key_exists('policy_types', $data)) { $selectionPayload['policy_types'] = $data['policy_types']; } if (array_key_exists('include_foundations', $data)) { $selectionPayload['include_foundations'] = (bool) $data['include_foundations']; } if (array_key_exists('include_dependencies', $data)) { $selectionPayload['include_dependencies'] = (bool) $data['include_dependencies']; } $computed = $inventorySyncService->normalizeAndHashSelection($selectionPayload); $existing = InventorySyncRun::query() ->where('tenant_id', $tenant->getKey()) ->where('selection_hash', $computed['selection_hash']) ->whereIn('status', [InventorySyncRun::STATUS_PENDING, InventorySyncRun::STATUS_RUNNING]) ->first(); if ($existing instanceof InventorySyncRun) { Notification::make() ->title('Sync already running') ->body('An inventory sync is already running for this tenant. Check the progress widget for status.') ->warning() ->send(); return; } $run = $inventorySyncService->createPendingRunForUser($tenant, $user, $computed['selection']); $policyTypes = $computed['selection']['policy_types'] ?? []; if (! is_array($policyTypes)) { $policyTypes = []; } $bulkRun = $bulkOperationService->createRun( tenant: $tenant, user: $user, resource: 'inventory', action: 'sync', itemIds: $policyTypes, totalItems: count($policyTypes), ); $auditLogger->log( tenant: $tenant, action: 'inventory.sync.dispatched', context: [ 'metadata' => [ 'inventory_sync_run_id' => $run->id, 'bulk_run_id' => $bulkRun->id, 'selection_hash' => $run->selection_hash, ], ], actorId: $user->id, actorEmail: $user->email, actorName: $user->name, resourceType: 'inventory_sync_run', resourceId: (string) $run->id, ); Notification::make() ->title('Inventory sync started') ->body('Sync dispatched. Check the bottom-right progress widget for status.') ->icon('heroicon-o-arrow-path') ->iconColor('warning') ->success() ->sendToDatabase($user) ->send(); RunInventorySyncJob::dispatch( tenantId: (int) $tenant->getKey(), userId: (int) $user->getKey(), bulkRunId: (int) $bulkRun->getKey(), inventorySyncRunId: (int) $run->getKey(), ); }), ]; } public function getInventoryItemsUrl(): string { return InventoryItemResource::getUrl('index', tenant: Tenant::current()); } public function getSyncRunsUrl(): string { return InventorySyncRunResource::getUrl('index', tenant: Tenant::current()); } public function getCoverageUrl(): string { return InventoryCoverage::getUrl(tenant: Tenant::current()); } }