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:
2026-04-05 16:37:07 +02:00
parent eed1e848df
commit 7220aa7459
7 changed files with 332 additions and 62 deletions

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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));

View File

@@ -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;
}
}
$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,

View File

@@ -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'],
],
]);

View File

@@ -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'],
],
],
],
]);

View File

@@ -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',