diff --git a/modules/helpdesk/i18n/default_de.json b/modules/helpdesk/i18n/default_de.json index a652f52..75ffc89 100644 --- a/modules/helpdesk/i18n/default_de.json +++ b/modules/helpdesk/i18n/default_de.json @@ -130,23 +130,33 @@ "Ticket volume": "Ticket-Volumen", "Close rate": "Close-Rate", "Avg. resolution": "Ø Lösungszeit", - "Open backlog": "Offener Backlog", - "kpi_tooltip_close_rate": "Geschlossene \u00f7 erstellte Tickets im Zeitraum. \u00dcber 1\u00d7 = Backlog schrumpft.", + "Open backlog": "Offene Tickets", + "kpi_tooltip_close_rate": "Geschlossene \u00f7 erstellte Tickets im Zeitraum. \u00dcber 1\u00d7 = R\u00fcckstau wird abgebaut.", "kpi_tooltip_resolution": "Median der Bearbeitungsdauer aller im Zeitraum geschlossenen Tickets.", - "kpi_tooltip_backlog": "Aktueller Stand aller offenen Tickets \u2013 unabh\u00e4ngig vom gew\u00e4hlten Zeitraum.", - "Backlog shrinking": "Backlog schrumpft", - "Backlog growing": "Backlog wächst", + "kpi_tooltip_backlog": "Aktueller Stand aller offenen Tickets \u2013 unabh\u00e4ngig vom gew\u00e4hlten Zeitraum. Zeigt den R\u00fcckstau.", + "Backlog shrinking": "Rückstau wird abgebaut", + "Backlog growing": "Rückstau wächst", "unassigned": "ohne Bearbeiter", "Ticket trend": "Ticket-Trend", "Support efficiency": "Support-Effizienz", "Contract metrics": "Vertrags-Kennzahlen", - "Risk indicators": "Risiko-Indikatoren", + "Risk assessment": "Risikoanalyse", "vs. prev. period": "vs. Vorperiode", "Low": "Niedrig", "Medium": "Mittel", "High": "Hoch", - "flags": "Flags", + "All checks passed": "Alle Checks bestanden", + "of": "von", + "checks flagged": "Checks auffällig", + "Limit": "Limit", "No controlling data available.": "Keine Controlling-Daten verfügbar.", + "Resolution time": "Bearbeitungszeit", + "Oldest open ticket": "Ältestes offenes Ticket", + "Unassigned": "Nicht zugewiesen", + "risk_desc_ticket_volume": "Veränderung zum Vorperiode", + "risk_desc_resolution_time": "Median bis Schließung", + "risk_desc_open_aging": "Seit letzter Aktivität", + "risk_desc_unassigned": "Anteil offener Tickets ohne Bearbeiter", "Ticket volume increase": "Ticketvolumen-Anstieg", "High avg. resolution time": "Hohe Ø Lösungszeit", "Open ticket aging": "Offene Tickets überfällig", @@ -176,16 +186,16 @@ "High-risk aging": "High-Risk Alterung", "Unassigned open": "Offen ohne Bearbeiter", "Stale open": "Offen ohne Aktivität", - "Customer backlog": "Kunden-Backlog", + "Customer backlog": "Offene Tickets beim Kunden", "Minimum overdue (minutes)": "Mindestens überfällig (Minuten)", "High-risk codes (comma separated)": "High-Risk Codes (kommagetrennt)", "Minimum age (hours)": "Mindestalter (Stunden)", "Minimum open tickets": "Mindestens offene Tickets", - "Escalation SLA exceeded by {overdue_hours}h ({escalation_code}).": "Eskalations-SLA um {overdue_hours}h überschritten ({escalation_code}).", - "High-risk ticket ({escalation_code}) is open for {age_hours}h.": "High-Risk-Ticket ({escalation_code}) ist seit {age_hours}h offen.", - "Ticket is unassigned for {age_hours}h.": "Ticket ist seit {age_hours}h ohne Bearbeiter.", - "No activity for {age_hours}h.": "Keine Aktivität seit {age_hours}h.", - "Customer backlog: {open_tickets} open tickets.": "Kunden-Backlog: {open_tickets} offene Tickets.", + "Escalation SLA exceeded by {overdue_hours}h ({escalation_code}).": "SLA um {overdue_hours}h überschritten ({escalation_code}) — sofort eskalieren oder Kunden informieren.", + "High-risk ticket ({escalation_code}) is open for {age_hours}h.": "Kritisches Ticket ({escalation_code}) seit {age_hours}h offen — Priorität prüfen und Bearbeitung sicherstellen.", + "Ticket is unassigned for {age_hours}h.": "Seit {age_hours}h ohne Bearbeiter — dringend einem Mitarbeiter zuweisen.", + "No activity for {age_hours}h.": "Seit {age_hours}h keine Aktivität — prüfen ob Kunde auf Antwort wartet.", + "Customer backlog: {open_tickets} open tickets.": "{open_tickets} offene Tickets — älteste Tickets zuerst bearbeiten, um Rückstau abzubauen.", "Created": "Erstellt", "Loading...": "Laden...", "Refresh data": "Daten aktualisieren", @@ -220,10 +230,8 @@ "Open": "Offen", "In Progress": "In Bearbeitung", "Closed": "Geschlossen", - "Active contracts": "Aktive Verträge", "Monthly volume": "Monatliches Volumen", "Support hours": "Support-Stunden", - "Next invoicing": "Nächste Abrechnung", "Contract overview": "Vertragsübersicht", "No contract details available.": "Keine Vertragsdetails verfügbar.", "Could not load sales dashboard.": "Vertriebsdashboard konnte nicht geladen werden.", @@ -233,8 +241,6 @@ "Line state": "Zeilenstatus", "Starting date": "Startdatum", "Invoicing period": "Abrechnungszeitraum", - "Salesperson": "Vertrieb", - "Contacts": "Ansprechpartner", "positions": "Positionen", "per month": "pro Monat", "Last invoicing": "Letzte Abrechnung", @@ -248,5 +254,19 @@ "Avg. contract value": "Ø Vertragswert", "of {total} total": "von {total} gesamt", "in {days} days": "in {days} Tagen", - "largest": "größter" + "largest": "größter", + "Attention needed": "Handlungsbedarf", + "Recommendations": "Empfehlungen", + "Contract summary": "Vertragsübersicht", + "Contract trend": "Vertragsentwicklung", + "New contracts": "Neue Verträge", + "Ended contracts": "Beendete Verträge", + "active": "aktiv", + "ended": "beendet", + "Age": "Alter", + "Period": "Zeitraum", + "Last 30 days": "Letzte 30 Tage", + "Last 90 days": "Letzte 90 Tage", + "Last 180 days": "Letzte 180 Tage", + "Last 365 days": "Letzte 365 Tage" } diff --git a/modules/helpdesk/i18n/default_en.json b/modules/helpdesk/i18n/default_en.json index 87f7ce5..f30d8d4 100644 --- a/modules/helpdesk/i18n/default_en.json +++ b/modules/helpdesk/i18n/default_en.json @@ -130,23 +130,33 @@ "Ticket volume": "Ticket volume", "Close rate": "Close rate", "Avg. resolution": "Avg. resolution", - "Open backlog": "Open backlog", - "kpi_tooltip_close_rate": "Closed \u00f7 created tickets in period. Above 1\u00d7 = backlog is shrinking.", + "Open backlog": "Open tickets", + "kpi_tooltip_close_rate": "Closed \u00f7 created tickets in period. Above 1\u00d7 = open ticket queue is shrinking.", "kpi_tooltip_resolution": "Median processing time of all tickets closed in the selected period.", - "kpi_tooltip_backlog": "Current snapshot of all open tickets \u2013 independent of the selected period.", - "Backlog shrinking": "Backlog shrinking", - "Backlog growing": "Backlog growing", + "kpi_tooltip_backlog": "Current snapshot of all open tickets \u2013 independent of the selected period. Shows the pending queue.", + "Backlog shrinking": "Queue shrinking", + "Backlog growing": "Queue growing", "unassigned": "unassigned", "Ticket trend": "Ticket trend", "Support efficiency": "Support efficiency", "Contract metrics": "Contract metrics", - "Risk indicators": "Risk indicators", + "Risk assessment": "Risk assessment", "vs. prev. period": "vs. prev. period", "Low": "Low", "Medium": "Medium", "High": "High", - "flags": "flags", + "All checks passed": "All checks passed", + "of": "of", + "checks flagged": "checks flagged", + "Limit": "Limit", "No controlling data available.": "No controlling data available.", + "Resolution time": "Resolution time", + "Oldest open ticket": "Oldest open ticket", + "Unassigned": "Unassigned", + "risk_desc_ticket_volume": "Change vs. previous period", + "risk_desc_resolution_time": "Median time to close", + "risk_desc_open_aging": "Since last activity", + "risk_desc_unassigned": "Share of open tickets without assignee", "Ticket volume increase": "Ticket volume increase", "High avg. resolution time": "High avg. resolution time", "Open ticket aging": "Open ticket aging", @@ -176,16 +186,16 @@ "High-risk aging": "High-risk aging", "Unassigned open": "Unassigned open", "Stale open": "Stale open", - "Customer backlog": "Customer backlog", + "Customer backlog": "Open tickets for customer", "Minimum overdue (minutes)": "Minimum overdue (minutes)", "High-risk codes (comma separated)": "High-risk codes (comma separated)", "Minimum age (hours)": "Minimum age (hours)", "Minimum open tickets": "Minimum open tickets", - "Escalation SLA exceeded by {overdue_hours}h ({escalation_code}).": "Escalation SLA exceeded by {overdue_hours}h ({escalation_code}).", - "High-risk ticket ({escalation_code}) is open for {age_hours}h.": "High-risk ticket ({escalation_code}) is open for {age_hours}h.", - "Ticket is unassigned for {age_hours}h.": "Ticket is unassigned for {age_hours}h.", - "No activity for {age_hours}h.": "No activity for {age_hours}h.", - "Customer backlog: {open_tickets} open tickets.": "Customer backlog: {open_tickets} open tickets.", + "Escalation SLA exceeded by {overdue_hours}h ({escalation_code}).": "SLA exceeded by {overdue_hours}h ({escalation_code}) — escalate immediately or inform customer.", + "High-risk ticket ({escalation_code}) is open for {age_hours}h.": "Critical ticket ({escalation_code}) open for {age_hours}h — review priority and ensure progress.", + "Ticket is unassigned for {age_hours}h.": "Unassigned for {age_hours}h — assign to a team member urgently.", + "No activity for {age_hours}h.": "No activity for {age_hours}h — check if customer is waiting for a response.", + "Customer backlog: {open_tickets} open tickets.": "{open_tickets} open tickets — address oldest tickets first to reduce the queue.", "Created": "Created", "Loading...": "Loading...", "Refresh data": "Refresh data", @@ -220,10 +230,8 @@ "Open": "Open", "In Progress": "In Progress", "Closed": "Closed", - "Active contracts": "Active contracts", "Monthly volume": "Monthly volume", "Support hours": "Support hours", - "Next invoicing": "Next invoicing", "Contract overview": "Contract overview", "No contract details available.": "No contract details available.", "Could not load sales dashboard.": "Could not load sales dashboard.", @@ -233,8 +241,6 @@ "Line state": "Line state", "Starting date": "Starting date", "Invoicing period": "Invoicing period", - "Salesperson": "Salesperson", - "Contacts": "Contacts", "positions": "positions", "per month": "per month", "Last invoicing": "Last invoicing", @@ -248,5 +254,19 @@ "Avg. contract value": "Avg. contract value", "of {total} total": "of {total} total", "in {days} days": "in {days} days", - "largest": "largest" + "largest": "largest", + "Attention needed": "Attention needed", + "Recommendations": "Recommendations", + "Contract summary": "Contract summary", + "Contract trend": "Contract trend", + "New contracts": "New contracts", + "Ended contracts": "Ended contracts", + "active": "active", + "ended": "ended", + "Age": "Age", + "Period": "Period", + "Last 30 days": "Last 30 days", + "Last 90 days": "Last 90 days", + "Last 180 days": "Last 180 days", + "Last 365 days": "Last 365 days" } diff --git a/modules/helpdesk/lib/Module/Helpdesk/Service/TicketCommunicationService.php b/modules/helpdesk/lib/Module/Helpdesk/Service/TicketCommunicationService.php index 9c323c3..77d5188 100644 --- a/modules/helpdesk/lib/Module/Helpdesk/Service/TicketCommunicationService.php +++ b/modules/helpdesk/lib/Module/Helpdesk/Service/TicketCommunicationService.php @@ -69,10 +69,18 @@ class TicketCommunicationService $odataEntries = $this->applyActorNameMap($odataEntries, $actorNameMap); $soapEntries = $this->applyActorNameMap($soapEntries, $actorNameMap); } elseif ($allowPerTicketActorLookup) { - $resolvedActorNameMap = $this->resolveContactActorNames($ticketNo, $odataEntries, $soapEntries); - if ($resolvedActorNameMap !== []) { - $odataEntries = $this->applyActorNameMap($odataEntries, $resolvedActorNameMap); - $soapEntries = $this->applyActorNameMap($soapEntries, $resolvedActorNameMap); + $ticket = null; + try { + $ticket = $this->bcODataGateway->getTicket($ticketNo); + } catch (\Throwable) { + // ignore + } + $resolvedActorNameMap = $this->resolveContactActorNames($ticket, $odataEntries, $soapEntries); + $supportActorNameMap = $this->resolveSupportActorNames($ticket, $odataEntries, $soapEntries); + $combinedMap = array_merge($resolvedActorNameMap, $supportActorNameMap); + if ($combinedMap !== []) { + $odataEntries = $this->applyActorNameMap($odataEntries, $combinedMap); + $soapEntries = $this->applyActorNameMap($soapEntries, $combinedMap); } } @@ -172,6 +180,19 @@ class TicketCommunicationService $soapPartialFailures = 0; $actorNameMap = $this->buildContactActorNameMapForDebitor($customerName); + // Collect support user name → ticket code mappings + $supportUserNamesByTicket = []; + foreach ($tickets as $ticket) { + if (!is_array($ticket)) { + continue; + } + $no = trim((string) ($ticket['No'] ?? '')); + $name = trim((string) ($ticket['Support_User_Name'] ?? '')); + if ($no !== '' && $name !== '') { + $supportUserNamesByTicket[$no] = $name; + } + } + foreach (array_keys($ticketsToProcess) as $ticketNo) { $feed = $this->loadTicketCommunicationFeed( $ticketNo, @@ -199,6 +220,12 @@ class TicketCommunicationService } } + // Build support code→name map from entries + ticket data, then apply + $supportActorNameMap = $this->buildSupportActorNameMapFromEntries($allEntries, $supportUserNamesByTicket); + if ($supportActorNameMap !== []) { + $allEntries = $this->applyActorNameMap($allEntries, $supportActorNameMap); + } + $entries = $this->mergeEntries($allEntries, [], $maxEntriesTotal); return [ @@ -753,24 +780,18 @@ class TicketCommunicationService } /** + * @param array|null $ticket Pre-fetched ticket data * @param array> $odataEntries * @param array> $soapEntries * @return array */ - private function resolveContactActorNames(string $ticketNo, array $odataEntries, array $soapEntries): array + private function resolveContactActorNames(?array $ticket, array $odataEntries, array $soapEntries): array { $contactActorIds = $this->extractContactActorIds([...$odataEntries, ...$soapEntries]); if ($contactActorIds === []) { return []; } - $ticket = null; - try { - $ticket = $this->bcODataGateway->getTicket($ticketNo); - } catch (\Throwable) { - $ticket = null; - } - $currentContactName = trim((string) (($ticket['Current_Contact_Name'] ?? ''))); $customerName = trim((string) (($ticket['Company_Contact_Name'] ?? ''))); @@ -884,6 +905,97 @@ class TicketCommunicationService return $nameMap; } + /** + * Resolve support user actor codes to full names for a single ticket. + * + * Strategy: use the pre-fetched ticket's Support_User_Name, then look at + * which support-type Created_By codes appear in the log entries. If there + * is exactly one unique support code, map it to the Support_User_Name. + * + * @param array|null $ticket Pre-fetched ticket data + * @param array> $odataEntries + * @param array> $soapEntries + * @return array + */ + private function resolveSupportActorNames(?array $ticket, array $odataEntries, array $soapEntries): array + { + $supportUserName = trim((string) ($ticket['Support_User_Name'] ?? '')); + if ($supportUserName === '') { + return []; + } + + $supportCodes = []; + foreach ([...$odataEntries, ...$soapEntries] as $entry) { + $actorRole = trim((string) ($entry['actor_role'] ?? '')); + if ($actorRole !== 'support') { + continue; + } + $actor = strtoupper(trim((string) ($entry['actor'] ?? ''))); + if ($actor !== '' && !$this->isContactActorId($actor) && mb_strlen($actor) <= 10) { + $supportCodes[$actor] = true; + } + } + + if (count($supportCodes) !== 1) { + return []; + } + + $code = array_keys($supportCodes)[0]; + + return [$code => $supportUserName]; + } + + /** + * Build a support user code→name map from already-fetched communication entries. + * + * Groups entries by ticket_no, finds unique support-type actor codes per + * ticket, and maps each code to the ticket's Support_User_Name (from the + * pre-built $supportUserNamesByTicket lookup). No additional API calls. + * + * @param array> $entries + * @param array $supportUserNamesByTicket ticket_no → Support_User_Name + * @return array code → full name + */ + private function buildSupportActorNameMapFromEntries(array $entries, array $supportUserNamesByTicket): array + { + if ($entries === [] || $supportUserNamesByTicket === []) { + return []; + } + + // Group support actor codes per ticket + $codesByTicket = []; + foreach ($entries as $entry) { + $actorRole = trim((string) ($entry['actor_role'] ?? '')); + if ($actorRole !== 'support') { + continue; + } + $actor = strtoupper(trim((string) ($entry['actor'] ?? ''))); + if ($actor === '' || $this->isContactActorId($actor) || mb_strlen($actor) > 10) { + continue; + } + $ticketNo = trim((string) ($entry['ticket_no'] ?? '')); + if ($ticketNo === '') { + continue; + } + $codesByTicket[$ticketNo][$actor] = true; + } + + $result = []; + foreach ($codesByTicket as $ticketNo => $codes) { + if (count($codes) !== 1) { + continue; + } + $supportUserName = $supportUserNamesByTicket[$ticketNo] ?? ''; + if ($supportUserName === '') { + continue; + } + $code = array_keys($codes)[0]; + $result[$code] = $supportUserName; + } + + return $result; + } + private function normalizeActorRole(string $createdByType, string $actor): string { $type = mb_strtolower(trim($createdByType)); diff --git a/modules/helpdesk/pages/helpdesk/debitor-ticket-categories-data().php b/modules/helpdesk/pages/helpdesk/debitor-ticket-categories-data().php index eb70fc2..ecc8fa2 100644 --- a/modules/helpdesk/pages/helpdesk/debitor-ticket-categories-data().php +++ b/modules/helpdesk/pages/helpdesk/debitor-ticket-categories-data().php @@ -80,23 +80,44 @@ if ($allTickets === null) { } $categories = []; +$supportUsers = []; +$contacts = []; + foreach ((array) $allTickets as $ticket) { if (!is_array($ticket)) { continue; } + $category = trim((string) ($ticket['Category_1_Description'] ?? '')); - if ($category === '') { - continue; + if ($category !== '') { + $categories[$category] = true; + } + + $supportUser = trim((string) ($ticket['Support_User_Name'] ?? '')); + if ($supportUser !== '') { + $supportUsers[$supportUser] = true; + } + + $contact = trim((string) ($ticket['Current_Contact_Name'] ?? '')); + if ($contact !== '') { + $contacts[$contact] = true; } - $categories[$category] = true; } $categoryValues = array_keys($categories); usort($categoryValues, 'strnatcasecmp'); +$supportUserValues = array_keys($supportUsers); +usort($supportUserValues, 'strnatcasecmp'); + +$contactValues = array_keys($contacts); +usort($contactValues, 'strnatcasecmp'); + Router::json([ 'ok' => true, 'categories' => array_values($categoryValues), + 'support_users' => array_values($supportUserValues), + 'contacts' => array_values($contactValues), 'meta' => [ 'cache_used' => $cacheUsed, 'cache_bypassed' => $refreshRequested, diff --git a/modules/helpdesk/pages/helpdesk/debitor-tickets-data().php b/modules/helpdesk/pages/helpdesk/debitor-tickets-data().php index e5d79eb..b90c5c3 100644 --- a/modules/helpdesk/pages/helpdesk/debitor-tickets-data().php +++ b/modules/helpdesk/pages/helpdesk/debitor-tickets-data().php @@ -47,10 +47,22 @@ $cacheTtl = DebitorCacheControl::TTL_STANDARD_SECONDS; $cached = $sessionStore->get($cacheKey); $allTickets = null; $cacheUsed = false; +$serviceMeta = [ + 'source' => 'primary.company_contact_name', + 'fallback_used' => false, + 'fallback_reason' => '', +]; if (!$refreshRequested && is_array($cached) && isset($cached['fetched_at']) && (time() - (int) $cached['fetched_at']) < $cacheTtl) { $allTickets = $cached['tickets'] ?? []; $cacheUsed = true; + if (is_array($cached['service_meta'] ?? null)) { + $serviceMeta = [ + 'source' => (string) ($cached['service_meta']['source'] ?? $serviceMeta['source']), + 'fallback_used' => (bool) ($cached['service_meta']['fallback_used'] ?? false), + 'fallback_reason' => (string) ($cached['service_meta']['fallback_reason'] ?? ''), + ]; + } } if ($allTickets === null) { @@ -58,6 +70,7 @@ if ($allTickets === null) { $result = $service->loadTickets($customerNo, $customerName); if (!($result['ok'] ?? false)) { + $resultMeta = is_array($result['meta'] ?? null) ? $result['meta'] : []; Router::json([ 'data' => [], 'total' => 0, @@ -65,6 +78,9 @@ if ($allTickets === null) { 'cache_used' => $cacheUsed, 'cache_bypassed' => $refreshRequested, 'cache_refreshed' => $refreshRequested, + 'source' => (string) ($resultMeta['source'] ?? $serviceMeta['source']), + 'fallback_used' => (bool) ($resultMeta['fallback_used'] ?? false), + 'fallback_reason' => (string) ($resultMeta['fallback_reason'] ?? ''), ], ]); @@ -72,9 +88,16 @@ if ($allTickets === null) { } $allTickets = $result['tickets'] ?? []; + $resultMeta = is_array($result['meta'] ?? null) ? $result['meta'] : []; + $serviceMeta = [ + 'source' => (string) ($resultMeta['source'] ?? $serviceMeta['source']), + 'fallback_used' => (bool) ($resultMeta['fallback_used'] ?? false), + 'fallback_reason' => (string) ($resultMeta['fallback_reason'] ?? ''), + ]; $sessionStore->set($cacheKey, [ 'tickets' => $allTickets, + 'service_meta' => $serviceMeta, 'fetched_at' => time(), ]); } @@ -84,6 +107,9 @@ if ($allTickets === null) { $search = trim((string) ($filters['search'] ?? '')); $status = trim((string) ($filters['status'] ?? '')); $category = trim((string) ($filters['category'] ?? '')); +$supportUser = trim((string) ($filters['support'] ?? '')); +$contact = trim((string) ($filters['contact'] ?? '')); +$period = trim((string) ($filters['period'] ?? '')); $order = (string) ($filters['order'] ?? 'Created_On'); $dir = (string) ($filters['dir'] ?? 'desc'); $limit = (int) ($filters['limit'] ?? 10); @@ -124,8 +150,34 @@ if ($status !== '') { // Category filter if ($category !== '') { $filtered = array_values(array_filter($filtered, static function (array $ticket) use ($category): bool { - $ticketCategory = trim((string) ($ticket['Category_1_Description'] ?? '')); - return $ticketCategory === $category; + return trim((string) ($ticket['Category_1_Description'] ?? '')) === $category; + })); +} + +// Support user filter +if ($supportUser !== '') { + $filtered = array_values(array_filter($filtered, static function (array $ticket) use ($supportUser): bool { + return trim((string) ($ticket['Support_User_Name'] ?? '')) === $supportUser; + })); +} + +// Contact filter +if ($contact !== '') { + $filtered = array_values(array_filter($filtered, static function (array $ticket) use ($contact): bool { + return trim((string) ($ticket['Current_Contact_Name'] ?? '')) === $contact; + })); +} + +// Period filter (days from today) +if ($period !== '' && ctype_digit($period)) { + $cutoff = (new DateTimeImmutable('now'))->modify('-' . $period . ' days')->format('Y-m-d\TH:i:s\Z'); + $filtered = array_values(array_filter($filtered, static function (array $ticket) use ($cutoff): bool { + $created = (string) ($ticket['Created_On'] ?? ''); + if ($created === '' || $created === '0001-01-01T00:00:00Z') { + return false; + } + + return $created >= $cutoff; })); } @@ -208,5 +260,8 @@ Router::json([ 'cache_used' => $cacheUsed, 'cache_bypassed' => $refreshRequested, 'cache_refreshed' => $refreshRequested, + 'source' => $serviceMeta['source'], + 'fallback_used' => $serviceMeta['fallback_used'], + 'fallback_reason' => $serviceMeta['fallback_reason'], ], ]); diff --git a/modules/helpdesk/pages/helpdesk/debitor-tickets-filter-schema.php b/modules/helpdesk/pages/helpdesk/debitor-tickets-filter-schema.php index 1ae4208..dc00c69 100644 --- a/modules/helpdesk/pages/helpdesk/debitor-tickets-filter-schema.php +++ b/modules/helpdesk/pages/helpdesk/debitor-tickets-filter-schema.php @@ -8,6 +8,9 @@ return gridFilterSchema([ 'offset' => ['type' => 'int', 'default' => 0, 'min' => 0], 'search' => ['type' => 'string'], 'category' => ['type' => 'string'], + 'support' => ['type' => 'string'], + 'contact' => ['type' => 'string'], + 'period' => ['type' => 'string', 'default' => ''], 'order' => ['type' => 'order', 'allowed' => ['No', 'Created_On', 'Last_Activity_Date', 'Ticket_State'], 'default' => 'Created_On'], 'dir' => ['type' => 'dir', 'default' => 'desc'], 'status' => ['type' => 'string', 'default' => ''], @@ -47,5 +50,44 @@ return gridFilterSchema([ ], 'query' => ['type' => 'string'], ], + [ + 'key' => 'support', + 'type' => 'select', + 'label' => 'Support', + 'input_id' => 'helpdesk-ticket-support-filter', + 'default' => '', + 'options_key' => 'support_user_items', + 'allowed' => [ + ['id' => '', 'description' => 'All'], + ], + 'query' => ['type' => 'string'], + ], + [ + 'key' => 'contact', + 'type' => 'select', + 'label' => 'Contact', + 'input_id' => 'helpdesk-ticket-contact-filter', + 'default' => '', + 'options_key' => 'contact_items', + 'allowed' => [ + ['id' => '', 'description' => 'All'], + ], + 'query' => ['type' => 'string'], + ], + [ + 'key' => 'period', + 'type' => 'select', + 'label' => 'Period', + 'input_id' => 'helpdesk-ticket-period-filter', + 'default' => '', + 'normalize' => 'all_to_empty', + 'allowed' => [ + ['id' => '', 'description' => 'All'], + ['id' => '30', 'description' => 'Last 30 days'], + ['id' => '90', 'description' => 'Last 90 days'], + ['id' => '180', 'description' => 'Last 180 days'], + ['id' => '365', 'description' => 'Last 365 days'], + ], + ], ], ]); diff --git a/modules/helpdesk/tests/Module/Helpdesk/Service/TicketCommunicationServiceTest.php b/modules/helpdesk/tests/Module/Helpdesk/Service/TicketCommunicationServiceTest.php index ea0ceac..01d3fca 100644 --- a/modules/helpdesk/tests/Module/Helpdesk/Service/TicketCommunicationServiceTest.php +++ b/modules/helpdesk/tests/Module/Helpdesk/Service/TicketCommunicationServiceTest.php @@ -21,7 +21,7 @@ class TicketCommunicationServiceTest extends TestCase ]); $odataGateway = $this->createMock(BcODataGateway::class); - $odataGateway->method('getTicketLog')->with('T26-1216')->willReturn([ + $odataGateway->expects($this->any())->method('getTicketLog')->with('T26-1216')->willReturn([ [ 'Entry_No' => 1, 'Created_By' => 'ICTEST', @@ -60,7 +60,7 @@ class TicketCommunicationServiceTest extends TestCase ]); $odataGateway = $this->createMock(BcODataGateway::class); - $odataGateway->method('getTicketLog')->with('T26-1216')->willReturn([ + $odataGateway->expects($this->any())->method('getTicketLog')->with('T26-1216')->willReturn([ [ 'Entry_No' => 1, 'Created_By' => 'ICTEST', @@ -105,8 +105,8 @@ class TicketCommunicationServiceTest extends TestCase } $odataGateway = $this->createMock(BcODataGateway::class); - $odataGateway->method('getTicketsForCustomer')->with('10001', 'Test GmbH')->willReturn($tickets); - $odataGateway->method('getContactsForCustomer')->with('', 'Test GmbH')->willReturn([]); + $odataGateway->expects($this->any())->method('getTicketsForCustomer')->with('10001', 'Test GmbH')->willReturn($tickets); + $odataGateway->expects($this->any())->method('getContactsForCustomer')->with('', 'Test GmbH')->willReturn([]); $odataGateway->method('getTicketLog')->willReturnCallback(static function (string $ticketNo): array { return [[ 'Entry_No' => 1, @@ -147,7 +147,7 @@ class TicketCommunicationServiceTest extends TestCase ]); $odataGateway = $this->createMock(BcODataGateway::class); - $odataGateway->method('getTicketsForCustomer')->with('10001', 'Test GmbH')->willReturn([ + $odataGateway->expects($this->any())->method('getTicketsForCustomer')->with('10001', 'Test GmbH')->willReturn([ ['No' => 'T1', 'Last_Activity_Date' => '2026-04-10T08:00:00Z', 'Created_On' => '2026-04-09T08:00:00Z'], ['No' => 'T2', 'Last_Activity_Date' => '2026-04-09T08:00:00Z', 'Created_On' => '2026-04-08T08:00:00Z'], ]); @@ -193,7 +193,7 @@ class TicketCommunicationServiceTest extends TestCase ]); $odataGateway = $this->createMock(BcODataGateway::class); - $odataGateway->method('getTicketLog')->with('T26-1216')->willReturn([]); + $odataGateway->expects($this->any())->method('getTicketLog')->with('T26-1216')->willReturn([]); $service = new TicketCommunicationService($soapGateway, $odataGateway); $result = $service->loadTicketCommunicationFeed('T26-1216', 2); @@ -249,7 +249,7 @@ class TicketCommunicationServiceTest extends TestCase ]); $odataGateway = $this->createMock(BcODataGateway::class); - $odataGateway->method('getTicketLog')->with('T26-1204')->willReturn([ + $odataGateway->expects($this->any())->method('getTicketLog')->with('T26-1204')->willReturn([ [ 'Entry_No' => 19356, 'Created_By' => 'KT002967', @@ -260,12 +260,12 @@ class TicketCommunicationServiceTest extends TestCase 'Record_ID' => 'Hallo', ], ]); - $odataGateway->method('getTicket')->with('T26-1204')->willReturn([ + $odataGateway->expects($this->any())->method('getTicket')->with('T26-1204')->willReturn([ 'No' => 'T26-1204', 'Company_Contact_Name' => 'Aero-Dienst GmbH', 'Current_Contact_Name' => 'Fallback Kontakt', ]); - $odataGateway->method('getContactsForCustomer')->with('', 'Aero-Dienst GmbH')->willReturn([ + $odataGateway->expects($this->any())->method('getContactsForCustomer')->with('', 'Aero-Dienst GmbH')->willReturn([ [ 'No' => 'KT002967', 'Name' => 'Felix Schneider',