forked from fa/breadcrumb-the-shire
feat(helpdesk): add ticket grid filters and resolve support user names in chat
Add 3 new ticket list filters: Support User (dynamic select), Contact (dynamic select), and Period (30/90/180/365 days). Extend categories endpoint to also return support_users and contacts arrays from the same cached ticket data. Add server-side filtering for all three in the data endpoint. Resolve support user abbreviation codes to full names in the communication chat widget. For single-ticket view, infer code-to-name from Support_User_Name when only one support code appears. For debitor view, build map from already-fetched entries across tickets with no additional API calls. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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<string, mixed>|null $ticket Pre-fetched ticket data
|
||||
* @param array<int, array<string, string>> $odataEntries
|
||||
* @param array<int, array<string, string>> $soapEntries
|
||||
* @return array<string, string>
|
||||
*/
|
||||
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<string, mixed>|null $ticket Pre-fetched ticket data
|
||||
* @param array<int, array<string, string>> $odataEntries
|
||||
* @param array<int, array<string, string>> $soapEntries
|
||||
* @return array<string, string>
|
||||
*/
|
||||
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<int, array<string, string>> $entries
|
||||
* @param array<string, string> $supportUserNamesByTicket ticket_no → Support_User_Name
|
||||
* @return array<string, string> 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));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'],
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -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'],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user