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>
This commit is contained in:
@@ -132,30 +132,52 @@ class HandoverRevisionService
|
||||
|
||||
$restoredStatus = (string) ($revision['status'] ?? $handover['status']);
|
||||
|
||||
// Update the handover with restored values
|
||||
$fieldValuesJson = json_encode($restoredValues, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
$this->handoverRepository->updateFieldValues($tenantId, $handoverId, $fieldValuesJson, $userId);
|
||||
$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) {
|
||||
$this->handoverRepository->updateStatus($tenantId, $handoverId, $restoredStatus, $userId);
|
||||
$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')]];
|
||||
}
|
||||
|
||||
// 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
|
||||
$this->createRevision(
|
||||
$tenantId,
|
||||
$handoverId,
|
||||
$restoredValues,
|
||||
$restoredStatus,
|
||||
$schemaVersion,
|
||||
$userId,
|
||||
self::CHANGE_TYPE_RESTORE
|
||||
);
|
||||
|
||||
return ['ok' => true, 'errors' => []];
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,8 @@ class HandoverService
|
||||
string $debitorNo,
|
||||
string $debitorName,
|
||||
string $productCode,
|
||||
string $domainNo,
|
||||
string $domainUrl,
|
||||
int $userId,
|
||||
array $fieldValues = [],
|
||||
): array {
|
||||
@@ -77,6 +79,11 @@ class HandoverService
|
||||
$errors['product_code'] = t('Software product is required');
|
||||
}
|
||||
|
||||
$domainNo = trim($domainNo);
|
||||
if ($domainNo === '') {
|
||||
$errors['domain_no'] = t('Please select a domain');
|
||||
}
|
||||
|
||||
if ($errors !== []) {
|
||||
return ['ok' => false, 'id' => null, 'errors' => $errors];
|
||||
}
|
||||
@@ -113,6 +120,8 @@ class HandoverService
|
||||
'debitor_no' => $debitorNo,
|
||||
'debitor_name' => trim($debitorName),
|
||||
'product_code' => $productCode,
|
||||
'domain_no' => $domainNo,
|
||||
'domain_url' => trim($domainUrl),
|
||||
'status' => self::STATUS_DRAFT,
|
||||
'schema_snapshot' => $schemaRaw,
|
||||
'field_values' => $fieldValuesJson,
|
||||
@@ -222,7 +231,8 @@ class HandoverService
|
||||
int $handoverId,
|
||||
array $fieldValues,
|
||||
string $status,
|
||||
string $previousStatus,
|
||||
bool $fieldsChanged,
|
||||
bool $statusChanged,
|
||||
int $schemaVersion,
|
||||
int $userId,
|
||||
): void {
|
||||
@@ -230,10 +240,12 @@ class HandoverService
|
||||
return;
|
||||
}
|
||||
|
||||
$statusChanged = $status !== $previousStatus;
|
||||
$changeType = $statusChanged
|
||||
? HandoverRevisionService::CHANGE_TYPE_BOTH
|
||||
: HandoverRevisionService::CHANGE_TYPE_FIELDS;
|
||||
$changeType = HandoverRevisionService::CHANGE_TYPE_FIELDS;
|
||||
if ($fieldsChanged && $statusChanged) {
|
||||
$changeType = HandoverRevisionService::CHANGE_TYPE_BOTH;
|
||||
} elseif ($statusChanged) {
|
||||
$changeType = HandoverRevisionService::CHANGE_TYPE_STATUS;
|
||||
}
|
||||
|
||||
$this->revisionService->createRevision(
|
||||
$tenantId,
|
||||
@@ -246,41 +258,6 @@ class HandoverService
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a revision for a status-only change.
|
||||
*/
|
||||
public function createRevisionForStatusChange(
|
||||
int $tenantId,
|
||||
int $handoverId,
|
||||
string $newStatus,
|
||||
int $schemaVersion,
|
||||
int $userId,
|
||||
): void {
|
||||
if ($this->revisionService === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$handover = $this->repository->findById($tenantId, $handoverId);
|
||||
if ($handover === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fieldValues = json_decode((string) ($handover['field_values'] ?? '{}'), true);
|
||||
if (!is_array($fieldValues)) {
|
||||
$fieldValues = [];
|
||||
}
|
||||
|
||||
$this->revisionService->createRevision(
|
||||
$tenantId,
|
||||
$handoverId,
|
||||
$fieldValues,
|
||||
$newStatus,
|
||||
$schemaVersion,
|
||||
$userId,
|
||||
HandoverRevisionService::CHANGE_TYPE_STATUS
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{total: int, rows: list<array<string, mixed>>}
|
||||
*/
|
||||
@@ -294,6 +271,25 @@ class HandoverService
|
||||
return $this->repository->findById($tenantId, $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $ids
|
||||
* @return array{ok: bool, count: int, error?: string}
|
||||
*/
|
||||
public function deleteByIds(int $tenantId, array $ids, string $permissionLevel): array
|
||||
{
|
||||
if ($permissionLevel !== self::PERMISSION_MANAGE) {
|
||||
return ['ok' => false, 'count' => 0, 'error' => t('Only managers can delete handovers')];
|
||||
}
|
||||
|
||||
if ($ids === []) {
|
||||
return ['ok' => false, 'count' => 0, 'error' => t('No handovers selected')];
|
||||
}
|
||||
|
||||
$count = $this->repository->deleteByIds($tenantId, $ids);
|
||||
|
||||
return ['ok' => true, 'count' => $count];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user with the given permission level can edit a handover in the given status.
|
||||
*/
|
||||
|
||||
@@ -44,6 +44,15 @@ class SoftwareProductSyncService
|
||||
];
|
||||
}
|
||||
|
||||
if ($bcTypes === []) {
|
||||
return [
|
||||
'status' => 'skipped',
|
||||
'error_code' => 'bc_empty_response',
|
||||
'error_message' => 'BC returned no contract types — skipping sync to preserve local state',
|
||||
'result' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$syncedCodes = [];
|
||||
$upserted = 0;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user