274 lines
13 KiB
PHP
274 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;
|
|
|
|
$tenantCampaigns = array_map(fn($n) => $n->id, PreordercampaignModel::getAll(['network_id' => array_map(fn($n) => $n->id, $networks)]));
|
|
if (empty($tenantCampaigns)) continue;
|
|
|
|
$filters['preordercampaign_id'] = $tenantCampaigns;
|
|
$newPreorders = PreorderModel::searchActive($filters);
|
|
|
|
foreach ($newPreorders as $preorder) {
|
|
$existingWorkorder = 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;
|
|
}
|
|
|
|
$tenantCampaignIds = array_column(PreordercampaignModel::getAll(['network_id' => array_column($networks, 'id')]), 'id');
|
|
if (empty($tenantCampaignIds)) {
|
|
continue;
|
|
}
|
|
|
|
$activeFilters['preordercampaign_id'] = $tenantCampaignIds;
|
|
|
|
$activePreorderIds = array_column(PreorderModel::searchActive($activeFilters), 'id');
|
|
$activePreorderIdsSet = array_flip($activePreorderIds);
|
|
|
|
$statusesToCheck = ['new', 'assigned', 'scheduled', 'in_progress', 'correction_requested', 'intervention_required', 'civil_engineering_required', 'civil_engineering_completed', 'problem_solved'];
|
|
|
|
$allTenantPreorders = PreorderModel::getAll(['preordercampaign_id' => $tenantCampaignIds]);
|
|
if(empty($allTenantPreorders)) continue;
|
|
|
|
$allTenantPreorderIds = array_column($allTenantPreorders, 'id');
|
|
|
|
$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
|
|
}
|