, error?: string} */ public function loadCustomer(string $customerNo): array { $customerNo = trim($customerNo); if ($customerNo === '') { return ['status' => 'not_found']; } if (!$this->settingsGateway->isConfigured()) { return ['status' => 'not_configured', 'error' => 'BC connection not configured']; } try { $customer = $this->bcODataGateway->getCustomer($customerNo); } catch (\Throwable) { return ['status' => 'error', 'error' => 'BC connection failed']; } if ($customer === null) { return ['status' => 'not_found']; } return [ 'status' => 'success', 'customer' => $customer, ]; } /** * Load contacts for a customer (for async data endpoint). * * @return array{ok: bool, contacts?: array>, error?: string} */ public function loadContacts(string $customerNo, string $customerName): array { $customerNo = trim($customerNo); $customerName = trim($customerName); if ($customerNo === '' || $customerName === '') { return ['ok' => false, 'error' => 'Missing customer number or name']; } if (!$this->settingsGateway->isConfigured()) { return ['ok' => false, 'error' => 'BC connection not configured']; } try { $contacts = $this->bcODataGateway->getContactsForCustomer($customerNo, $customerName); } catch (\Throwable $e) { return ['ok' => false, 'error' => $e->getMessage()]; } return ['ok' => true, 'contacts' => $contacts]; } /** * Load tickets for a customer (for async data endpoint). * * @return array{ok: bool, tickets?: array>, error?: string} */ public function loadTickets(string $customerNo, string $customerName): array { $customerNo = trim($customerNo); $customerName = trim($customerName); if ($customerNo === '' || $customerName === '') { return ['ok' => false, 'error' => 'Missing customer number or name']; } if (!$this->settingsGateway->isConfigured()) { return ['ok' => false, 'error' => 'BC connection not configured']; } try { $tickets = $this->bcODataGateway->getTicketsForCustomer($customerNo, $customerName); } catch (\Throwable $e) { return ['ok' => false, 'error' => $e->getMessage()]; } return ['ok' => true, 'tickets' => $tickets]; } /** * Load a summary of ticket KPIs for a customer (exact counts, not sampled). * * @return array{ok: bool, total?: int, open?: int, last_activity?: string, error?: string} */ public function loadTicketSummary(string $customerNo, string $customerName): array { $result = $this->loadTickets($customerNo, $customerName); if (!$result['ok']) { return $result; } return self::summarizeTickets($result['tickets'] ?? []); } /** * Aggregate ticket KPIs from a raw ticket array. * * Used by both the service method and the data endpoint (which has its own cache layer). * * @param array> $tickets * @return array{ok: true, total: int, open: int, last_activity: string} */ public static function summarizeTickets(array $tickets): array { $closedStates = ['Erledigt', 'Resolved', 'Closed', 'Geschlossen']; $openCount = 0; $lastActivity = ''; foreach ($tickets as $ticket) { $state = (string) ($ticket['Ticket_State'] ?? ''); if (!in_array($state, $closedStates, true)) { $openCount++; } $activityDate = (string) ($ticket['Last_Activity_Date'] ?? ''); if ($activityDate !== '' && $activityDate !== '0001-01-01T00:00:00Z' && $activityDate > $lastActivity) { $lastActivity = $activityDate; } } return [ 'ok' => true, 'total' => count($tickets), 'open' => $openCount, 'last_activity' => $lastActivity, ]; } /** * Load activity log for a ticket (for async data endpoint). * * @return array{ok: bool, log?: array>, error?: string} */ public function loadTicketLog(string $ticketNo): array { $ticketNo = trim($ticketNo); if ($ticketNo === '') { return ['ok' => false, 'error' => 'Missing ticket number']; } if (!$this->settingsGateway->isConfigured()) { return ['ok' => false, 'error' => 'BC connection not configured']; } try { $log = $this->bcODataGateway->getTicketLog($ticketNo); } catch (\Throwable $e) { return ['ok' => false, 'error' => $e->getMessage()]; } return ['ok' => true, 'log' => $log]; } /** * Load full detail for a debtor: master data, contacts, and tickets. * * @deprecated Use loadCustomer() + async endpoints for contacts/tickets instead. * * @return array{status: string, customer?: array, contacts?: array>, tickets?: array>, error?: string, contactsError?: string, ticketsError?: string} */ public function loadDetail(string $customerNo): array { $customerNo = trim($customerNo); if ($customerNo === '') { return ['status' => 'not_found']; } if (!$this->settingsGateway->isConfigured()) { return ['status' => 'not_configured', 'error' => 'BC connection not configured']; } try { $customer = $this->bcODataGateway->getCustomer($customerNo); } catch (\Throwable) { return ['status' => 'error', 'error' => 'BC connection failed']; } if ($customer === null) { return ['status' => 'not_found']; } // Load contacts — BC contacts are linked via Company_Name, not customer number. // IntegrationCustomerNo is not filterable in BC OData, so we use the customer name. $contacts = []; $contactsError = ''; $customerName = (string) ($customer['Name'] ?? ''); try { $contacts = $this->bcODataGateway->getContactsForCustomer($customerNo, $customerName); } catch (\Throwable $e) { $contactsError = $e->getMessage(); } // Load tickets via PBI_FP_Tickets entity which supports server-side filtering. $tickets = []; $ticketsError = ''; try { $tickets = $this->bcODataGateway->getTicketsForCustomer($customerNo, $customerName); } catch (\Throwable $e) { $ticketsError = $e->getMessage(); } return [ 'status' => 'success', 'customer' => $customer, 'contacts' => $contacts, 'tickets' => $tickets, 'contactsError' => $contactsError, 'ticketsError' => $ticketsError, ]; } }