Files
Mitgliederverwaltung/lib/Controller/ReportController.php
T
shahondin1624 53b3fd945a
Database Portability Tests / Integration (mysql) (push) Has been skipped
Database Portability Tests / Integration (postgres) (push) Has been skipped
Database Portability Tests / Integration (sqlite) (push) Has been skipped
Database Portability Tests / Verify no MySQL-specific SQL (push) Successful in 5s
Database Portability Tests / Unit Tests (PlatformHelper) (push) Failing after 43s
Release v0.3.2: Inventory tracking, test infra, and bugfixes
Inventory:
- General material, stock items, and sales CRUD
- Category management with CategoryPicker component
- Inventory reports service
- Database migrations and mappers

Frontend:
- Inventory view with tabs (Allgemeinmaterial, Verkaufsmaterial, Verkäufe)
- Forms for general material, stock items, and sales
- Search bar fix: flexbox wrapper with inline icon replaces broken NcTextField icon slot

Tests:
- All 1,491 tests pass (zero errors, warnings, deprecations)
- BundleImportServiceTest: fixed ZipArchive empty-file deprecation
- BundleImportService: null-coalescing for targetFields
- PHPUnit test infra: make test target via container

CLAUDE.md:
- Added Testing section as PR gating criterion
2026-04-22 10:15:22 +02:00

292 lines
11 KiB
PHP

<?php
declare(strict_types=1);
namespace OCA\Mitgliederverwaltung\Controller;
use OCA\Mitgliederverwaltung\Service\EncryptedExportService;
use OCA\Mitgliederverwaltung\Service\InventoryReportService;
use OCA\Mitgliederverwaltung\Service\PdfService;
use OCA\Mitgliederverwaltung\Service\PermissionService;
use OCA\Mitgliederverwaltung\Service\ReportService;
use OCA\Mitgliederverwaltung\Service\ValidationException;
use OCP\AppFramework\ApiController;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\DataDownloadResponse;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;
use OCP\IUserSession;
use Psr\Log\LoggerInterface;
/**
* REST API controller for report generation and preview.
*
* Supports:
* - Preview: returns structured JSON data for frontend table display
* - PDF download: generates and returns a PDF file
* - Encrypted PDF: wraps the PDF in a password-protected ZIP
*
* Part of Issue #46.
*/
class ReportController extends ApiController {
private ReportService $reportService;
private InventoryReportService $inventoryReportService;
private PdfService $pdfService;
private EncryptedExportService $encryptedService;
private PermissionService $permissionService;
private IUserSession $userSession;
private LoggerInterface $logger;
public function __construct(
string $appName,
IRequest $request,
ReportService $reportService,
InventoryReportService $inventoryReportService,
PdfService $pdfService,
EncryptedExportService $encryptedService,
PermissionService $permissionService,
IUserSession $userSession,
LoggerInterface $logger
) {
parent::__construct($appName, $request);
$this->reportService = $reportService;
$this->inventoryReportService = $inventoryReportService;
$this->pdfService = $pdfService;
$this->encryptedService = $encryptedService;
$this->permissionService = $permissionService;
$this->userSession = $userSession;
$this->logger = $logger;
}
/**
* List available report types.
*
* GET /api/v1/reports
*/
#[NoCSRFRequired]
public function index(): JSONResponse {
return new JSONResponse([
'reports' => [
['id' => 'mitgliederliste', 'name' => 'Mitgliederliste', 'params' => ['status']],
['id' => 'beitragsliste', 'name' => 'Beitragsliste', 'params' => ['year']],
['id' => 'stufenliste', 'name' => 'Stufenliste', 'params' => []],
['id' => 'allergieliste', 'name' => 'Allergieliste', 'params' => []],
['id' => 'geburtstagsliste', 'name' => 'Geburtstagsliste', 'params' => ['months']],
['id' => 'kontaktliste', 'name' => 'Kontaktliste', 'params' => ['stufeId']],
['id' => 'bankverbindungen', 'name' => 'Bankverbindungen', 'params' => [], 'sensitive' => true],
['id' => 'familienliste', 'name' => 'Familienliste', 'params' => []],
['id' => 'lagerhistorie', 'name' => 'Lagerhistorie', 'params' => ['memberId']],
['id' => 'verletzungsprotokoll', 'name' => 'Verletzungsprotokoll', 'params' => ['dateFrom', 'dateTo', 'memberId']],
['id' => 'inventur-verkaeufe', 'name' => 'Inventur-Verkaeufe', 'params' => ['dateFrom', 'dateTo']],
['id' => 'materialzustand', 'name' => 'Materialzustand', 'params' => []],
],
]);
}
/**
* Preview report data as JSON (for frontend table display).
*
* GET /api/v1/reports/{type}/preview?...params
*/
#[NoCSRFRequired]
public function preview(string $type): JSONResponse {
try {
$data = $this->generateReportData($type);
if ($data === null) {
return new JSONResponse(
['error' => "Unbekannter Berichtstyp: {$type}"],
Http::STATUS_BAD_REQUEST
);
}
return new JSONResponse($data);
} catch (ValidationException $e) {
return new JSONResponse(
['error' => $e->getMessage()],
Http::STATUS_FORBIDDEN
);
} catch (\Exception $e) {
$this->logger->error('Failed to generate report preview', [
'type' => $type,
'exception' => $e,
'app' => 'mitgliederverwaltung',
]);
return new JSONResponse(
['error' => 'Berichtsvorschau fehlgeschlagen'],
Http::STATUS_INTERNAL_SERVER_ERROR
);
}
}
/**
* Download report as PDF.
*
* GET /api/v1/reports/{type}/pdf?...params
*/
#[NoCSRFRequired]
public function pdf(string $type): DataDownloadResponse|JSONResponse {
try {
$data = $this->generateReportData($type);
if ($data === null) {
return new JSONResponse(
['error' => "Unbekannter Berichtstyp: {$type}"],
Http::STATUS_BAD_REQUEST
);
}
$result = $this->pdfService->generateReport($data);
return new DataDownloadResponse(
$result['content'],
$result['filename'],
'application/pdf'
);
} catch (ValidationException $e) {
return new JSONResponse(
['error' => $e->getMessage()],
Http::STATUS_FORBIDDEN
);
} catch (\Exception $e) {
$this->logger->error('Failed to generate PDF report', [
'type' => $type,
'exception' => $e,
'app' => 'mitgliederverwaltung',
]);
return new JSONResponse(
['error' => 'PDF-Erzeugung fehlgeschlagen'],
Http::STATUS_INTERNAL_SERVER_ERROR
);
}
}
/**
* Download report as encrypted PDF (password-protected ZIP).
*
* POST /api/v1/reports/{type}/encrypted
* Body: { "password": "...", ...params }
*/
public function encrypted(string $type): DataDownloadResponse|JSONResponse {
try {
$password = $this->request->getParam('password');
if ($password === null || $password === '') {
return new JSONResponse(
['error' => 'Passwort ist erforderlich'],
Http::STATUS_BAD_REQUEST
);
}
$data = $this->generateReportData($type);
if ($data === null) {
return new JSONResponse(
['error' => "Unbekannter Berichtstyp: {$type}"],
Http::STATUS_BAD_REQUEST
);
}
$pdfResult = $this->pdfService->generateReport($data);
$zipResult = $this->encryptedService->createEncryptedExport(
$pdfResult['content'],
$pdfResult['filename'],
$password,
$data['title']
);
return new DataDownloadResponse(
$zipResult['content'],
$zipResult['filename'],
'application/zip'
);
} catch (ValidationException $e) {
return new JSONResponse(
['error' => $e->getMessage()],
Http::STATUS_FORBIDDEN
);
} catch (\Exception $e) {
$this->logger->error('Failed to generate encrypted report', [
'type' => $type,
'exception' => $e,
'app' => 'mitgliederverwaltung',
]);
return new JSONResponse(
['error' => 'Verschluesselter Bericht fehlgeschlagen'],
Http::STATUS_INTERNAL_SERVER_ERROR
);
}
}
/**
* Route to the appropriate report generator based on type.
*
* @return array{title: string, headers: string[], rows: array[]}|null
*/
private function generateReportData(string $type): ?array {
return match ($type) {
'mitgliederliste' => $this->reportService->generateMitgliederliste(
$this->request->getParam('status')
),
'beitragsliste' => $this->reportService->generateBeitragsliste(
(int)($this->request->getParam('year') ?? (new \DateTime())->format('Y'))
),
'stufenliste' => $this->reportService->generateStufenliste(),
'allergieliste' => $this->reportService->generateAllergieliste(),
'geburtstagsliste' => $this->reportService->generateGeburtstagsliste(
(int)($this->request->getParam('months') ?? 3)
),
'kontaktliste' => $this->reportService->generateKontaktliste(
$this->request->getParam('stufeId') !== null
? (int)$this->request->getParam('stufeId')
: null
),
'bankverbindungen' => $this->generateBankverbindungenWithPermCheck(),
'familienliste' => $this->reportService->generateFamilienliste(),
'lagerhistorie' => $this->reportService->generateLagerhistorie(
$this->request->getParam('memberId') !== null
? (int)$this->request->getParam('memberId')
: null
),
'verletzungsprotokoll' => $this->reportService->generateVerletzungsprotokoll(
$this->request->getParam('dateFrom') ?: null,
$this->request->getParam('dateTo') ?: null,
$this->request->getParam('memberId') !== null
? (int)$this->request->getParam('memberId')
: null
),
'inventur-verkaeufe' => $this->inventoryReportService->generateSalesReport(
$this->request->getParam('dateFrom') ?: null,
$this->request->getParam('dateTo') ?: null
),
'materialzustand' => $this->inventoryReportService->generateConditionReport(),
default => null,
};
}
/**
* Generate Bankverbindungen report with permission check.
*
* @return array{title: string, headers: string[], rows: array[]}
* @throws ValidationException
*/
private function generateBankverbindungenWithPermCheck(): array {
$userId = $this->getCurrentUserId();
if (!$this->permissionService->canSeeBanking($userId)) {
throw new ValidationException(
'Keine Berechtigung: Bankdaten-Bericht erfordert Bankdaten-Sichtbarkeit.'
);
}
return $this->reportService->generateBankverbindungen();
}
private function getCurrentUserId(): string {
$user = $this->userSession->getUser();
return $user ? $user->getUID() : 'system';
}
}