}> */ public function steps(): array { return [ [ 'key' => 'beauftragung', 'label' => 'Order received', 'description' => 'The security check was sold for one or more customer projects; sales briefs the security officer with all details.', 'kind' => self::KIND_STEP, ], [ 'key' => self::STEP_INFORMATION, 'label' => 'Gather information', 'description' => 'Collect all relevant information from Customer Care / the project lead.', 'kind' => self::KIND_INFO, 'fields' => [ ['key' => 'technical_contact', 'label' => 'Technical contact at customer', 'type' => self::FIELD_TEXTAREA, 'required' => true], ['key' => 'deployment', 'label' => 'Deployment location & type', 'type' => self::FIELD_TEXTAREA, 'required' => true], ['key' => 'system_info', 'label' => 'Technical system information', 'type' => self::FIELD_TEXTAREA, 'required' => true], ['key' => 'external_systems', 'label' => 'External systems / API endpoints (exact URLs)', 'type' => self::FIELD_TEXTAREA, 'required' => true], ['key' => 'special_notes', 'label' => 'Special notes', 'type' => self::FIELD_TEXTAREA, 'required' => false], ], ], [ 'key' => 'scheduling', 'label' => 'Scheduling', 'description' => 'Agree internally and with the customer on the check date. Only the 2–3h go-live window is relevant to the customer.', 'kind' => self::KIND_STEP, 'fields' => [ ['key' => 'golive_start', 'label' => 'Go-live window start', 'type' => self::FIELD_DATETIME, 'required' => true], ['key' => 'golive_end', 'label' => 'Go-live window end', 'type' => self::FIELD_DATETIME, 'required' => true], ], ], [ 'key' => 'blocker_appointment', 'label' => 'Internal blocker appointment', 'description' => 'Create a 1–2 day internal blocker; invite Customer Care, Customizing and Sales.', 'kind' => self::KIND_STEP, 'fields' => [ ['key' => 'blocker_start', 'label' => 'Blocker start', 'type' => self::FIELD_DATETIME, 'required' => true], ['key' => 'blocker_end', 'label' => 'Blocker end', 'type' => self::FIELD_DATETIME, 'required' => true], ], ], [ 'key' => self::STEP_PERFORM_CHECK, 'label' => 'Perform security check', 'description' => 'Work through the product-specific technical checklist.', 'kind' => self::KIND_TECH_CHECKLIST, ], [ 'key' => self::STEP_REPORT, 'label' => 'Create report & notify customer', 'description' => 'Create the report for the customer and notify them.', 'kind' => self::KIND_STEP, 'fields' => [ ['key' => self::FIELD_CUSTOMER_REPORT, 'label' => 'Official customer report', 'type' => self::FIELD_TEXTAREA, 'required' => true, 'rows' => 12], ], ], ]; } /** * Keys of steps completed manually (everything except the tech-checklist step). * * @return list */ public function manualStepKeys(): array { $keys = []; foreach ($this->steps() as $step) { if ($step['kind'] !== self::KIND_TECH_CHECKLIST) { $keys[] = $step['key']; } } return $keys; } /** * Structured fields captured on a given step (info textareas, datetimes, …). * * @return list */ public function stepFields(string $stepKey): array { foreach ($this->steps() as $step) { if ($step['key'] === $stepKey) { return $step['fields'] ?? []; } } return []; } /** * Keys of a step's fields that must be filled before it can be completed. * (E.g. "Special notes" and the free-text note stay optional.) * * @return list */ public function requiredFieldKeys(string $stepKey): array { $keys = []; foreach ($this->stepFields($stepKey) as $field) { if ($field['required']) { $keys[] = $field['key']; } } return $keys; } /** * Extract the checkable items from a tech-schema snapshot. * * @param array $techSnapshot * @return list */ public static function techCheckItems(array $techSnapshot): array { $items = is_array($techSnapshot['items'] ?? null) ? $techSnapshot['items'] : []; $checks = []; foreach ($items as $item) { if (!is_array($item)) { continue; } if (($item['type'] ?? '') === 'check' && trim((string) ($item['key'] ?? '')) !== '') { $checks[] = ['key' => (string) $item['key'], 'label' => (string) ($item['label'] ?? '')]; } } return $checks; } /** * Compute progress + derived status purely from state. * * @param array $processState per-step state keyed by step key * @param array $techSnapshot frozen tech-schema snapshot * @param array $techState per-item state keyed by item key * @return array{done: int, total: int, percent: int, manual_done: int, manual_total: int, tech_done: int, tech_total: int, step6_done: bool, status: string} */ public function computeProgress(array $processState, array $techSnapshot, array $techState, bool $archived = false): array { $manualKeys = $this->manualStepKeys(); $manualTotal = count($manualKeys); $manualDone = 0; foreach ($manualKeys as $key) { if (!empty($processState[$key]['done'])) { $manualDone++; } } $techItems = self::techCheckItems($techSnapshot); $techTotal = count($techItems); $techDone = 0; foreach ($techItems as $item) { if (!empty($techState[$item['key']]['done'])) { $techDone++; } } $step6Done = $techTotal === 0 ? true : ($techDone === $techTotal); $total = $manualTotal + $techTotal; $done = $manualDone + $techDone; $percent = $total > 0 ? (int) floor(($done / $total) * 100) : 0; if ($archived) { $status = self::STATUS_ARCHIVED; } elseif ($total > 0 && $done >= $total) { $status = self::STATUS_COMPLETED; } elseif ($done > 0) { $status = self::STATUS_IN_PROGRESS; } else { $status = self::STATUS_OPEN; } return [ 'done' => $done, 'total' => $total, 'percent' => $percent, 'manual_done' => $manualDone, 'manual_total' => $manualTotal, 'tech_done' => $techDone, 'tech_total' => $techTotal, 'step6_done' => $step6Done, 'status' => $status, ]; } /** * Whether every step preceding the final "report" step is complete: all * manual steps except the report itself are done AND the technical checklist * is fully ticked. Gates the "Generate PDF customer report" action so the * report can only be produced once the check has actually been carried out. * * @param array $processState * @param array $techSnapshot * @param array $techState */ public function reportPrerequisitesMet(array $processState, array $techSnapshot, array $techState): bool { foreach ($this->manualStepKeys() as $key) { if ($key === self::STEP_REPORT) { continue; } if (empty($processState[$key]['done'])) { return false; } } return $this->computeProgress($processState, $techSnapshot, $techState)['step6_done']; } public static function statusLabel(string $status): string { return match ($status) { self::STATUS_OPEN => 'Open', self::STATUS_IN_PROGRESS => 'In progress', self::STATUS_COMPLETED => 'Completed', self::STATUS_ARCHIVED => 'Archived', default => $status, }; } public static function statusVariant(string $status): string { return match ($status) { self::STATUS_IN_PROGRESS => 'warning', self::STATUS_COMPLETED => 'success', default => 'neutral', }; } }