Migrates the ticket detail view (communication feed + ticket metadata aside)
into the core detail drawer pattern. Click a ticket number in the customer's
support tab → drawer slides in with the full ticket thread; arrow icons step
through the current filter page; ↗ opens the full page.
- `modules/helpdesk/templates/helpdesk-ticket-detail.phtml` extracts the
body (communication feed container + metadata aside + i18n JSON) so the
full page and the fragment render from the same source.
- `ticket-fragment($id).php` / `(none).phtml` reuse BcODataGateway::getTicket
and enforce the same ABILITY_ACCESS guard as the full page. Connection
errors render inline without aborting the fragment.
- Route `helpdesk/ticket-fragment/{id}` declared in module.php.
- `helpdesk-detail.js` wires the tickets grid: ticket-no formatter emits
`data-drawer-trigger`, `linkColumn` disabled, `initDetailDrawer` called
with `onContentLoaded → initHelpdeskTicket(contentEl)` so the async
communication feed loads inside the drawer just like on the full page.
- `fetchUrl`/`fullUrl` written as single-expression arrows so the
DetailDrawerFragmentContractTest can discover the consumer and validate
the fragment endpoint files exist — the test now covers all three drawer
consumers (address book, admin users, helpdesk tickets).
The full-page ticket view is unchanged for users who deep-link directly;
only the router-to-body wiring moved through the partial.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The ticket description was only visible as a tooltip on the ticket number
link — easy to miss and not searchable by eye when scanning the list.
Move it to a dedicated "Description" column right after "Ticket".
- helpdesk-updates-index.js: new column between Ticket and Ticket status,
using the shared `.app-cell-note` class for ellipsis + title fallback
on long descriptions. Drop the now-redundant data-tooltip on the
ticket link. mapData gets one extra positional entry.
- updates/index(default).phtml: add ticketDescription label to
pageConfig. i18n key "Description" already exists in both core and
helpdesk dictionaries, no new strings required.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a reusable CSV-export building block to the core so any Grid.js list
page can gain a filter/sort-aware download with two clicks of glue.
Core primitive:
- core/Service/Export/CsvExportService: flavor-aware writer (plain CSV
or Excel-compatible UTF-8+BOM+semicolon), with OWASP-aligned formula-
injection escape (=, @, +, -, TAB, CR) applied to both rows *and*
headers. Value objects for columns (ExportColumn) carry an extractor
closure and an allowSignedNumeric flag for phone-number-shaped cells.
- core/Support/helpers/export.php: thin HTTP layer (exportSendCsv,
exportCapLimit, exportResolveFlavor, exportRequireGetRequest) reusing
the requestInput() contract for GR-CORE-003 consistency. Filename is
sanitized and length-clamped; callers must still enforce auth.
- templates/partials/app-list-export-dropdown.phtml: zero-config
<details class="dropdown"> with CSV + Excel triggers.
- web/js/core/app-list-export.js: initListExport({ gridConfig, exportUrl })
mirrors the current grid filters + sort onto the export URL and
navigates, preserving session cookies for download.
First consumer — Helpdesk domains:
- DomainListService extracts the shared filter/enrich/sort logic from
domains-data so the grid endpoint and the new domains/export endpoint
cannot drift.
- domains/export endpoint delegates to DomainListService, declares
ExportColumns (with translated level labels), and exits via
exportSendCsv.
- domains-data now ~20 lines, delegating to DomainListService.
Tests:
- CsvExportServiceTest (10 cases): both flavors, BOM/no-BOM, formula
escapes incl. TAB/CR, header escape, signed-numeric allowlist,
quoting, multiline, empty columns, generator-compatible iterable.
- ExportHelpersTest (5 cases): exportCapLimit bounds.
- DomainListServiceTest (8 cases): filter, search, "all" sentinel,
sort, paging, enrichment, BC failure, tenant-scoped security filter.
Gates: PHPUnit green, PHPStan clean on touched files, module:sync ok.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Saving security levels appeared broken in both the domain detail view and
the domains grid. Root causes:
- Repository never unwrapped MintyPHP DB rows (nested by table name), so
reads always returned defaults even though writes succeeded.
- Batch queries bound tenant_id last while SQL expected it first, so
list/detail enrichment for the grid matched nothing.
- Detail page AJAX submit looked up CSRF via a non-existent data attribute
and dropped the note field entirely.
- Endpoint used a hand-rolled CSRF check instead of Session::checkCsrfToken().
Fixes:
- DomainSecurityLevelRepository: use RepositoryArrayHelper::unwrap(List),
swap param order in listLevelsByDomainNos / listDetailsByDomainNos,
centralize table name in a const.
- security-level-data endpoint: canonical Session::checkCsrfToken(),
unified JSON/PRG error path with proper HTTP status codes and flash.
- Detail page: drop broken AJAX hijack, rely on native form POST + PRG
(GR-CORE-012).
- Grid list: refetch via gridRef.grid.forceRender() after dialog save
instead of DOM-only mutation, so Grid.js internal state stays in sync.
- Add dedicated Note column next to the badge with ellipsis + title
tooltip.
- Replace console.* in the dialog with showAsyncFlash for user-visible
errors; add i18n keys (de/en) for all error messages.
- Drop unused postAction import and TENANT_ID_OTHER test constant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New Updates page in the Helpdesk module that fetches UPDATE/UPDATE-HF tickets
from Business Central (PBI_LV_Tickets) and allows assigning a domain and Gitea
link via a dialog. Ticket status (from BC) and assignment status (local) are
shown as separate columns with filters for both plus type and free-text search.
Assigned updates also appear on the domain detail page. Includes session-cached
BC fetch with refresh button, admin permissions, migration, and 16 unit tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New domain detail page showing customer info, contract data, linked handovers,
updates section, and related domains for the same customer. Data loaded async
with skeleton loading states. Includes DomainDetailService and tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds domain selection (cascaded from debitor) to handover creation and edit,
bulk delete with confirmation dialog, and various UI improvements to the
handover wizard and list page. Includes migration for domain columns,
domain-select AJAX endpoint, and updated tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implement handover protocols as a new entity in the helpdesk module,
allowing users to create fillable protocol records from admin-defined
software product schemas.
Key additions:
- DB migration (helpdesk_handovers table) with tenant scope
- HandoverService with status workflow (draft/in_progress/completed/archived)
- Three-tier permissions (view/create/manage)
- Two-step creation wizard (Stripe-style assistant)
- Grid.js list page with search and status filter
- Edit/detail page with aside metadata and status controls
- Reusable core autocomplete lookup component (app-lookup-field)
- Debitor lookup data endpoint for autocomplete
- Dynamic form rendering from schema snapshots
- 11 PHPUnit tests for HandoverService
- DE+EN i18n translations (48 keys each)
Also includes: PHPStan fixes (dead code removal, stale baseline cleanup),
software product edit title improvement, fieldset simplification,
and Stripe-style hover for schema preview.
Workflow: HD-HANDOVERS-001 (.agents/runs/HD-HANDOVERS-001/)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Auto-generate field keys and option values from labels via slugify,
add live preview panel with hover-highlight, insert-between-fields
dividers, and various spacing/styling improvements.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a JSON-based schema editor as a new tab on the software product edit
page. Admins can define protocol fields (heading, paragraph, text,
textarea, number, date, checkbox, select) via a structured UI with
add/remove/reorder controls. The schema is stored as a versioned JSON
column on the product row.
Includes DB migration, server-side validation (type whitelist, key
uniqueness, key format, max 50 fields, select option checks), i18n
(DE+EN), CSS, and 10 PHPUnit tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a new Software-Produkte feature to the helpdesk module that syncs
contract types (Create_SaaS_License=true) from BC OData into a local
database table with a nightly scheduler job, providing a Grid.js list
page and detail/edit page for managing custom product names.
- DB migration 003: helpdesk_software_products table (code UNIQUE key)
- BcODataGateway: FS_Contract_Types entity with SaaS filter + fallback
- SoftwareProductRepository: upsert, listPaged, softDelete, updateName
- SoftwareProductSyncService: fetch → upsert → soft-delete lifecycle
- SoftwareProductSyncJobHandler: daily at 02:00 via scheduler platform
- SoftwareProductService: web UI business logic with validation
- Permission: helpdesk.software-products.manage (nav-gated)
- List page: Grid.js with Code, BC Description, Name, Status columns
- Detail page: Code/BC Description read-only, Name editable, PRG pattern
- i18n: de + en translation keys
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a new "Domains" page to the helpdesk module that fetches domain data
from the FS_Contract_Domains OData endpoint and enriches each domain with
its contract type (PI_Header_Type) by joining with FS_Contract_Lines_Test
where Type = 'Domain'.
New files:
- domains/index().php, index(default).phtml, filter-schema.php — list page
- domains-data().php — data endpoint with PHP-side filtering/sorting
- helpdesk-domains-index.js — Grid.js via initStandardListPage()
Gateway additions:
- listDomains() — fetch all contract domains
- listDomainContractLines() — fetch domain-type contract lines for type lookup
Sidebar restructured into three collapsible groups (Lookup, Monitoring,
Administration) with per-group icons and color coding, matching the core
admin panel pattern.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a meetings section to the debitor detail page, fetching meeting data from BC OData API with cache control support. Includes timeline UI for upcoming and past meetings.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Restructure helpdesk tenant settings into per-connection config with
improved token handling. Update risk radar data endpoint and UI.
Clean up ImageUploadTrait and update architecture contract tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduce per-tenant override for helpdesk BC connection config.
New table helpdesk_tenant_settings stores tenant-specific credentials
with encrypted secrets (AES-256-GCM). EffectiveHelpdeskSettingsService
resolves global vs tenant config per request. Settings UI extended with
tenant override tab, toggle, and dual editing mode.
All runtime consumers (OData, SOAP, OAuth) read through the new
resolver. Token cache flushed on any config change. Default behavior
unchanged — tenants use global config until override explicitly enabled.
Also includes risk radar UI refinements: Stripe-style card redesign
with accent borders and score pills, removal of redundant KPI tiles
and search, and filtering of zero-score entries.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Stripe-style layout: large colored score circle on the left, customer
name + risk level label on the right. One-line key facts below
separated by middot (e.g. '5 open · 2 critical · 1 SLA · ↑3').
Removed border-left color accent, pill list, and metric row.
Three visual levels: score (eye-catching), name+level (context),
facts (detail). Clean scannable layout.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Each dimension shows 'points / max' (e.g. '18 / 35') making the
score composition immediately understandable. Total row sums to
the final 'score / 100'. Replaces abstract percentage column.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove duplicate metrics row and reason list. Replace with a single
row of compact pills: '5 open', '2 critical' (orange), '1 SLA' (orange),
'↑3' (orange) or '↓2'. No bullet points — pills as flex-wrap row.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove '• ' prefix from reason list items (CSS list-style handles it)
- Replace abstract dimension bars in dialog with a clear table showing
dimension name, score (0-100), and weighted contribution per row
- Total row shows final risk score
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cards now show plain-language reasons instead of abstract progress bars:
'8 open, 3 critical', '↑4 more than resolved', '2 SLA breaches',
'Oldest ticket 12d'. Instantly understandable without knowing the
scoring formula. Driver bars remain in the detail dialog for the
full dimension breakdown.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Show ↑3 (growing) or ↓2 (shrinking) instead of '+3 net' / '-2 net'.
Same visual language as the team dashboard trend arrows.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New portfolio view scoring customers 0-100 across five dimensions:
- Open pressure (30%): open + critical ticket count
- Trend (25%): net ticket flow (created - closed) in period
- SLA overdue (20%): tickets exceeding escalation targets
- Resolution time (15%): median hours to close (null = neutral)
- Inactivity (10%): age of oldest open ticket
Card grid with score badges (color-coded high/medium/low), metric
pills, driver bars. Click opens detail dialog with all dimensions
and open ticket list. Clientside search filter.
PBI_LV_Tickets with client-driven paging ($skip/$top, hardcap 5000).
Escalation definitions cached separately (30min TTL). Truncated
banner when data is capped.
New: RiskRadarService, getTicketsForRiskRadar(), 13 PHPUnit tests,
permission helpdesk.risk-radar.view, 2 routes, i18n de+en.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
FsLightbox uses data-type to skip content detection. Combined with
Cache-Control: max-age=3600 on the proxy, the browser should serve
the lightbox image from cache instead of making a second SOAP call.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Skeleton shimmer placeholder shown while SOAP image loads
- onload hides skeleton, onerror hides entire preview
- data-fslightbox="ticket-files" on image link enables FsLightbox
overlay (auto-detected by global MutationObserver)
- cursor: zoom-in on preview images
- Gallery name "ticket-files" groups all images in the feed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Proxy endpoint supports ?inline=1 parameter that serves images with
correct Content-Type and Content-Disposition: inline. Uses fpassthru
via php://memory stream to bypass MintyPHP Analyzer restrictions.
Frontend always attempts image preview via <img> tag. Non-image files
gracefully hide the preview via onerror handler. Download link shown
for all file types regardless.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
BC's GetTicketFile requires a valid contactNo for authorization.
The endpoint now accepts ticketNo, loads the ticket via OData to get
Current_Contact_Name, looks up the contact's No, and passes it to
the SOAP call. Frontend includes ticketNo in the file download URL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use Router::download() instead of die() to avoid Analyzer rejection
- Add 5 PHPUnit tests for BcSoapGateway::getTicketFile() (F-001)
- Add test for file metadata on communication entries (F-002)
- Add rel=noopener noreferrer on target=_blank links (F-003)
- Add aria-hidden on download icon, aria-label on image link (F-003)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New SOAP method getTicketFile() on BcSoapGateway calls BC's
GetTicketFile to retrieve file attachments as Base64, decodes in
memory and returns binary data without writing to disk.
New proxy endpoint helpdesk/ticket-file-data streams files directly
to the browser with correct MIME type. Images served inline, other
files as download attachment.
TicketCommunicationService now attaches file_entry_no and file_name
to file-type events. Frontend renders download links with bi-download
icon and inline image previews for jpg/png/gif attachments.
Works in both ticket detail and debitor detail communication feeds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds 'Refresh data' button that re-fetches the communication feed
with ?refresh=1 to bypass the session cache. Shows loading skeleton
during reload.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Contracts & products KPI tiles (active contracts, monthly volume,
support hours, next invoicing) distract from the ticket-focused support
view. Contract data remains available in the dedicated Sales tab.
Removes: section HTML, skeleton placeholder, JS render function + call.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Change default period from 90 to 30 days for a more current snapshot.
Add legend footnote: '↑↓ Trend basiert auf den letzten 30 Tagen'
so the user knows what the arrows refer to.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Hover over ↑/↓ shows a data-tooltip explaining the delta in plain
language: 'More tickets received than resolved — backlog is growing.'
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Shows a small arrow next to the ticket count in each agent card header:
↑3 (orange) = received 3 more than resolved → backlog growing
↓2 (green) = resolved 2 more than received → backlog shrinking
Hidden when delta is 0 (stable).
Delta is computed from received_in_period (tickets created in the
selected period assigned to this agent) minus resolved_in_period.
No snapshot storage needed — derived from existing ticket data.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Revert queue detection, wait time summary, and visual separation.
All members are treated equally again — queue appears as a normal
agent entry sorted by the existing criteria.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
BC uses 'Wartschlange Customer Care' not 'Warteschlange'. Match on
common stem 'wartschlang' to cover both spellings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Queue detection: any member with 'warteschlange' in support_user name
(or empty) is treated as queue, not as a regular agent.
Current tab: queue cards shown at top with a wait time summary
(Ø/Min/Max from open ticket ages). Agents listed below.
Historical tab: queue cards shown before agents, same performance
widget pattern.
No backend changes — purely client-side separation using existing data.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reorder: Top customers | Top categories | Resolution time.
More logical flow — pick a filter on the left, see the result on the right.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Clicking a customer or category in the Historical tab recalculates
Ø/Min/Max resolution times for only that filter. Click again to reset.
All computation is client-side from resolved_details already in the
response — no extra API calls. Active filter highlighted with primary
color, resolution values turn primary when filtered.
Backend now includes resolved_ticket_details per agent (customer_name,
category, resolution_hours) for client-side filtering.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Skeleton loading now mirrors the actual agent card layout: bordered card
with header (avatar circle + name bar + badge) and three table-like rows.
Removes aria-busy toggle from JS. Three skeleton cards shown during load.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the custom Ø/Min/Max column layout with the same ranked list
component used for top customers and categories. All three columns
now use identical markup and styling for visual consistency.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Each agent gets a card with three insight columns:
- Resolution time: Ø / Min / Max (formatted as hours or days)
- Top 3 customers by resolved ticket count (ranked list)
- Top 3 categories by resolved ticket count (ranked list)
Backend collects resolved ticket details per agent: customer names,
categories, and resolution hours. Aggregates top-3 and min/max/avg.
Replaces the old compact single-row layout with rich widget cards
matching the Current tab pattern.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ticket log entries without Created_By (e.g. system-generated) now show
'System' instead of 'Unbekannter Benutzer'. Missing timestamps show
a dash instead of 'Unbekannte Zeit'.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Table keeps its fixed column widths and becomes horizontally scrollable
on narrow viewports instead of breaking the layout.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use table-layout:fixed with colgroup defining column widths in ch units.
Every agent card table gets the same colgroup, ensuring identical column
alignment across all cards. Ticket (9ch), Customer (22ch), Category
(16ch), Activity (11ch) are fixed; Description takes remaining space
with ellipsis. All cells get overflow:hidden + text-overflow:ellipsis.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>