Files
thetool/application/WorkorderBase/WorkorderBaseController.php
2025-12-17 13:44:23 +01:00

277 lines
13 KiB
PHP

<?php
// WorkorderBaseController.php
class WorkorderBaseController extends TTCrud
{
/**
* @var array Shared status column definition for consistency.
*/
protected array $statusColumn = [
'key' => 'status', 'text' => 'Status', 'modal' => false, 'table' => ['filter' => 'iconSelect', 'filterOptions' => [
['value' => 'new', 'text' => 'Neu', 'icon' => 'fas fa-star text-primary'],
['value' => 'assigned', 'text' => 'Zugewiesen', 'icon' => 'fas fa-user-check text-info'],
['value' => 'scheduled', 'text' => 'Geplant', 'icon' => 'fas fa-calendar-check text-warning'],
['value' => 'in_progress', 'text' => 'In Bearbeitung', 'icon' => 'fas fa-cog text-warning'],
['value' => 'correction_requested', 'text' => 'Korrektur angefordert', 'icon' => 'fas fa-exclamation-triangle text-danger'],
['value' => 'intervention_required', 'text' => 'Eingriff erforderlich', 'icon' => 'fas fa-times-circle text-danger'],
['value' => 'civil_engineering_required', 'text' => 'Tiefbau benötigt', 'icon' => 'fas fa-hard-hat text-orange'],
['value' => 'civil_engineering_completed', 'text' => 'Tiefbau abgeschlossen', 'icon' => 'fas fa-hard-hat text-success'],
['value' => 'problem_solved', 'text' => 'Problem gelöst', 'icon' => 'fas fa-check-circle text-success'],
['value' => 'documented', 'text' => 'Dokumentiert', 'icon' => 'fas fa-file-alt text-success'],
['value' => 'completed', 'text' => 'Abgeschlossen', 'icon' => 'fas fa-check-double text-secondary'],
['value' => 'charged', 'text' => 'Verrechnet', 'icon' => 'fas fa-euro-sign text-purple'],
['value' => 'cancelled', 'text' => 'Abgebrochen', 'icon' => 'fas fa-ban text-danger'],
['value' => 'archived', 'text' => 'Archiviert', 'icon' => 'fas fa-archive text-muted'],
]]
];
protected array $additionalJS = ["js/pages/WorkorderBase/WorkorderBase.js"];
protected array $additionalHead = ["<link rel='stylesheet' href='/js/pages/WorkorderBase/WorkorderBase.css'>"];
/**
* Gets the display text for a given status key.
* @param string $statusKey
* @return string
*/
protected function getStatusText(string $statusKey): string
{
$statusMap = array_column($this->statusColumn['table']['filterOptions'] ?? [], 'text', 'value');
return $statusMap[$statusKey] ?? ucfirst(str_replace('_', ' ', $statusKey));
}
//region SHARED ACTIONS
/**
* Fetches documentation and journal entries for a given workorder.
*/
protected function getDocumentationAction()
{
if (empty($this->request->workorderId)) self::sendError("Arbeitsauftrags-ID fehlt.");
$docs = WorkorderDocumentationModel::getAll(['workorderId' => intval($this->request->workorderId)], null, 0, ['key' => 'create', 'order' => 'ASC']);
$journals = WorkorderJournalModel::getAll(['workorderId' => intval($this->request->workorderId)], null, 0, ['key' => 'create', 'order' => 'DESC']);
$tenantConfig = $this->getTenantConfigFromWorkorder((int)$this->request->workorderId);
$translationMap = [];
if ($tenantConfig && !empty($tenantConfig->documentationTypes)) {
$customTypes = json_decode($tenantConfig->documentationTypes, true);
$customMap = array_column($customTypes, 'text', 'value');
$translationMap = array_merge(['civil_engineering_photo' => 'Tiefbau_Foto'], $customMap);
}
$responseDocs = [];
$typeCounts = [];
foreach ($docs as $doc) {
$file = new File($doc->fileId);
$documentTypeKey = $doc->documentType;
$typeCounts[$documentTypeKey] = ($typeCounts[$documentTypeKey] ?? 0) + 1;
$originalFilename = $file->orig_filename ?? $file->filename;
$extension = pathinfo($originalFilename, PATHINFO_EXTENSION);
$translatedType = $translationMap[$documentTypeKey] ?? $documentTypeKey;
$newFilename = "{$translatedType}_{$typeCounts[$documentTypeKey]}." . strtolower($extension);
$responseDocs[] = [
'id' => $doc->id, 'fileId' => $doc->fileId, 'fileName' => $newFilename, 'description' => $doc->description,
'documentType' => $documentTypeKey, 'userName' => UserModel::getOne($doc->createBy)->name ?? 'Unbekannt',
'mimetype' => $file->mimetype ?? 'application/octet-stream', 'create' => $doc->create
];
}
foreach ($journals as $journal) {
$journal->createByName = UserModel::getOne($journal->createBy)->name ?? 'Unbekannt';
}
self::returnJson(['docs' => $responseDocs, 'journals' => $journals]);
}
/**
* Adds a new entry to a workorder's journal.
*/
protected function addJournalAction()
{
$post = json_decode(file_get_contents('php://input'), true);
if (empty($post['workorderId']) || empty(trim($post['text']))) self::sendError("Arbeitsauftrags-ID und Text sind erforderlich.");
WorkorderJournalModel::create(['workorderId' => $post['workorderId'], 'text' => $post['text'], 'createBy' => $this->user->id, 'create' => time()]);
$journals = WorkorderJournalModel::getAll(['workorderId' => intval($post['workorderId'])], null, 0, ['key' => 'create', 'order' => 'DESC']);
foreach ($journals as $journal) {
$journal->createByName = UserModel::getOne($journal->createBy)->name ?? 'Unbekannt';
}
self::returnJson(['success' => true, 'message' => 'Journaleintrag hinzugefügt.', 'journals' => $journals]);
}
/**
* Updates the additional info field for a workorder.
*/
protected function updateAdditionalInfoAction()
{
$post = json_decode(file_get_contents('php://input'), true);
if (empty($post['workorderId'])) self::sendError("Arbeitsauftrags-ID fehlt.");
$workorder = WorkorderModel::get($post['workorderId']);
if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden.");
$oldInfo = $workorder->additionalInfo;
$newInfo = $post['additionalInfo'] ?? null;
$workorder->additionalInfo = $newInfo;
WorkorderModel::update((array)$workorder);
WorkorderJournalModel::create([
'workorderId' => $workorder->id, 'text' => "Zusatzinfo geändert.\nAlt: '{$oldInfo}'\nNeu: '{$newInfo}'",
'create' => time(), 'createBy' => $this->user->id,
]);
self::returnJson(['success' => true, 'message' => 'Zusatzinfo aktualisiert.', 'newInfo' => $newInfo]);
}
//endregion
protected function getTenantConfigFromWorkorder(int $workorderId) {
if (empty($workorderId)) self::sendError("Arbeitsauftrags-ID fehlt.");
$workorder = WorkorderModel::get($workorderId);
if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden.");
$preorder = new Preorder($workorder->preorderId);
if (!$preorder->id) self::sendError("Vorbestellung nicht gefunden.");
$campaign = new Preordercampaign($preorder->preordercampaign_id);
if (!$campaign->id) self::sendError("Kampagne nicht gefunden.");
$network = NetworkModel::getOne($campaign->network_id);
if (!$network) self::sendError("Netzwerk nicht gefunden.");
return WorkorderTenantConfigModel::getFirst(['addressId' => $network->owner_id]) ?? null;
}
//region BACKGROUND TASKS
/**
* Creates new workorders from preorders based on tenant configurations.
* Runs at most once every 5 minutes to avoid performance issues.
*/
protected function createWorkordersFromPreorders()
{
$lockFile = TEMP_DIR . "/task_create_workorders.lock";
if (file_exists($lockFile) && (time() - intval(file_get_contents($lockFile))) < 300) {
return; // Run only every 5 minutes
}
$configs = WorkorderTenantConfigModel::getAll();
foreach ($configs as $config) {
$filters = json_decode($config->workorderCreationFilters, true);
if (empty($filters)) continue;
$networks = NetworkModel::search(['owner_id' => $config->addressId]);
if (empty($networks)) continue;
$networkIds = array_map(fn($n) => $n->id, $networks);
$tenantCampaigns = array_map(fn($c) => $c->id, PreordercampaignModel::search(['network_id' => $networkIds]));
if (empty($tenantCampaigns)) continue;
$filters['preordercampaign_id'] = $tenantCampaigns;
$newPreorders = PreorderModel::searchActive($filters);
foreach ($newPreorders as $preorder) {
$existingWorkorder = (array) WorkorderModel::getFirst(['preorderId' => $preorder->id]);
if ($existingWorkorder) {
if ($existingWorkorder['status'] === 'archived') {
$oldStatus = $existingWorkorder['status'];
$new = (array) $existingWorkorder;
$new['status'] = 'new';
$new['companyId'] = null;
$new['civilEngineeringCompanyId'] = null;
$new['deadlineDate'] = null;
$new['appointmentDate'] = null;
$new['clusterId'] = $preorder->preordercampaign_id;
WorkorderModel::update($new);
WorkorderJournalModel::create([
'workorderId' => $existingWorkorder['id'],
'text' => 'Arbeitsauftrag wurde automatisch reaktiviert, da die zugehörige Vorbestellung wieder den Kriterien entspricht.',
'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('new'),
'create' => time(),
'createBy' => 1,
]);
}
} else {
WorkorderModel::create([
'preorderId' => $preorder->id,
'clusterId' => $preorder->preordercampaign_id,
'status' => 'new',
'create' => time(),
'createBy' => 1
]);
}
}
}
file_put_contents($lockFile, time());
}
/**
* Archives workorders that are no longer considered active based on tenant configurations.
* Runs at most once every 5 minutes to avoid performance issues.
*/
protected function archiveWorkorders()
{
$lockFile = TEMP_DIR . "/task_archive_workorders.lock";
if (file_exists($lockFile) && (time() - intval(file_get_contents($lockFile))) < 300) {
return; // Run only every 5 minutes
}
$configs = WorkorderTenantConfigModel::getAll();
foreach ($configs as $config) {
$activeFilters = json_decode($config->workorderActiveFilters, true);
if (empty($activeFilters)) {
continue;
}
$networks = NetworkModel::search(['owner_id' => $config->addressId]);
if (empty($networks)) {
continue;
}
$networkIds = array_map(fn($n) => $n->id, $networks);
$tenantCampaignIds = array_map(fn($c) => $c->id, PreordercampaignModel::search(['network_id' => $networkIds]));
if (empty($tenantCampaignIds)) {
continue;
}
$activeFilters['preordercampaign_id'] = $tenantCampaignIds;
$activePreorderIds = array_map(fn($p) => $p->id, PreorderModel::searchActive($activeFilters));
$activePreorderIdsSet = array_flip($activePreorderIds);
$statusesToCheck = ['new', 'assigned', 'scheduled', 'in_progress', 'correction_requested', 'intervention_required', 'civil_engineering_required', 'civil_engineering_completed', 'problem_solved'];
// Get ALL preorders for tenant (including deleted/cancelled) to ensure their workorders get archived
// Note: Not passing 'deleted' filter means all preorders are returned regardless of deleted status
$allTenantPreorders = PreorderModel::search(['preordercampaign_id' => $tenantCampaignIds]);
if(empty($allTenantPreorders)) continue;
$allTenantPreorderIds = array_map(fn($p) => $p->id, $allTenantPreorders);
$workordersToCheck = WorkorderModel::getAll([
'status' => $statusesToCheck,
'preorderId' => $allTenantPreorderIds
]);
foreach ($workordersToCheck as $workorder) {
if (!isset($activePreorderIdsSet[$workorder->preorderId])) {
$oldStatus = $workorder->status;
$workorder->status = 'archived';
WorkorderModel::update((array)$workorder);
WorkorderJournalModel::create([
'workorderId' => $workorder->id,
'text' => 'Arbeitsauftrag wurde automatisch archiviert, da die zugehörige Vorbestellung nicht mehr den Aktiv-Kriterien entspricht.',
'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('archived'),
'create' => time(),
'createBy' => 1, // System user
]);
}
}
}
file_put_contents($lockFile, time());
}
//endregion
}