$filters Optional filters: status, category * @return array{rows: list>, truncated: bool} */ public function getUpdatesList(int $tenantId, array $filters = []): array { $bcResult = $this->bcGateway->fetchUpdateTickets(); $bcTickets = $bcResult['tickets']; $truncated = $bcResult['truncated']; $localRows = $this->repository->findAllByTenant($tenantId); $localByTicketNo = []; foreach ($localRows as $local) { $ticketNo = trim((string) ($local['ticket_no'] ?? '')); if ($ticketNo !== '') { $localByTicketNo[$ticketNo] = $local; } } $filterStatus = trim((string) ($filters['status'] ?? '')); $filterCategory = trim((string) ($filters['category'] ?? '')); $filterTicketState = trim((string) ($filters['ticket_state'] ?? '')); $filterSearch = trim((string) ($filters['search'] ?? '')); $merged = []; foreach ($bcTickets as $ticket) { $ticketNo = trim((string) ($ticket['No'] ?? '')); if ($ticketNo === '') { continue; } $categoryCode = trim((string) ($ticket['Category_1_Code'] ?? '')); if (!in_array($categoryCode, self::UPDATE_CATEGORIES, true)) { continue; } $local = $localByTicketNo[$ticketNo] ?? null; $status = $local !== null ? (string) ($local['status'] ?? self::STATUS_OPEN) : self::STATUS_OPEN; $ticketState = trim((string) ($ticket['Ticket_State'] ?? '')); // Apply filters if ($filterStatus !== '' && $filterStatus !== 'all' && $status !== $filterStatus) { continue; } if ($filterCategory !== '' && $filterCategory !== 'all' && $categoryCode !== $filterCategory) { continue; } if ($filterTicketState !== '' && $filterTicketState !== 'all' && $ticketState !== $filterTicketState) { continue; } $debitorNo = trim((string) ($ticket['Customer_No'] ?? '')); $debitorName = trim((string) ($ticket['Cust_Name'] ?? '')); // Search filter: match against ticket no, debitor name/no, domain url, gitea path if ($filterSearch !== '') { $searchHaystack = strtolower($ticketNo . ' ' . $debitorName . ' ' . $debitorNo . ' ' . ($local['domain_url'] ?? '') . ' ' . ($local['gitea_path'] ?? '')); if (strpos($searchHaystack, strtolower($filterSearch)) === false) { continue; } } $merged[] = [ 'ticket_no' => $ticketNo, 'ticket_description' => trim((string) ($ticket['Description'] ?? '')), 'category_code' => $categoryCode, 'debitor_no' => $debitorNo, 'debitor_name' => $debitorName, 'ticket_state' => trim((string) ($ticket['Ticket_State'] ?? '')), 'support_user' => trim((string) ($ticket['Support_User_Name'] ?? '')), 'created_on' => trim((string) ($ticket['Created_On'] ?? '')), 'last_activity' => trim((string) ($ticket['Last_Activity_Date'] ?? '')), 'domain_no' => (string) ($local['domain_no'] ?? ''), 'domain_url' => (string) ($local['domain_url'] ?? ''), 'gitea_path' => (string) ($local['gitea_path'] ?? ''), 'notes' => (string) ($local['notes'] ?? ''), 'status' => $status, 'local_id' => $local !== null ? (int) ($local['id'] ?? 0) : null, ]; } return ['rows' => $merged, 'truncated' => $truncated]; } /** * Assign a domain and gitea path to an update ticket. * * Creates a new helpdesk_updates record or updates an existing one. * * @return array{ok: bool, id: int|null, errors: array} */ public function assignUpdate( int $tenantId, string $ticketNo, string $debitorNo, string $debitorName, string $categoryCode, string $domainNo, string $domainUrl, string $giteaPath, string $notes, int $userId, ): array { $errors = []; $ticketNo = trim($ticketNo); if ($ticketNo === '') { $errors['ticket_no'] = t('Ticket number is required'); } $debitorNo = trim($debitorNo); if ($debitorNo === '') { $errors['debitor_no'] = t('Debitor number is required'); } $categoryCode = trim($categoryCode); if (!in_array($categoryCode, self::UPDATE_CATEGORIES, true)) { $errors['category_code'] = t('Invalid category'); } if ($errors !== []) { return ['ok' => false, 'id' => null, 'errors' => $errors]; } $domainNo = trim($domainNo); $domainUrl = trim($domainUrl); $giteaPath = trim($giteaPath); $notes = trim($notes); $status = ($domainNo !== '' || $giteaPath !== '') ? self::STATUS_ASSIGNED : self::STATUS_OPEN; $existing = $this->repository->findByTicketNo($tenantId, $ticketNo); if ($existing !== null) { $existingId = (int) ($existing['id'] ?? 0); if ($existingId <= 0) { return ['ok' => false, 'id' => null, 'errors' => ['general' => t('Failed to save')]]; } $updated = $this->repository->updateAssignment($tenantId, $existingId, [ 'domain_no' => $domainNo, 'domain_url' => $domainUrl, 'gitea_path' => $giteaPath, 'notes' => $notes, 'status' => $status, 'updated_by' => $userId, ]); if (!$updated) { return ['ok' => false, 'id' => null, 'errors' => ['general' => t('Failed to save')]]; } return ['ok' => true, 'id' => $existingId, 'errors' => []]; } $id = $this->repository->insert([ 'tenant_id' => $tenantId, 'ticket_no' => $ticketNo, 'category_code' => $categoryCode, 'debitor_no' => $debitorNo, 'debitor_name' => trim($debitorName), 'domain_no' => $domainNo, 'domain_url' => $domainUrl, 'gitea_path' => $giteaPath, 'notes' => $notes, 'status' => $status, 'created_by' => $userId, ]); if ($id === null) { return ['ok' => false, 'id' => null, 'errors' => ['general' => t('Failed to save')]]; } return ['ok' => true, 'id' => $id, 'errors' => []]; } public static function statusLabel(string $status): string { return match ($status) { self::STATUS_OPEN => t('Unassigned'), self::STATUS_ASSIGNED => t('Assigned'), default => $status, }; } public static function statusVariant(string $status): string { return match ($status) { self::STATUS_OPEN => 'warning', self::STATUS_ASSIGNED => 'success', default => 'neutral', }; } public static function ticketStateLabel(string $state): string { return match ($state) { 'Offen' => t('Open'), 'In Bearbeitung' => t('In progress'), 'Erledigt' => t('Resolved'), 'Geschlossen' => t('Closed'), default => $state !== '' ? $state : '—', }; } public static function ticketStateVariant(string $state): string { return match ($state) { 'Offen' => 'warning', 'In Bearbeitung' => 'info', 'Erledigt' => 'success', 'Geschlossen' => 'neutral', default => 'neutral', }; } public static function categoryLabel(string $code): string { return match ($code) { self::CATEGORY_UPDATE => t('Update'), self::CATEGORY_HOTFIX => t('Hotfix'), default => $code, }; } public static function categoryVariant(string $code): string { return match ($code) { self::CATEGORY_UPDATE => 'neutral', self::CATEGORY_HOTFIX => 'warning', default => 'neutral', }; } }