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>
Reduce tenant grid from 8 columns to 2. Remove status, theme, sso,
active/inactive users, and created columns from the list view.
All details remain accessible on the edit page.
- filter-schema: allow order by description and users only
- data endpoint: return minimal row shape (id, uuid, description,
status metadata for badge only, is_default, has_avatar, total_users)
- JS grid: 2 visible columns + hidden UUID for navigation
- CSS: add .grid-tenant-profile utility for flex avatar+name layout
Refs: TENANT-LIST-MINIMAL-001
Cluster 1 der Testabdeckungs-Initiative (.agents/runs/TEST-SEC-COVERAGE-001).
+38 Tests / +233 Assertions, nur tests/** veraendert.
- SettingsAuthorizationPolicyTest: Allow-Pfade fuer UPDATE/BRANDING_UPDATE/
TOKENS_REVOKE/USER_LIFECYCLE_RUN plus Unknown-Ability — schliesst die
Halb-Test-Luecke, die zuvor nur Deny-Pfade fuer 4 von 5 Abilities pruefte.
- AssignableRoleServiceTest: neu (GR-TEST-001, vorher nur transitiv via
UserAssignmentServiceTest gedeckt).
- AuthCryptoGatewayTest: neu (Roundtrip + Fehlerpfad, GR-SEC-005).
- AuthSettingsGatewayTest: neu (Delegation zu 5 Settings-Sub-Gateways).
- UserTenantContextServiceTest: neu, 18 Tests, Tenant-Scope-Logik
(GR-SEC-009) — current/primary/fallback, inactive-filter, assign-checks.
LdapConnectionGateway und OidcHttpGateway bleiben bewusst ohne Direkttests:
beide sind architektonische Test-Seams (dokumentiert im Docblock bzw. reiner
statischer Curl-Delegator). Ihre Logik ist ueber Konsumenten-Tests
(LdapAuthServiceTest, MicrosoftOidcServiceTest) bereits gedeckt.
Gates: QG-001 (1945 Tests) / QG-002 (PHPStan L5) / QG-003 (Architecture) /
QG-006 (php-cs-fixer) pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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 first pass had admin/users sharing the address book profile partial,
which surfaced Contact/Address/Organization/About tabs that ignored the
admin-specific context. Separates the admin quick-view so it mirrors the
edit-page tab structure while staying read-only.
- `UserProfileViewService::buildAdminProfile()` extends the base profile
with `admin_extras` (active, email_verified_at, last_login_at,
last_login_provider, created, modified).
- `templates/partials/app-admin-user-profile.phtml` is the admin-only
render path: Master data · Organization · Security · About. Status
badge under the name; timestamps locale-formatted.
- `pages/admin/users/view-fragment($id).php` / `(none).phtml` switched
to the admin service method + partial.
- Drawer CSS aligns the aside with the main section's inline padding so
the content strip does not visually step in/out.
Address book is unchanged — still uses `buildProfile()` + the base
partial. Two i18n keys added (`Login provider`).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
Make the core-primitive-end-to-end rule for Grid.js list exports binding
instead of advisory — so future consumers (and reviewers) cannot quietly
drift back into inline fputcsv / hand-rolled headers / bespoke click
listeners / lurl() for query-carrying URLs.
- CLAUDE.md: new UI Patterns bullet + two "Never Do This" entries
spelling out the server/view/client contract in one place.
- .agents/checks/guard-catalog.json: new GR-UI-EXPORT entry describing
the end-to-end requirement (exportRequireGetRequest + exportCapLimit +
exportSendCsv + ExportColumn + shared filter-schema + dropdown partial
+ endpointUrl() + initListExport).
- .agents/checks/guard-enforcement-map.json: wire the new guard to
tests/Architecture/ListExportContractTest.php as the automated
evidence source.
- .agents/checks/guard-checklist.md & .agents/prompts/reviewer-code.md:
add GR-UI-EXPORT so the Code Reviewer prompt and the checklist flag
violations alongside GR-UI-LIST.
- tests/Architecture/ListExportContractFiles: registry of every list
export (currently helpdesk-domains, admin/users, audit/system-audit).
Adding a new list = one entry, contract test covers the rest.
- tests/Architecture/ListExportContractTest: six checks per registered
entry — endpoint uses the primitive, no hand-rolled output, data and
export share the same filter-schema, template includes the dropdown
partial and uses endpointUrl(), page module imports and invokes
initListExport, and the shared dropdown partial stays zero-config.
- pages/admin/users/export().php: align with the new contract by
switching from ad-hoc requestInput()->queryAll() reads to
gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'), same
as the data endpoint — eliminates the last place Grid and export
could disagree on what "the current filter" means.
Gates: PHPUnit 1900 OK (+6 contract checks), PHPStan 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
Two pre-existing findings surfaced during the export-feature test run.
Both are trivial housekeeping, not related to the feature:
- phpstan-baseline.neon: drop the stale ignore for
RequestInput::wantsJson — the method is now called from the
helpdesk security-level endpoint and the new export flavor helper,
so PHPStan no longer matched the pattern and failed with
`ignore.unmatched` (non-ignorable).
- tests/Service/Auth/SsoUserLinkServiceTest.php: replace the deprecated
isType('array') / isType('string') assertions with the
PHPUnit 13-compatible isArray() / isString() equivalents.
Gates after cleanup: PHPUnit 1880 OK (0 deprecations), PHPStan 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the inline export implementation with the new generic primitive
(core/Service/Export + helpers/export + app-list-export.js). No more
hand-rolled fputcsv loop, header setup, or escape closure duplicated
here — one implementation, tested once, reused everywhere.
- export().php: uses exportRequireGetRequest, exportCapLimit,
exportResolveFlavor, ExportColumn[], exportSendCsv. Phone/mobile/
short_dial keep allowSignedNumeric=true so +49 numbers render
untouched.
- export(none).phtml: deleted — headers + fputcsv now live in the
core helper.
- index(default).phtml: inline <details class="dropdown"> "Export CSV"
replaced by the reusable app-list-export-dropdown partial, exposing
CSV and Excel flavors. exportUrl added to pageConfig.
- app-users-list.js: hand-rolled export-button binding removed in
favor of initListExport({ gridConfig, exportUrl }).
- admin-users-index.js: forwards config.exportUrl into
initUsersListPage.
Net effect: ~100 lines of duplicated export plumbing removed from the
users page; users gain the Excel flavor for free.
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
Jobs that have never run have last_run_status=NULL which caused a crash
when calling ->value and ->badgeVariant() on null. Use explicit null
checks instead of direct property access.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The new permission was missing from the admin role seed in init.sql and
needed an idempotent update script for existing installations.
Co-Authored-By: Claude Opus 4.6 (1M context) <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>