feat: add administration reporting and correction services

This commit is contained in:
Marco0300
2026-09-01 21:03:38 +02:00
parent de2bf277c4
commit b983f90dcb
24 changed files with 1392 additions and 31 deletions
+30
View File
@@ -6,6 +6,7 @@ require_once __DIR__ . '/../app/Domain/Reporting/ReportDataMapper.php';
require_once __DIR__ . '/../app/Domain/Reporting/ClientJobcardReport.php';
require_once __DIR__ . '/../app/Domain/Reporting/ClientHistoryReport.php';
require_once __DIR__ . '/../app/Domain/Reporting/TechnicianActivityReport.php';
require_once __DIR__ . '/../app/Domain/Reporting/SlaReport.php';
require_once __DIR__ . '/../app/Domain/Reporting/PrintReportRenderer.php';
$filters = ReportFilters::fromArray([
@@ -47,9 +48,38 @@ if ($activity !== [['technician_id' => 4, 'technician_name' => 'Tess', 'client_i
throw new RuntimeException('Technician activity must aggregate hours deterministically.');
}
$crossClientActivity = (new TechnicianActivityReport())->build([
['id' => 20, 'technician_id' => 4, 'technician_name' => 'Tess', 'client_id' => 8, 'client_name' => 'Beta', 'work_date' => '2026-09-02', 'hours' => 2, 'counts_toward_sla' => true, 'credentials' => 'secret'],
['id' => 10, 'technician_id' => 4, 'technician_name' => 'Tess', 'client_id' => 7, 'client_name' => 'Acme', 'work_date' => '2026-09-02', 'hours' => 1, 'counts_toward_sla' => true, 'internal_notes' => 'secret'],
], 'internal');
if ($crossClientActivity !== [
['technician_id' => 4, 'technician_name' => 'Tess', 'client_id' => 7, 'client_name' => 'Acme', 'hours' => 1.0, 'sla_hours' => 1.0],
['technician_id' => 4, 'technician_name' => 'Tess', 'client_id' => 8, 'client_name' => 'Beta', 'hours' => 2.0, 'sla_hours' => 2.0],
]) {
throw new RuntimeException('Technician activity must preserve client attribution and sort by technician then client.');
}
$clientActivity = (new TechnicianActivityReport())->build([
['technician_id' => 4, 'technician_name' => 'Tess', 'client_id' => 7, 'client_name' => 'Acme', 'work_date' => '2026-09-02', 'hours' => 1, 'counts_toward_sla' => true, 'password' => 'secret'],
], 'client');
if ($clientActivity !== [['client_id' => 7, 'client_name' => 'Acme', 'hours' => 1.0, 'sla_hours' => 1.0]]) {
throw new RuntimeException('Client technician activity projection must exclude technician and secret fields.');
}
$filteredSla = (new SlaReport(ReportFilters::fromArray(['client_id' => 7, 'date_from' => '2026-09-01', 'date_to' => '2026-09-30'])))->build([
['client_id' => 8, 'client_name' => 'Beta', 'allocated_hours' => 10, 'hours' => [1], 'start_date' => '2026-09-01'],
['client_id' => 7, 'client_name' => 'Acme', 'allocated_hours' => 10, 'hours' => [2], 'start_date' => '2026-09-01', 'credentials' => 'secret'],
], 'client');
if ($filteredSla !== [['client_id' => 7, 'client_name' => 'Acme', 'allocated_hours' => 10.0, 'used_hours' => 2.0, 'remaining_hours' => 8.0, 'usage_percentage' => 20.0, 'status' => 'within_limit']]) {
throw new RuntimeException('SLA report must apply filters and expose a safe audience projection.');
}
$html = (new PrintReportRenderer())->render('Client Jobcards', ['Reference', 'Status'], [['JC-2', 'open']]);
if (!str_contains($html, '@media print') || !str_contains($html, '<th>Reference</th>') || !str_contains($html, 'JC-2') || str_contains($html, '<script>')) {
throw new RuntimeException('Print report renderer must emit escaped, print-friendly HTML.');
}
$escapedHtml = (new PrintReportRenderer())->render('Report <x>', ['Value'], [['<script>alert(1)</script>']]);
if (str_contains($escapedHtml, '<script>alert') || !str_contains($escapedHtml, '&lt;script&gt;')) {
throw new RuntimeException('PDF-ready HTML must escape report content.');
}
printf("Report workflow tests: 5 passed\n");