Commit Graph

186 Commits

Author SHA1 Message Date
fs
149d4515de refactor(settings): remove global appearance settings — tenant is sole source
Removes the global app_theme, app_theme_user and app_primary_color settings
from the admin/settings area and the underlying service/cache/API/i18n/docs
layers. The tenant columns (tenants.primary_color, default_theme,
allow_user_theme) become the single source of truth for appearance.
Branding helpers are tenant-only and fall back to a hardcoded 'light' theme
with no brand color when no tenant context is available (login, error,
pre-session pages). The APP_THEME env var is removed.

Scope:
- UI: Appearance tab gone from /admin/settings; tenant edit form drops the
  global-fallback color preview.
- Service: SettingsAppGateway no longer exposes setting-backed accessors
  for theme/user-theme/primary-color. Theme catalog utilities (listThemes,
  isAllowedTheme, normalizeTheme, resolveDefaultTheme) stay and are used
  across Tenant / Auth / User flows; resolveDefaultTheme now hardcodes
  'light' with a catalog fallback (no global/env lookup).
- AdminSettingsService: buildPageData, sanitization, audit snapshot, apply
  step and cache payload are all stripped of the three keys. Dead helper
  applyAppPrimaryColor + normalizePrimaryColor removed.
- Cache: SettingCacheService HOT_PATH_KEYS drops the three keys.
- API: /settings (GET/PUT) and /settings/public (GET) no longer expose
  appearance fields; OpenAPI schemas pruned accordingly.
- Audit redaction list shortened.
- DB: db/init/init.sql seed rows removed. No migration for existing
  installs — legacy rows stay inert.
- i18n + docs: setting.app_theme, setting.app_theme_user,
  setting.app_primary_color removed from de+en locales; reference and
  explanation docs updated.
- Tests: covering tests for the removed methods pruned; ThemeResolutionTest
  rewritten for tenant-only semantics. One unrelated PHP-CS-Fixer issue in
  UserRegistrar.php cleaned up to keep QG-006 green.

Workflow: .agents/runs/SETTINGS-APPEARANCE-TENANT-ONLY/ (gitignored).
Gates: QG-001..003, QG-006, QG-008 all pass (1985 tests, 28764 assertions).
Reviews: code, security, acceptance all pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 22:40:15 +02:00
fs
6b05bbfe9c refactor(customfield): interface-based DI for repositories
Align CustomField-Schicht mit core/Repository/Access-Muster. Vier
Repositories (Tenant-Definition, Tenant-Option, User-Value, User-Value-Option)
bekommen je ein Interface, werden von static auf instance umgestellt und
ueber CustomFieldRepositoryFactory injiziert.

TenantCustomFieldService und UserCustomFieldValueService injizieren die
Repository-Interfaces statt statisch aufzurufen. CustomFieldServicesFactory
reicht Instanzen durch. AddressBookService (Modul) bekommt
TenantCustomFieldOptionRepositoryInterface per Container — Interface-Kopplung
verbessert Modul-Isolation gegenueber dem vorherigen statischen Aufruf.

Kein Verhaltenswechsel, keine SQL-Aenderung, keine Service-Signatur-Aenderung.
Alle tenant_id-Parameter erhalten (GR-SEC-009, Security-Review SR-002).

Oeffnet den Weg fuer Cluster B1 (TenantCustomFieldService-Tests) und B2
(UserCustomFieldValueService-Tests), die nun mit createMock() moeglich sind.

Nebenbei zwei PHPStan-Folge-Findings korrigiert:
- Veralteter Ignore-Eintrag fuer findById aus phpstan-baseline.neon entfernt
- Redundanter is_array-Check in TenantCustomFieldOptionRepository entfernt
  (durch typisierte Interface-Signatur abgedeckt)

Gates: QG-001 (1945 Tests, 28581 Assertions) / QG-002 / QG-003 / QG-006 pass.

Workflow: .agents/runs/CUSTOMFIELD-DI-REFACTOR-001/

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 19:32:14 +02:00
fs
f793f5d493 chore(drawer): close consolidation gaps after three-consumer rollout
Loose ends surfaced by comparing the three drawer consumers side by side:

- Core i18n was missing `Open full page`, `Loading`, `Failed to load` —
  admin/users showed English strings in the German UI because those labels
  resolve from core, not module catalogs.
- Helpdesk tickets page config did not pass drawer labels; the JS fell back
  to its hard-coded English defaults. Labels now wired via `t(...)`.
- CLAUDE.md notes the `fetchUrl` single-expression convention (required so
  the architecture test can statically locate the fragment path) and
  documents the `onContentLoaded` hook for module-specific runtime
  (helpdesk's async communication feed uses it).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:25:07 +02:00
fs
22966fdacf feat(helpdesk): ticket detail drawer — third drawer consumer
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>
2026-04-22 17:03:25 +02:00
fs
c1cf9f94bb feat(core): UserProfileViewService + admin/users detail drawer
Extracts the user profile read-view into a core service + shared partial so
modules no longer duplicate the assignment/scope logic, and migrates the
admin/users list as the drawer's second consumer.

Consolidation:
- `core/Service/User/UserProfileViewService` owns the single read-path that
  resolves tenants/departments/roles for a user UUID, enforces scope-aware
  department visibility, and returns a consistent `{status, user}` shape.
- `templates/partials/app-user-profile.phtml` is the shared render template;
  takes `$profileKey` for storage namespace + lightbox grouping.
- `AddressBookService::buildViewContext()` shrinks to a one-line delegation
  and drops its `UserAssignmentService` dependency. The old
  `modules/addressbook/templates/address-book-profile.phtml` is removed.

admin/users migration:
- `pages/admin/users/view-fragment($id).php` + `(none).phtml` use the core
  service and enforce `ABILITY_ADMIN_USERS_VIEW`.
- `admin-users-index.js` replaces the grid-native `linkColumn` on the first
  name with a formatter that emits `data-drawer-trigger`; `linkColumn` is
  disabled so the drawer click handler wins. `fullUrl` points at the edit
  form — drawer → edit is one click.
- Drawer labels wired via page config.

The new `DetailDrawerFragmentContractTest` validated the users fragment
endpoint automatically — no manual test additions were needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 15:49:10 +02:00
fs
21c331cd06 test(architecture): lock down two routing/filter silent-failure modes
Encodes two bugs that recently cost debugging time as architecture tests so
they cannot recur.

PageTemplateNamingTest
  MintyPHP's Router splits page filenames on parens. `view(default).phtml` →
  view="view", template="default". A nested group like `view($id)(none).phtml`
  produces an empty view name, resolves to the wrong file, and the response
  silently becomes an empty body. URL parameters belong only in the .php
  action filename. This test flags any .phtml with more than one paren group.

FilterSchemaConsistencyTest
  Every query key declared in a list's filter-schema.php must also appear in
  the toolbar section (standard pagination/sort keys exempted). If a filter
  param is not in the toolbar, Grid.js's URL-sync drops it on every refetch —
  the filter works on initial page load and breaks after any user interaction.
  This test caught one dormant violation (system-audit's target_type), which
  is now declared as a hidden toolbar field for future use.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 15:37:26 +02:00
fs
811588290c feat(core): detail drawer + address book list redesign
Introduces a reusable core detail-drawer primitive that slides in from the
right and loads any view via a `*-fragment(none).phtml` endpoint. Bundles the
address book list overhaul that is its first consumer.

Core additions:
- `app-detail-drawer.js` — generic drawer with stepper, focus trap, body
  scroll-lock, URL-hash deep-linking, session-expiry detection
- `app-fragment-init.js` — auto-wires tabs/lookups/confirm/file-upload/
  fslightbox inside injected HTML; consumers do not re-initialize components
- `app-focus-trap.js` — shared focus-trap + refcounted scroll-lock, used by
  both filter-drawer and detail-drawer
- `getHtml()` in `app-http.js` + `SessionExpiredError`; drawer reloads the
  page on auth redirect instead of rendering the login form in the panel
- `DetailDrawerFragmentContractTest` enforces that every `initDetailDrawer`
  consumer ships matching `*-fragment($id).php` + `*-fragment(none).phtml`

Address book list:
- Grid collapses from 9 columns to 4 (identity / context / phone / actions)
  with a two-line identity cell (avatar + name + email)
- Tenant register tabs above the grid using the `app-list-tabs` partial;
  tenant filter wired via hidden toolbar field so grid.js forwards it on
  every data fetch
- Profile body extracted to a shared partial so the full-page view and the
  new drawer fragment share the same markup
- New i18n keys for the drawer/list labels

Also refactors `app-filter-drawer` to reuse the shared focus-trap and
scroll-lock instead of maintaining its own copy, and documents the
detail-drawer convention in CLAUDE.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 15:22:26 +02:00
fs
c14ff27c37 feat(helpdesk): promote ticket description to its own column in updates list
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>
2026-04-22 10:24:29 +02:00
fs
0e82e00495 feat(core): endpointUrl() helper — query-safe URLs for module routes
Introduce a helper that returns the registered route TARGET for a given
source path, so URLs built for endpoints that carry query parameters do
not rely on MintyPHP's applyRoutes() rewrite layer — that layer compares
the full request URI (including `?query`) against source paths and
silently misses modules where path ≠ target.

- core/Support/helpers/app.php: endpointUrl($path) consults
  ModuleRegistry::getRoutes(), returns lurl(target) when the path is a
  registered module source path, otherwise falls through to lurl($path).
  Safe fallback when the container or registry is unavailable.
- modules/audit/pages/audit/system-audit/index(default).phtml: replace
  the ad-hoc target-path workaround with endpointUrl('admin/system-audit/
  export'). Callers can now write the natural source path without
  knowing about the rewrite trap.
- Apply the same helper to modules/helpdesk/.../domains/index and
  pages/admin/users/index for consistency — a no-op where path already
  equals target, but establishes the convention: every export/data
  endpoint URL in page configs goes through endpointUrl().
- tests/Support/Helpers/EndpointUrlTest: 5 cases covering source→target
  resolution, idempotence when path == target, fall-through for
  unregistered paths, leading-slash normalization, and graceful
  degradation when the registry is missing. Uses
  AppContainerIsolationTrait per the contract test in
  tests/Architecture/AppContainerIsolationContractTest.

Gates: PHPUnit 1894 OK, PHPStan 0 errors, module:sync ok.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:39:36 +02:00
fs
7c67d273aa fix(audit): reach system-audit export via target path, not route path
The export dropdown built URLs via lurl('admin/system-audit/export') which
hit a framework quirk: MintyPHP Router::applyRoutes() compares the full
request URI (including ?query) against registered source paths. When
a browser fetched /de/admin/system-audit/export?format=csv the rewrite
to audit/system-audit/export silently missed, the router fell back to
file-based resolution, found no such file under admin/, and redirected
back to the list page — the user saw the grid "reload" and nothing
downloaded.

Only audit is affected because it is the only module where the route
path (admin/…) differs from the page target path (audit/…); helpdesk
and admin/users route paths equal their targets so file-based routing
catches the export URL even when applyRoutes misses.

Fix: use lurl('audit/system-audit/export') in the page config — the
target path — which resolves directly via file-based routing regardless
of query string. This matches the convention the same JS already uses
for `dataUrl: 'audit/system-audit/data'`. Short inline comment records
why.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:31:42 +02:00
fs
3b6896266e refactor(audit): extract SystemAuditRowPresenter shared by data + export
Move the row formatting that was duplicated between system-audit's
data() and export() endpoints into a single presenter. Both endpoints
now call presentAll() on the same object, so enum label translation,
actor resolution, and timestamp formatting cannot drift between the
Grid.js UI and the CSV download.

- SystemAuditRowPresenter::present()/presentAll(): canonical row shape
  with outcome + outcome_label + outcome_badge, channel + channel_label,
  actor_user_label with display-name-then-email fallback, and safe
  defaults for every missing field.
- data().php shrinks from ~45 to ~15 lines; export().php drops its
  inline outcome/channel/actor resolvers and reads the already-
  resolved fields from the presenter output.
- AuditContainerRegistrar registers the presenter.
- tests/Module/Audit/Service/SystemAuditRowPresenterTest: 9 cases
  covering enum normalization, unknown-value fallback, actor label
  precedence (display name → email → "-"), whitespace trimming, safe
  defaults for missing keys, and iterable input.
- StatusTaxonomyContractFiles: the taxonomy data contract now points
  at the presenter (the single source of truth for badge/label
  resolution) instead of the thin data endpoint, and the presenter is
  added to the literal-guard file list.

Gates: PHPUnit 1889 OK, PHPStan 0 errors, module:sync ok.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:20:39 +02:00
fs
0386ee08a6 feat(audit): CSV/Excel export for system-audit via core primitive
Third consumer of the generic export primitive. Adds a
GET /admin/system-audit/export endpoint, the reusable export dropdown
to the list titlebar, and wires initListExport() into the page JS.

- pages/audit/system-audit/export().php: guard + ABILITY_SYSTEM_AUDIT_VIEW,
  reuses the existing filter-schema.php via gridParseFiltersFromSchemaFile
  so exported rows match the grid under the same filters, caps the limit
  at 5000, and declares 11 ExportColumns (ID, created, event, status,
  channel, actor, actor email, target type/uuid, request id, error code)
  with translated enum labels for outcome/channel.
- module.php: new admin/system-audit/export route.
- index(default).phtml: app-list-export-dropdown partial placed above the
  existing purge action; exportUrl added to pageConfig via
  lurl('admin/system-audit/export').
- admin-system-audit-index.js: imports initListExport and invokes it
  whenever config.exportUrl is present.
- i18n de/en: new "Actor email" key (required by TranslationKeysTest).

Gates: PHPUnit 1880 OK, PHPStan 0 errors, module:sync ok.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:15:43 +02:00
fs
12356e2266 feat(core): generic CSV export primitive + helpdesk domains consumer
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>
2026-04-21 21:57:19 +02:00
fs
fb86c02427 fix(helpdesk): repair security-level persistence, enrichment, and list UX
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>
2026-04-21 14:04:21 +02:00
fs
6ac685084d Feature: Security Level mit Notiz für Domains
Implementiert ein Dialog-basiertes UI zum Bearbeiten des Security Levels
mit optionaler Notiz-Funktion für Helpdesk-Domains.

Backend:
- DomainSecurityLevelRepository: CRUD für security_levels mit note-Spalte
- DomainSecurityLevelService: Business-Logik mit Batch-Enrichment
- security-level-data() Endpoint: AJAX + Full-Page POST mit CSRF
- Migration 010: note TEXT-Spalte hinzugefügt
- Bugfix: findByTenantAndDomainNo gibt null bei leerem Array zurück

Frontend (Domain-Liste):
- Badge in Grid klickbar → öffnet Modal-Dialog
- Dialog mit Level-Select + Notiz-Textarea
- AJAX-Save mit Live-Badge-Update ohne Reload
- Event-Delegation für dynamische Grid-Inhalte

Frontend (Domain-Detail):
- Notiz-Feld im Security-Level-Formular
- PRG-Pattern mit Flash-Messages

Core:
- app-http.js: DEFAULT_HEADERS auf XMLHttpRequest für CSRF-Kompatibilität

i18n:
- Keys: Priority, Note, Optional note, Security level updated

Tests:
- DomainSecurityLevelServiceTest: CRUD + Batch-Enrichment
2026-04-21 13:43:16 +02:00
fs
c3de7d4238 feat(js): enforce global lifecycle hard-cuts and helpdesk list adapter 2026-04-20 23:01:54 +02:00
fs
5219bbaff2 refactor(js): migrate audit and addressbook list pages to list factory 2026-04-20 22:45:54 +02:00
fs
7c47e818f2 feat(js): harden global HTTP and module import contracts 2026-04-20 22:41:07 +02:00
fs
f189ef9df6 feat(js): add app-http contracts and migrate helpdesk runtime layer 2026-04-20 22:31:13 +02:00
fs
ec2f03e0cf helpdesk: remove unused UpdateRepository::findById 2026-04-20 19:58:51 +02:00
fs
da0abc824a feat(helpdesk): add software updates tracking with BC ticket integration
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>
2026-04-16 13:21:40 +02:00
fs
693d33a0c8 feat(helpdesk): add domain detail page with customer, contract, and handover sections
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>
2026-04-16 13:21:24 +02:00
fs
e7c60468c9 feat(helpdesk): add domain linking, bulk actions, and revision polish to handovers
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>
2026-04-16 13:21:12 +02:00
fs
03e15eaf08 feat(helpdesk): add handover revision history with timeline, diff, and restore
Every save creates an immutable revision snapshot. The aside shows a
commit-graph-style timeline (newest first) with vertical line, circle
nodes, version numbers, change type, user, and date. Clicking a past
revision renders it readonly with inline git-diff-style highlights
(changed/added/removed with tinted backgrounds). Compare mode allows
diffing any two arbitrary revisions. MANAGE users can restore old
versions, which creates a new revision preserving full history.

New: HandoverRevisionService, HandoverRevisionRepository,
migration 006, 14 PHPUnit tests, DE/EN translations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:14:40 +02:00
fs
320f0a5a00 feat(helpdesk): polish handover wizard and list page UI
- Simplify wizard step indicator markup (divs instead of ol/li)
- Narrow wizard content width, compact spacing
- Use app-action-success button style on list titlebar
- Add handover-form-heading class for consistent section spacing
- Remove unused wrapper divs and redundant CSS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:29:43 +02:00
fs
9a49e18e27 feat(helpdesk): redesign handover edit page sidebar
- JOIN users table in findById for created/updated-by display names
- Fix row mapping to handle merged JOIN results
- Use product name instead of debitor name in page title
- Sidebar: collapsible customer/product sections, status dropdown,
  audit partial with user attribution

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:29:36 +02:00
fs
2600e62a73 fix(helpdesk): tighten debitor lookup permission and use Router::json
Require ABILITY_HANDOVERS_CREATE instead of generic ABILITY_ACCESS.
Replace manual header + echo with Router::json() for consistency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:29:31 +02:00
fs
f878d8909d feat(helpdesk): pass field values directly during handover creation
Avoids the separate updateFields call after insert by accepting fieldValues
in HandoverService::create() and validating/encoding them before the insert.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:29:27 +02:00
fs
a26c106083 feat(helpdesk): add handover protocol management
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>
2026-04-15 20:42:41 +02:00
fs
63be2def76 docs: add UI/UX component guidelines and improve breadcrumb/tab-panel styling
Add "Komponenten in Formularen" section to boundaries-ui.md with rules
for core button classes, margin resets, checkbox sizing, sticky positioning,
icon-only buttons, responsive grids, and CSS variables. Add matching
quick-reference bullets to CLAUDE.md UI Patterns section.

Fix breadcrumb wrapping: nowrap + overflow hidden + flex-shrink for
single-line behavior. Add data-tab-panel-wide opt-out for full-width
tab panels (default 70ch).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 18:44:29 +02:00
fs
2ec632ac8b feat(helpdesk): polish handover schema editor UI/UX
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>
2026-04-15 18:08:12 +02:00
fs
44153f513f feat(helpdesk): add handover protocol schema editor to software products
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>
2026-04-15 16:44:12 +02:00
fs
364bda67a9 fix(helpdesk): handle missing session in EffectiveHelpdeskSettingsService
Scheduler jobs run without an active session, causing SessionStore::all()
to fail. Wrap the call in try-catch to fall back to global config (tenant
ID 0) when no session is available.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 12:31:38 +02:00
fs
20f582f459 refactor(helpdesk): remove redundant readonly fields from software product form
Code and BC description are already shown in the aside — no need to
duplicate them as readonly inputs in the form.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 22:26:18 +02:00
fs
14ba29c468 fix(helpdesk): align software products aside to core detail page standard
Use badge-list wrapper, <p><small> pattern, <hr> separators, and dt()
for date formatting — matching the departments edit page convention.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 22:24:09 +02:00
fs
06c7651be7 fix(helpdesk): fix Session class not found in software products form
Use fully qualified \MintyPHP\Session in _form.phtml and remove
duplicate CSRF token call from edit template (already in form partial).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 22:23:14 +02:00
fs
ef4473de64 feat(helpdesk): add Software Products page with BC contract type sync
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>
2026-04-14 22:16:21 +02:00
fs
d472026df4 feat(helpdesk): add Domains list page with BC OData contract type enrichment
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>
2026-04-14 21:34:32 +02:00
fs
3f0db68b27 refactor: fix all 105 PHPStan 2 strict type findings across core and modules
Resolve all non-false-positive PHPStan 2 findings, reducing the baseline
from 486 to 382 entries (only unused-public false positives remain).

Categories fixed:
- Remove 39 redundant type checks (is_string, is_array, is_object, method_exists)
- Remove 12 no-op array_values() on already-sequential lists
- Simplify 13 null-coalesce on guaranteed offsets
- Remove 8 always-true instanceof checks
- Replace 12 redundant test assertions (assertTrue(true) → addToAssertionCount)
- Simplify 6 unnecessary nullsafe operators (?-> → ->)
- Fix 6 always-true !== '' comparisons
- Fix remaining: unset.offset, return.unusedType, staticMethod narrowing

53 files changed across core/, modules/, pages/, and tests/.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 12:54:20 +02:00
fs
4e6b586b77 refactor: enforce layer discipline — move repositories, fix constructor injection
- Move HelpdeskTenantSettingsRepository and HelpdeskTokenRepository from
  Service/ to Repository/ directory (GR-SEC-003: SQL only in repositories)
- Make AccessControl constructor require IntendedUrlService explicitly
  instead of falling back to `new IntendedUrlService()` (GR-TEST-002:
  no service instantiation outside factories)
- Update all imports and tests accordingly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 19:28:07 +02:00
fs
a17c2c7cea refactor(audit): decouple API audit classes from core via interfaces
Core code (lib/) no longer imports any MintyPHP\Module\Audit\* classes directly.
New ApiAuditServiceInterface and ApiSystemAuditReporterInterface follow the
existing pattern (interface + null implementation in core, concrete binding
from module registrar). DoctorRunner and helpers/app.php now resolve through
DI interfaces, ensuring the audit module remains fully optional.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 19:19:17 +02:00
fs
4967095bb8 fix: resolve all quality gate failures — i18n key, typography tokens, PHPStan errors, style
- Add missing i18n key "LDAP attribute map could not be encoded" (de+en)
- Replace raw font-weight/line-height with design tokens in app-topbar.css
- Fix import ordering in SchedulerRunService.php (php-cs-fixer)
- Fix PHPStan mock chain errors in DebitorDetailServiceTest and TenantSsoServiceTest (PHPUnit 12 API)
- Simplify redundant comparison in NotificationService dedupe window
- Add PHPStan type hint for mock callback array in SchedulerJobFailedNotificationListenerTest
- Fix undefined variable and add flow-analysis suppression in login().php
- Increase dev PHP memory_limit to 1G for PHPStan (prod stays 256M)

All 6 quality gates now pass: QG-001 (1618 tests), QG-002 (0 errors),
QG-003 (6630 assertions), QG-006 (0 fixable), QG-008, QG-009.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 19:06:12 +02:00
fs
3827b09205 test(helpdesk): add OData injection rejection tests and meetings cache key coverage
Adds 11 tests for BcODataGateway covering getMeetingsForCustomer (empty,
whitespace, not-configured), OData injection rejection for all customerNo
methods (meetings, contract lines, escalation tickets, controlling), and
valid input acceptance. Also adds meetingsKey format and inclusion tests
for DebitorCacheControl.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 15:16:57 +02:00
fs
a0cb345e04 fix(helpdesk): use strict input validation for all user-supplied customerNo in OData filters
Replaces escapeODataTrustedLiteral with escapeODataStrictUserInput for all
customerNo parameters that originate from HTTP request input. The trusted
literal escape (quote-only) remains for BC-sourced values like customerName.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 15:07:40 +02:00
fs
42efdb8102 feat(helpdesk): add debitor meetings tab with upcoming/past meeting display
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>
2026-04-13 14:13:29 +02:00
fs
cacd1fb6d1 feat(helpdesk): refactor multi-tenant BC settings and risk radar improvements
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>
2026-04-06 22:22:17 +02:00
fs
a3d05baffa feat(ui): add outline/fill icon-swap pattern for aside icon bar
Introduce data-icon-swap attribute with paired outline/fill <i> elements.
CSS toggles to filled icon on hover and active state. Module slots
auto-derive fill variant from icon name, with explicit icon_fill override
for icons without a fill counterpart (e.g. bi-headset). Add enforcement
script bin/icon-swap-check.sh.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 22:22:00 +02:00
fs
ae509c607b refactor(ui): consistent outline/fill icons in topbar, remove dropdown chevrons, truncate breadcrumbs
Use Stripe pattern: outline icons by default, filled for active state
(bookmarks bookmarked, notifications unread). Remove chevron arrows
from tenant/user dropdowns. Add max-width with ellipsis to breadcrumb
items to prevent overflow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 22:21:50 +02:00
fs
fe88e748a3 fix(notifications): use createFromMessage directly for CLI-safe scheduler notifications
createForTenantAdminUsersFromMessage relies on listActiveUserIdsByTenantId
which returns empty results in CLI context (scheduler). Instead, iterate
admin users directly and call createFromMessage per user per tenant.
Verified end-to-end: notification created, dedup blocks duplicates.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 12:38:47 +02:00
fs
34892aff6f fix(notifications): use per-type dedup window, 24h for scheduler failures
Change DEDUPE_TYPES from bool to int|true — int value overrides the
default 30-min window. scheduler.job_failed uses 1440 min (24h) so
admins receive at most one notification per failed job per day.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 12:29:04 +02:00