1
0
Files
breadcrumb-the-shire/modules/helpdesk/lib/Module/Helpdesk/Service/HandoverRevisionService.php
fs e7c60468c9 feat(helpdesk): add domain linking, bulk actions, and revision polish to handovers
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>
2026-04-16 13:21:12 +02:00

199 lines
7.0 KiB
PHP

<?php
namespace MintyPHP\Module\Helpdesk\Service;
use MintyPHP\Module\Helpdesk\Repository\HandoverRepository;
use MintyPHP\Module\Helpdesk\Repository\HandoverRevisionRepository;
/** @api Called from pages/helpdesk/handovers action files */
class HandoverRevisionService
{
public const CHANGE_TYPE_INITIAL = 'initial';
public const CHANGE_TYPE_FIELDS = 'fields';
public const CHANGE_TYPE_STATUS = 'status';
public const CHANGE_TYPE_BOTH = 'both';
public const CHANGE_TYPE_RESTORE = 'restore';
public function __construct(
private readonly HandoverRevisionRepository $revisionRepository,
private readonly HandoverRepository $handoverRepository,
) {
}
/**
* Create a new revision snapshot.
*
* @param array<string, mixed> $fieldValues
*/
public function createRevision(
int $tenantId,
int $handoverId,
array $fieldValues,
string $status,
int $schemaVersion,
int $userId,
string $changeType = self::CHANGE_TYPE_FIELDS,
): ?int {
$nextRevision = $this->revisionRepository->getLatestRevisionNumber($tenantId, $handoverId) + 1;
$fieldValuesJson = json_encode($fieldValues, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
return $this->revisionRepository->insert([
'tenant_id' => $tenantId,
'handover_id' => $handoverId,
'revision' => $nextRevision,
'field_values' => $fieldValuesJson,
'status' => $status,
'schema_version' => $schemaVersion,
'change_type' => $changeType,
'changed_by' => $userId,
]);
}
/**
* List all revisions for a handover, newest first.
*
* @return list<array<string, mixed>>
*/
public function listByHandover(int $tenantId, int $handoverId): array
{
return $this->revisionRepository->listByHandover($tenantId, $handoverId);
}
/**
* Find a specific revision.
*
* @return array<string, mixed>|null
*/
public function findRevision(int $tenantId, int $handoverId, int $revision): ?array
{
return $this->revisionRepository->findByRevision($tenantId, $handoverId, $revision);
}
/**
* Compute the diff between two sets of field values.
*
* @param array<string, mixed> $oldValues
* @param array<string, mixed> $newValues
* @return array<string, array{type: string, old: mixed, new: mixed}>
*/
public function computeDiff(array $oldValues, array $newValues): array
{
$diff = [];
$allKeys = array_unique(array_merge(array_keys($oldValues), array_keys($newValues)));
foreach ($allKeys as $key) {
$oldVal = $oldValues[$key] ?? null;
$newVal = $newValues[$key] ?? null;
if ($oldVal === null && $newVal !== null) {
$diff[$key] = ['type' => 'added', 'old' => null, 'new' => $newVal];
} elseif ($oldVal !== null && $newVal === null) {
$diff[$key] = ['type' => 'removed', 'old' => $oldVal, 'new' => null];
} elseif ((string) $oldVal !== (string) $newVal) {
$diff[$key] = ['type' => 'changed', 'old' => $oldVal, 'new' => $newVal];
}
}
return $diff;
}
/**
* Restore a handover to a previous revision's values.
* Creates a new revision with the restored values.
*
* @return array{ok: bool, errors: array<string, string>}
*/
public function restoreRevision(
int $tenantId,
int $handoverId,
int $revisionNumber,
int $userId,
string $permissionLevel,
): array {
if ($permissionLevel !== HandoverService::PERMISSION_MANAGE) {
return ['ok' => false, 'errors' => ['general' => t('Only managers can restore revisions')]];
}
$revision = $this->revisionRepository->findByRevision($tenantId, $handoverId, $revisionNumber);
if ($revision === null) {
return ['ok' => false, 'errors' => ['general' => t('Revision not found')]];
}
$handover = $this->handoverRepository->findById($tenantId, $handoverId);
if ($handover === null) {
return ['ok' => false, 'errors' => ['general' => t('Handover not found')]];
}
$restoredValues = json_decode((string) ($revision['field_values'] ?? '{}'), true);
if (!is_array($restoredValues)) {
$restoredValues = [];
}
$restoredStatus = (string) ($revision['status'] ?? $handover['status']);
$this->handoverRepository->beginTransaction();
try {
// Update the handover with restored values.
$fieldValuesJson = json_encode($restoredValues, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$updatedFields = $this->handoverRepository->updateFieldValues($tenantId, $handoverId, $fieldValuesJson, $userId);
if (!$updatedFields) {
throw new \RuntimeException('Failed to restore handover field values');
}
$currentStatus = (string) ($handover['status'] ?? '');
if ($restoredStatus !== $currentStatus) {
$updatedStatus = $this->handoverRepository->updateStatus($tenantId, $handoverId, $restoredStatus, $userId);
if (!$updatedStatus) {
throw new \RuntimeException('Failed to restore handover status');
}
}
// Determine schema version from current handover.
$schema = json_decode((string) ($handover['schema_snapshot'] ?? '{}'), true);
$schemaVersion = is_array($schema) ? (int) ($schema['version'] ?? 1) : 1;
// Create a new revision marking the restore.
$revisionId = $this->createRevision(
$tenantId,
$handoverId,
$restoredValues,
$restoredStatus,
$schemaVersion,
$userId,
self::CHANGE_TYPE_RESTORE
);
if ($revisionId === null) {
throw new \RuntimeException('Failed to create restore revision');
}
$this->handoverRepository->commitTransaction();
} catch (\Throwable) {
try {
$this->handoverRepository->rollbackTransaction();
} catch (\Throwable) {
// no-op
}
return ['ok' => false, 'errors' => ['general' => t('Failed to restore')]];
}
return ['ok' => true, 'errors' => []];
}
/**
* Get a human-readable label for a change type.
*/
public static function changeTypeLabel(string $changeType): string
{
return match ($changeType) {
self::CHANGE_TYPE_INITIAL => t('Created'),
self::CHANGE_TYPE_FIELDS => t('Fields changed'),
self::CHANGE_TYPE_STATUS => t('Status changed'),
self::CHANGE_TYPE_BOTH => t('Fields and status changed'),
self::CHANGE_TYPE_RESTORE => t('Restored'),
default => $changeType,
};
}
}