diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..b5be4ee --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,12 @@ +{ + "permissions": { + "allow": [ + "Bash(Remove-Item -Recurse -Force \"modules/helpdesk/pages/helpdesk/dashboard\")", + "Bash(Remove-Item -Force \"modules/helpdesk/pages/helpdesk/dashboard-data\\(\\).php\")", + "Bash(Remove-Item -Force \"modules/helpdesk/web/js/helpdesk-dashboard.js\")", + "Bash(git checkout *)", + "Bash(git add *)", + "Bash(git commit -m ' *)" + ] + } +} diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index e98191c..fefe8fe 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -49,6 +49,7 @@ services: db: image: mariadb:11.4 + command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci env_file: - .env environment: diff --git a/docker-compose.yml b/docker-compose.yml index d625989..24a54d9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,6 +41,7 @@ services: db: image: mariadb:11.4 + command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci env_file: - .env environment: diff --git a/docker/php/php.ini b/docker/php/php.ini index 4ae18b8..131d34b 100644 --- a/docker/php/php.ini +++ b/docker/php/php.ini @@ -1,3 +1,8 @@ +; --- Character encoding --- +default_charset = "UTF-8" +mbstring.internal_encoding = UTF-8 +mbstring.encoding_translation = Off + ; --- Error handling (verbose for development) --- display_errors=1 display_startup_errors=1 @@ -12,8 +17,8 @@ post_max_size=12M ; --- Security hardening (match prod to catch issues early) --- expose_php=Off allow_url_include=Off -open_basedir=/var/www:/tmp -disable_functions=exec,passthru,shell_exec,system,proc_open,popen,parse_ini_file,show_source +open_basedir= +disable_functions= ; --- Session hardening --- session.use_strict_mode=1 diff --git a/docker/php/php.prod.ini b/docker/php/php.prod.ini index 4054d3d..655b194 100644 --- a/docker/php/php.prod.ini +++ b/docker/php/php.prod.ini @@ -1,3 +1,8 @@ +; --- Character encoding --- +default_charset = "UTF-8" +mbstring.internal_encoding = UTF-8 +mbstring.encoding_translation = Off + ; --- Error handling --- display_errors=0 display_startup_errors=0 diff --git a/modules/helpdesk/CLAUDE.md b/modules/helpdesk/CLAUDE.md new file mode 100644 index 0000000..984594e --- /dev/null +++ b/modules/helpdesk/CLAUDE.md @@ -0,0 +1,179 @@ +# CLAUDE.md — Helpdesk Module + +Self-contained MintyPHP module. Namespace: `MintyPHP\Module\Helpdesk\*`. All changes stay inside `modules/helpdesk/` — never touch core templates or global layout. + +After any manifest change (new route, permission, slot): `docker compose exec php php bin/console module:sync` + +--- + +## Architecture + +``` +lib/Module/Helpdesk/ + HelpdeskAuthorizationPolicy.php — all ability/permission constants + authorize() + HelpdeskContainerRegistrar.php — all DI bindings (add new services here) + Handler/ — scheduled job handlers + Providers/HelpdeskLayoutProvider.php — resolves can_* flags for sidebar nav + Repository/ — SQL only, no HTTP + Service/ — business logic, no HTML +pages/helpdesk/ — actions (*.php) + views (*.phtml) +templates/ + aside-helpdesk-panel.phtml — sidebar navigation + helpdesk-ticket-detail.phtml — shared ticket detail body +i18n/default_de.json + default_en.json — 551 keys each, always add both +migrations/ — 011 SQL files applied via module:migrate +tests/Module/Helpdesk/Service/ — 20 test files, happy path + edge case required +web/css/helpdesk.css +web/js/ — 16 JS files, components + page scripts +``` + +--- + +## Page File Naming + +| URL | Action file | View file | +|-----|-------------|-----------| +| `helpdesk/team` | `pages/helpdesk/team/index().php` | `team/index(default).phtml` | +| `helpdesk/handovers/edit/{id}` | `pages/helpdesk/handovers/edit($id).php` | `handovers/edit(default).phtml` | +| `helpdesk/dashboard` | `pages/helpdesk/dashboard/index().php` | `dashboard/index(default).phtml` | +| data endpoint | `pages/helpdesk/foo-data().php` | — | +| drawer fragment | `pages/helpdesk/foo-fragment($id).php` | `foo-fragment(none).phtml` | + +**View structure pattern** (always): +```php +
+
+ t('Page title')]; + require templatePath('partials/app-details-titlebar.phtml'); + ?> + +
+
+``` + +--- + +## Permissions (10 total) + +All constants in `HelpdeskAuthorizationPolicy`: + +| Constant | Permission key | +|----------|---------------| +| `ABILITY_ACCESS` | `helpdesk.access` — base access, customer search + detail views | +| `ABILITY_SETTINGS_MANAGE` | `helpdesk.settings.manage` | +| `ABILITY_TEAM_WORKLOAD` | `helpdesk.team-workload.view` | +| `ABILITY_RISK_RADAR` | `helpdesk.risk-radar.view` | +| `ABILITY_SOFTWARE_PRODUCTS_MANAGE` | `helpdesk.software-products.manage` | +| `ABILITY_HANDOVERS_VIEW` | `helpdesk.handovers.view` | +| `ABILITY_HANDOVERS_CREATE` | `helpdesk.handovers.create` | +| `ABILITY_HANDOVERS_MANAGE` | `helpdesk.handovers.manage` | +| `ABILITY_UPDATES_VIEW` | `helpdesk.updates.view` | +| `ABILITY_UPDATES_MANAGE` | `helpdesk.updates.manage` | + +New pages reuse existing permissions — no new permission keys without product decision. + +**Action file boilerplate:** +```php +Guard::requireLogin(); +Guard::requireAbilityOrForbidden(HelpdeskAuthorizationPolicy::ABILITY_ACCESS); +$session = app(SessionStoreInterface::class)->all(); +$tenantId = (int) ($session['current_tenant']['id'] ?? 0); +``` + +--- + +## Services & Repositories + +**Gateways (external BC API):** +- `BcODataGateway` — OData V4 queries (customers, domains, tickets, contacts) +- `BcSoapGateway` — SOAP (ticket communication feed, file downloads) + +**Core services:** +- `EffectiveHelpdeskSettingsService` — resolves BC config per tenant (global + override) +- `HelpdeskOAuthTokenService` — OAuth2 token acquisition + cache +- `DebitorSearchService` — debtor search by name/number +- `DebitorDetailService` — debtor master data + contacts + tickets +- `DomainDetailService` — domain/contract data + handover/update enrichment +- `DomainListService` — domain list with security level enrichment +- `HandoverService` — handover protocol CRUD + status transitions +- `HandoverRevisionService` — revision snapshots for audit trail +- `UpdateService` — software update tracking + assignment +- `SoftwareProductService` — product catalog + handover schema +- `SoftwareProductSyncService` — BC sync (scheduled daily 02:00 CET) +- `RiskRadarService` — risk scoring engine (5 dimensions, 0–100 score) +- `DomainSecurityLevelService` — security level get/set (niedrig/normal/hoch/kritisch) +- `SystemRecommendationEngine` — rule-based recommendation engine (no constructor) + +**Repositories:** +- `HandoverRepository` — insert, findById, listPaged, updateFieldValues, updateStatus, deleteByIds, findByDomainNo, transactions +- `UpdateRepository` — insert, findByTicketNo, findByDomainNo, findAllByTenant, updateAssignment +- `SoftwareProductRepository` — upsertByCode, listPaged, updateHandoverSchema, softDeleteNotInCodes +- `HandoverRevisionRepository` — insert, getLatestRevisionNumber, listByHandover +- `DomainSecurityLevelRepository` — findByTenantAndDomainNo, listLevelsByDomainNos, upsert +- `HelpdeskTenantSettingsRepository` — findByTenantId, upsert, deleteByTenantId +- `HelpdeskTokenRepository` — findValidToken, storeToken, deleteByTenantId (encrypted) + +New service → register in `HelpdeskContainerRegistrar::register()` + add `@api` annotation if called from pages. + +--- + +## Sidebar Navigation + +**`HelpdeskLayoutProvider`** resolves these flags → passed as `$layoutNav['helpdesk.nav']`: + +`can_view_dashboard`, `can_manage_settings`, `can_view_team`, `can_view_risk_radar`, +`can_manage_software_products`, `can_view_handovers`, `can_create_handovers`, +`can_manage_handovers`, `can_view_updates` + +**`aside-helpdesk-panel.phtml`** — 5 collapsible groups: +1. **Overview** (`bi-speedometer2`) — Dashboard +2. **Lookup** (`bi-search`) — Customers, Domains +3. **Monitoring** (`bi-bar-chart-line`) — Team workload, Risk radar +4. **Operations** (`bi-clipboard-check`) — Handovers, Updates +5. **Administration** (`bi-sliders`) — Software products, Settings + +Adding a nav item = 3 steps: (1) add `can_*` flag to `HelpdeskLayoutProvider`, (2) add `$dashboardActive = navActive(...)` + item to group in `aside-helpdesk-panel.phtml`, (3) add condition to `$customersActive` override if needed. + +--- + +## DB Schema (relevant tables) + +| Table | Key columns | +|-------|-------------| +| `helpdesk_handovers` | id, tenant_id, status (draft/in_progress/completed/archived), debitor_no, domain_no | +| `helpdesk_handover_revisions` | handover_id, revision_number, snapshot_json | +| `helpdesk_updates` | id, tenant_id, status (open/assigned/done/resolved/closed), ticket_no, domain_no | +| `helpdesk_software_products` | code, name, handover_schema_json | +| `helpdesk_domain_security_levels` | tenant_id, domain_no, level, note | +| `helpdesk_tenant_settings` | tenant_id, encrypted BC connection overrides | +| `helpdesk_oauth_token_cache` | tenant_id, token (encrypted), expires_at | + +Tenant scope enforced on every query — always pass `$tenantId` to repository methods. + +--- + +## Tests + +Location: `tests/Module/Helpdesk/Service/` +Pattern: `->method('foo')->willReturn([...])` — no `->with()` (PHPUnit 12 incompatible with PHPStan). +Every new/changed Service or Gateway needs happy path + at least one edge case test. + +Run: `docker compose exec php vendor/bin/phpunit modules/helpdesk/tests/` + +--- + +## JS Components + +Registered via `data-app-component=""` on the container div: +- `helpdesk-detail` — customer search detail +- `helpdesk-domain-detail` — domain detail +- `helpdesk-ticket` — ticket view +- `helpdesk-team` — team workload +- `helpdesk-risk-radar` — risk radar +- `helpdesk-settings` — settings config +- `helpdesk-communication` — ticket communication feed +- `helpdesk-handover-domain-select` — domain picker in handover wizard +- `helpdesk-handover-schema-editor` — handover schema JSON editor + +CSS class prefix: `helpdesk-` for module-specific styles in `web/css/helpdesk.css`. diff --git a/web/index.php b/web/index.php index d1dd49f..8e855df 100644 --- a/web/index.php +++ b/web/index.php @@ -64,6 +64,7 @@ $corePublicPaths = $routeCatalog->corePublicPaths(); // ── 4. Request context ────────────────────────────────────────────── RequestContext::start(); +header('Content-Type: text/html; charset=UTF-8'); header('X-Request-Id: ' . RequestContext::requestHeaderValue()); Firewall::start();