Files
breadcrumb-the-shire/modules/helpdesk/pages/helpdesk/handovers/edit($id).php
aminovfariz e3dfdef76f fix(helpdesk): always load tenant users, not only when schema has user-select
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>
2026-06-08 11:57:02 +02:00

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],
];