Schema snapshot on existing handovers may predate the field type change, so the conditional check was false and tenantUsers was empty. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
293 lines
11 KiB
PHP
293 lines
11 KiB
PHP
<?php
|
|
|
|
use MintyPHP\Buffer;
|
|
use MintyPHP\DB;
|
|
use MintyPHP\Http\SessionStoreInterface;
|
|
use MintyPHP\Module\Helpdesk\HelpdeskAuthorizationPolicy;
|
|
use MintyPHP\Module\Helpdesk\Service\HandoverRevisionService;
|
|
use MintyPHP\Module\Helpdesk\Service\HandoverService;
|
|
use MintyPHP\Router;
|
|
use MintyPHP\Session;
|
|
use MintyPHP\Support\Flash;
|
|
use MintyPHP\Support\Guard;
|
|
|
|
Guard::requireLogin();
|
|
Guard::requireAbilityOrForbidden(HelpdeskAuthorizationPolicy::ABILITY_HANDOVERS_VIEW);
|
|
|
|
$request = requestInput();
|
|
$returnTarget = requestResolveReturnTarget();
|
|
$closeTarget = requestResolveReturnTarget('helpdesk/handovers');
|
|
$handoverId = (int) ($id ?? 0);
|
|
$editBasePath = 'helpdesk/handovers/edit/' . $handoverId;
|
|
$editTarget = requestPathWithReturnTarget($editBasePath, $returnTarget);
|
|
|
|
$session = app(SessionStoreInterface::class)->all();
|
|
$tenantId = (int) ($session['current_tenant']['id'] ?? 0);
|
|
$userId = (int) ($session['user']['id'] ?? 0);
|
|
|
|
$service = app(HandoverService::class);
|
|
$revisionService = app(HandoverRevisionService::class);
|
|
$handover = $service->findById($tenantId, $handoverId);
|
|
|
|
if ($handover === null) {
|
|
Flash::error(t('Handover not found'), $closeTarget, 'handover_not_found');
|
|
Router::redirect($closeTarget);
|
|
|
|
return;
|
|
}
|
|
|
|
// Determine permission level
|
|
$authService = app(\MintyPHP\Service\Access\AuthorizationService::class);
|
|
$actorContext = ['actor_user_id' => $userId];
|
|
$canManage = $authService->authorize(HelpdeskAuthorizationPolicy::ABILITY_HANDOVERS_MANAGE, $actorContext)->isAllowed();
|
|
$canCreate = $authService->authorize(HelpdeskAuthorizationPolicy::ABILITY_HANDOVERS_CREATE, $actorContext)->isAllowed();
|
|
$permissionLevel = $canManage ? HandoverService::PERMISSION_MANAGE : ($canCreate ? HandoverService::PERMISSION_CREATE : '');
|
|
|
|
$currentStatus = (string) ($handover['status'] ?? 'draft');
|
|
$canEdit = $service->canEdit($currentStatus, $permissionLevel);
|
|
$allowedStatuses = $service->getAllowedStatuses($permissionLevel);
|
|
|
|
// Parse schema snapshot
|
|
$schemaRaw = (string) ($handover['schema_snapshot'] ?? '{}');
|
|
$schema = json_decode($schemaRaw, true);
|
|
$schemaFields = is_array($schema) ? ($schema['fields'] ?? []) : [];
|
|
$schemaVersion = is_array($schema) ? (int) ($schema['version'] ?? 1) : 1;
|
|
|
|
// Parse field values
|
|
$fieldValuesRaw = (string) ($handover['field_values'] ?? '{}');
|
|
$fieldValues = json_decode($fieldValuesRaw, true);
|
|
if (!is_array($fieldValues)) {
|
|
$fieldValues = [];
|
|
}
|
|
|
|
$errorBag = formErrors();
|
|
|
|
// Handle delete POST
|
|
if ($request->isMethod('POST') && (string) $request->body('action', '') === 'delete') {
|
|
if (!Session::checkCsrfToken()) {
|
|
Flash::error(t('Form expired, please try again'), $editTarget, 'csrf_expired');
|
|
Router::redirect($editTarget);
|
|
return;
|
|
}
|
|
|
|
if (!$canManage) {
|
|
Flash::error(t('Only managers can delete handovers'), $closeTarget, 'delete_denied');
|
|
Router::redirect($closeTarget);
|
|
return;
|
|
}
|
|
|
|
$deleteResult = $service->deleteByIds($tenantId, [$handoverId], HandoverService::PERMISSION_MANAGE);
|
|
if ($deleteResult['ok']) {
|
|
Flash::success(t('Handover deleted'), null, 'handover_deleted');
|
|
} else {
|
|
Flash::error($deleteResult['error'] ?? t('Failed to delete handovers'), null, 'delete_failed');
|
|
}
|
|
Router::redirect($closeTarget);
|
|
return;
|
|
}
|
|
|
|
// Handle submit-for-review POST
|
|
if ($request->isMethod('POST') && (string) $request->body('action', '') === 'submit_for_review') {
|
|
if (!Session::checkCsrfToken()) {
|
|
Flash::error(t('Form expired, please try again'), $editTarget, 'csrf_expired');
|
|
Router::redirect($editTarget);
|
|
return;
|
|
}
|
|
|
|
$reviewResult = $service->submitForReview($tenantId, $handoverId, $userId, $permissionLevel);
|
|
if ($reviewResult['ok']) {
|
|
Flash::success(t('Handover submitted for review'), $editTarget, 'handover_submitted');
|
|
} else {
|
|
$firstError = reset($reviewResult['errors']) ?: t('Failed to submit for review');
|
|
Flash::error($firstError, $editTarget, 'submit_failed');
|
|
}
|
|
Router::redirect($editTarget);
|
|
return;
|
|
}
|
|
|
|
// Handle restore POST
|
|
if ($request->isMethod('POST') && (string) $request->body('action', '') === 'restore') {
|
|
if (!Session::checkCsrfToken()) {
|
|
Flash::error(t('Form expired, please try again'), $editTarget, 'csrf_expired');
|
|
Router::redirect($editTarget);
|
|
|
|
return;
|
|
}
|
|
|
|
$restoreRevision = (int) $request->body('restore_revision', 0);
|
|
$restoreResult = $revisionService->restoreRevision($tenantId, $handoverId, $restoreRevision, $userId, $permissionLevel);
|
|
|
|
if ($restoreResult['ok']) {
|
|
Flash::success(t('Version restored'), $editTarget, 'revision_restored');
|
|
} else {
|
|
$firstError = reset($restoreResult['errors']) ?: t('Failed to restore');
|
|
Flash::error($firstError, $editTarget, 'revision_restore_failed');
|
|
}
|
|
|
|
Router::redirect($editTarget);
|
|
|
|
return;
|
|
}
|
|
|
|
// Handle normal save POST
|
|
if ($request->isMethod('POST') && $canEdit) {
|
|
if (!Session::checkCsrfToken()) {
|
|
Flash::error(t('Form expired, please try again'), $editTarget, 'csrf_expired');
|
|
Router::redirect($editTarget);
|
|
|
|
return;
|
|
}
|
|
|
|
// Collect field values
|
|
$submittedValues = [];
|
|
foreach ($schemaFields as $field) {
|
|
$key = $field['key'] ?? null;
|
|
if ($key === null) {
|
|
continue;
|
|
}
|
|
$type = $field['type'] ?? 'text';
|
|
if ($type === 'checkbox') {
|
|
$submittedValues[$key] = $request->body('field_' . $key, '') !== '' ? '1' : '0';
|
|
} elseif ($type === 'user-select') {
|
|
$ids = $request->body('field_' . $key, []);
|
|
$submittedValues[$key] = json_encode(array_values(array_filter((array) $ids)));
|
|
} else {
|
|
$submittedValues[$key] = (string) $request->body('field_' . $key, '');
|
|
}
|
|
}
|
|
|
|
$fieldsChanged = $revisionService->computeDiff($fieldValues, $submittedValues) !== [];
|
|
$updateResult = $service->updateFields($tenantId, $handoverId, $submittedValues, $userId, $permissionLevel);
|
|
$errorBag->merge($updateResult['errors'] ?? []);
|
|
|
|
// Handle status change
|
|
$newStatus = (string) $request->body('status', '');
|
|
$statusChanged = false;
|
|
if ($newStatus !== '' && $newStatus !== $currentStatus) {
|
|
$statusResult = $service->changeStatus($tenantId, $handoverId, $newStatus, $userId, $permissionLevel);
|
|
$errorBag->merge($statusResult['errors'] ?? []);
|
|
if (empty($statusResult['errors'])) {
|
|
$statusChanged = true;
|
|
}
|
|
}
|
|
|
|
if (!$errorBag->hasAny()) {
|
|
// Create a single revision for the combined save
|
|
$finalStatus = $statusChanged ? $newStatus : $currentStatus;
|
|
$service->createRevisionAfterSave(
|
|
$tenantId,
|
|
$handoverId,
|
|
$submittedValues,
|
|
$finalStatus,
|
|
$fieldsChanged,
|
|
$statusChanged,
|
|
$schemaVersion,
|
|
$userId
|
|
);
|
|
|
|
$handover = $service->findById($tenantId, $handoverId);
|
|
$fieldValues = $submittedValues;
|
|
$currentStatus = (string) ($handover['status'] ?? $currentStatus);
|
|
|
|
$action = (string) $request->body('action', 'save');
|
|
if ($action === 'save_close') {
|
|
Flash::success(t('Handover updated'), $closeTarget, 'handover_updated');
|
|
Router::redirect($closeTarget);
|
|
} else {
|
|
Flash::success(t('Handover updated'), $editTarget, 'handover_updated');
|
|
Router::redirect($editTarget);
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
$validationSummaryErrors = $errorBag->toArray();
|
|
$errors = $errorBag->toFlatList();
|
|
|
|
// Load tenant users — always available for user-select schema fields
|
|
$rows = DB::select(
|
|
'SELECT u.id, u.display_name, u.email'
|
|
. ' FROM users u'
|
|
. ' JOIN user_tenants ut ON ut.user_id = u.id'
|
|
. ' WHERE ut.tenant_id = ? AND u.active = 1'
|
|
. ' ORDER BY u.display_name ASC',
|
|
(string) $tenantId
|
|
);
|
|
$tenantUsers = is_array($rows) ? $rows : [];
|
|
|
|
// Load revision data
|
|
$revisions = $revisionService->listByHandover($tenantId, $handoverId);
|
|
$activeRevisionNumber = (int) $request->query('revision', 0);
|
|
$compareRevisionNumber = (int) $request->query('compare', 0);
|
|
$isViewingRevision = $activeRevisionNumber > 0;
|
|
$activeRevision = null;
|
|
$fieldDiff = [];
|
|
|
|
if ($isViewingRevision) {
|
|
$activeRevision = $revisionService->findRevision($tenantId, $handoverId, $activeRevisionNumber);
|
|
if ($activeRevision === null) {
|
|
// Invalid revision number — fall back to current
|
|
$isViewingRevision = false;
|
|
$activeRevisionNumber = 0;
|
|
} else {
|
|
// Load this revision's field values
|
|
$revisionFieldValues = json_decode((string) ($activeRevision['field_values'] ?? '{}'), true);
|
|
if (!is_array($revisionFieldValues)) {
|
|
$revisionFieldValues = [];
|
|
}
|
|
|
|
// Compute diff
|
|
if ($compareRevisionNumber > 0) {
|
|
// Compare against a specific revision
|
|
$compareRevision = $revisionService->findRevision($tenantId, $handoverId, $compareRevisionNumber);
|
|
if ($compareRevision !== null) {
|
|
$compareValues = json_decode((string) ($compareRevision['field_values'] ?? '{}'), true);
|
|
if (!is_array($compareValues)) {
|
|
$compareValues = [];
|
|
}
|
|
$fieldDiff = $revisionService->computeDiff($compareValues, $revisionFieldValues);
|
|
}
|
|
} elseif ($activeRevisionNumber > 1) {
|
|
// Compare against previous revision
|
|
$prevRevision = $revisionService->findRevision($tenantId, $handoverId, $activeRevisionNumber - 1);
|
|
if ($prevRevision !== null) {
|
|
$prevValues = json_decode((string) ($prevRevision['field_values'] ?? '{}'), true);
|
|
if (!is_array($prevValues)) {
|
|
$prevValues = [];
|
|
}
|
|
$fieldDiff = $revisionService->computeDiff($prevValues, $revisionFieldValues);
|
|
}
|
|
}
|
|
|
|
$fieldValues = $revisionFieldValues;
|
|
$currentStatus = (string) ($activeRevision['status'] ?? $currentStatus);
|
|
}
|
|
}
|
|
|
|
// Can the current user submit for review?
|
|
// Assignee (create-level) or manager may submit when status is draft/in_progress.
|
|
$assignedTo = (int) ($handover['assigned_to'] ?? 0);
|
|
$isAssignee = $assignedTo === $userId;
|
|
$canSubmitForReview = !$isViewingRevision
|
|
&& ($isAssignee || $canManage)
|
|
&& in_array($currentStatus, [HandoverService::STATUS_DRAFT, HandoverService::STATUS_IN_PROGRESS], true);
|
|
|
|
$assignUrl = $canManage ? lurl('helpdesk/handovers/assign/' . $handoverId) : '';
|
|
|
|
$productCode = (string) ($handover['product_code'] ?? '');
|
|
$product = app(\MintyPHP\Module\Helpdesk\Service\SoftwareProductService::class)->findByCode($productCode);
|
|
$productName = trim((string) ($product['name'] ?? '')) !== ''
|
|
? $product['name']
|
|
: (trim((string) ($product['bc_description'] ?? '')) !== '' ? $product['bc_description'] : $productCode);
|
|
$canViewSoftwareProducts = $authService->authorize(HelpdeskAuthorizationPolicy::ABILITY_SOFTWARE_PRODUCTS_MANAGE, $actorContext)->isAllowed();
|
|
$titleText = t('Handover') . ' ' . $productName;
|
|
Buffer::set('title', $titleText);
|
|
Buffer::set('style_groups', json_encode(['helpdesk']));
|
|
$breadcrumbs = [
|
|
['label' => t('Home'), 'path' => 'admin'],
|
|
['label' => t('Helpdesk'), 'path' => 'helpdesk'],
|
|
['label' => t('Handovers'), 'path' => 'helpdesk/handovers'],
|
|
['label' => $titleText],
|
|
];
|