Beide Module wurden aus einem anderen Kundenprojekt kopiert und bereinigt: Knowledgecenter: - Bild-Cache (1.985 Dateien) geleert, Ordnerstruktur mit .gitkeep behalten - Files-Gallery-Lizenzschlüssel entfernt (config.php) - Kundenspezifische Kategorien gelöscht: Teil I/II/III QM-Struktur, nummerierte QM-Kapitel, Gremien (Präsidium/Finanzausschuss/Landesausschuss), Sitzungsservice-Serie, Protokoll-Abteilungen, Testeinträge - 56 generische Kategorien bleiben (Downloads, Onboarding, Personalthemen …) Tasks: - Alle Betriebsdaten gelöscht (114 Tasks, 914 Submissions, 579 Assignments) - Task-Definitionen und Verlinkungen geleert - Kategoriestruktur bleibt (Allgemein, IT, Personal, Buchhaltung …) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
140 lines
4.6 KiB
PHP
140 lines
4.6 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
class CertificateService
|
|
{
|
|
private TaskCertificateRepository $certificateRepo;
|
|
private TaskAssignmentRepository $assignmentRepo;
|
|
private TaskDefinitionRepository $definitionRepo;
|
|
private CycleCalculatorService $cycleCalculator;
|
|
|
|
public function __construct(
|
|
TaskCertificateRepository $certificateRepo,
|
|
TaskAssignmentRepository $assignmentRepo,
|
|
TaskDefinitionRepository $definitionRepo,
|
|
CycleCalculatorService $cycleCalculator
|
|
) {
|
|
$this->certificateRepo = $certificateRepo;
|
|
$this->assignmentRepo = $assignmentRepo;
|
|
$this->definitionRepo = $definitionRepo;
|
|
$this->cycleCalculator = $cycleCalculator;
|
|
}
|
|
|
|
public function issueForAssignment(int $assignmentId, int $submissionId): int
|
|
{
|
|
$existing = $this->certificateRepo->findByAssignmentId($assignmentId);
|
|
if ($existing !== null) {
|
|
return (int) $existing['id'];
|
|
}
|
|
|
|
$assignment = $this->assignmentRepo->findById($assignmentId);
|
|
if ($assignment === null) {
|
|
throw new RuntimeException('Assignment nicht gefunden');
|
|
}
|
|
|
|
$task = $this->definitionRepo->findById((int) $assignment['task_id']);
|
|
if ($task === null) {
|
|
throw new RuntimeException('Aufgabe nicht gefunden');
|
|
}
|
|
|
|
if ((int) $task['certificate_enabled'] !== 1) {
|
|
return 0;
|
|
}
|
|
|
|
$validFrom = new DateTimeImmutable();
|
|
$validUntil = null;
|
|
|
|
$validityMode = $this->resolveValidityMode($task);
|
|
if ($validityMode === 'days') {
|
|
$validityDays = (int) ($task['certificate_validity_days'] ?? 0);
|
|
if ($validityDays > 0) {
|
|
$validUntil = $validFrom->modify('+' . $validityDays . ' days')->format('Y-m-d H:i:s');
|
|
}
|
|
} elseif ($validityMode === 'cycle') {
|
|
$validUntil = $this->resolveCycleValidUntil($task, $assignment, $validFrom);
|
|
}
|
|
|
|
return $this->certificateRepo->create([
|
|
'task_id' => (int) $assignment['task_id'],
|
|
'assignment_id' => $assignmentId,
|
|
'submission_id' => $submissionId,
|
|
'main_contact_id' => (int) $assignment['main_contact_id'],
|
|
'certificate_code' => $this->buildCertificateCode(),
|
|
'valid_from' => $validFrom->format('Y-m-d H:i:s'),
|
|
'valid_until' => $validUntil,
|
|
'status' => 'valid',
|
|
]);
|
|
}
|
|
|
|
public function listUserCertificates(int $mainContactId): array
|
|
{
|
|
return $this->certificateRepo->listForUser($mainContactId);
|
|
}
|
|
|
|
public function listUserTaskCertificates(int $mainContactId, int $taskId): array
|
|
{
|
|
if ($taskId <= 0) {
|
|
return [];
|
|
}
|
|
|
|
return $this->certificateRepo->listForUserTask($mainContactId, $taskId);
|
|
}
|
|
|
|
public function getUserTaskCertificateSummary(int $mainContactId, array $taskIds): array
|
|
{
|
|
return $this->certificateRepo->summarizeForUserTasks($mainContactId, $taskIds);
|
|
}
|
|
|
|
public function refreshExpiredCertificates(): int
|
|
{
|
|
return $this->certificateRepo->markExpiredByDate((new DateTimeImmutable())->format('Y-m-d H:i:s'));
|
|
}
|
|
|
|
private function buildCertificateCode(): string
|
|
{
|
|
return strtoupper(bin2hex(random_bytes(8)));
|
|
}
|
|
|
|
private function resolveCycleValidUntil(array $task, array $assignment, DateTimeImmutable $reference): ?string
|
|
{
|
|
$assignmentCycleEnd = trim((string) ($assignment['cycle_end_at'] ?? ''));
|
|
if ($assignmentCycleEnd !== '') {
|
|
try {
|
|
return (new DateTimeImmutable($assignmentCycleEnd))->format('Y-m-d H:i:s');
|
|
} catch (Throwable $throwable) {
|
|
// fallback to calculated cycle
|
|
}
|
|
}
|
|
|
|
if ((string) ($task['frequency_type'] ?? 'once') !== 'recurring') {
|
|
return null;
|
|
}
|
|
|
|
$cycle = $this->cycleCalculator->resolveCurrentCycle($task, $reference);
|
|
if ($cycle === [] || !($cycle['cycle_end_at'] ?? null) instanceof DateTimeImmutable) {
|
|
return null;
|
|
}
|
|
|
|
return $cycle['cycle_end_at']->format('Y-m-d H:i:s');
|
|
}
|
|
|
|
private function resolveValidityMode(array $task): string
|
|
{
|
|
$mode = (string) ($task['certificate_validity_mode'] ?? '');
|
|
if (in_array($mode, ['cycle', 'days', 'permanent'], true)) {
|
|
return $mode;
|
|
}
|
|
|
|
$days = (int) ($task['certificate_validity_days'] ?? 0);
|
|
if ($days > 0) {
|
|
return 'days';
|
|
}
|
|
|
|
if ((string) ($task['frequency_type'] ?? 'once') === 'recurring') {
|
|
return 'cycle';
|
|
}
|
|
|
|
return 'permanent';
|
|
}
|
|
}
|