overhauled workorder module

This commit is contained in:
Luca Haid
2025-10-08 13:18:48 +02:00
parent 200226a4bd
commit 2ce870746b
9 changed files with 314 additions and 50 deletions

View File

@@ -24,7 +24,7 @@ class WorkorderModel extends TTCrudBaseModel
$sql = Helper::generateFilterCondition(array_map('intval', $allowedCampaignIds), 'p.preordercampaign_id');
if (empty($filters['status'])) {
$sql .= " AND w.status NOT IN ('completed', 'cancelled')";
$sql .= " AND w.status NOT IN ('completed', 'cancelled', 'charged', 'archived')";
} else {
$sql .= Helper::generateFilterCondition($filters['status'], 'w.status', true);
}
@@ -114,12 +114,16 @@ class WorkorderModel extends TTCrudBaseModel
private static function buildCompanyWhereClause(array $filters, int $companyId): string
{
$sql = "(w.companyId = " . $companyId . " OR w.civilEngineeringCompanyId = " . $companyId . ")";
$sql = "(w.companyId = " . $companyId . " OR w.civilEngineeringCompanyId = " . $companyId . ") AND w.status != 'charged'";
if (empty($filters['status'])) {
$sql .= " AND w.status NOT IN ('completed', 'cancelled', 'archived')";
} else {
$sql .= Helper::generateFilterCondition($filters['status'], 'w.status', true);
}
if (empty($filters['status'])) $sql .= " AND w.status NOT IN ('completed', 'cancelled')";
else $sql .= Helper::generateFilterCondition($filters['status'], 'w.status', true);
if (!empty($filters['id'])) $sql .= Helper::generateFilterCondition($filters['id'], 'w.id', true);
if (!empty($filters['status'])) $sql .= Helper::generateFilterCondition($filters['status'], 'w.status');
if (!empty($filters['preordercampaign_id'])) $sql .= Helper::generateFilterCondition($filters['preordercampaign_id'], 'p.preordercampaign_id');
if (!empty($filters['deadlineDate'])) $sql .= Helper::generateFilterCondition($filters['deadlineDate'], 'w.deadlineDate');
if (!empty($filters['networkOwnerName'])) $sql .= Helper::generateFilterCondition($filters['networkOwnerName'], 'owner_addr.company');
if (!empty($filters['appointmentDate'])) $sql .= Helper::generateFilterCondition($filters['appointmentDate'], 'w.appointmentDate');
@@ -157,7 +161,7 @@ class WorkorderModel extends TTCrudBaseModel
$orderBy = "";
if (!empty($order['key'])) {
$sortableColumns = ['id', 'status', 'deadlineDate', 'appointmentDate', 'additionalInfo'];
$sortableColumns = ['id', 'status', 'deadlineDate', 'appointmentDate', 'additionalInfo', 'preordercampaign_id'];
if (in_array($order['key'], $sortableColumns)) {
$sortOrder = (strtoupper($order['order']) === 'DESC') ? 'DESC' : 'ASC';
$orderBy = " ORDER BY " . $db->real_escape_string($order['key']) . " " . $sortOrder;
@@ -193,4 +197,4 @@ class WorkorderModel extends TTCrudBaseModel
$result = $db->query($sql);
return $result ? $result->fetch_assoc()['count'] : 0;
}
}
}

View File

@@ -54,6 +54,7 @@ class WorkorderAdminController extends WorkorderBaseController
public function indexAction()
{
$this->createWorkordersFromPreorders();
$this->archiveWorkorders();
parent::indexAction();
}
@@ -177,6 +178,40 @@ class WorkorderAdminController extends WorkorderBaseController
self::returnJson(['success' => true, 'message' => 'Dokumentation akzeptiert und Arbeitsauftrag abgeschlossen.']);
}
protected function chargeWorkorderAction()
{
if (!$this->user->can('RMLAdmin')) {
self::sendError("Keine Berechtigung.");
}
if (empty($this->postData['workorderId'])) {
self::sendError("Arbeitsauftrags-ID fehlt.");
}
$workorder = WorkorderModel::get($this->postData['workorderId']);
if (!$workorder) {
self::sendError("Arbeitsauftrag nicht gefunden.");
}
if ($workorder->status !== 'completed') {
self::sendError("Nur abgeschlossene Arbeitsaufträge können verrechnet werden.");
}
$oldStatus = $workorder->status;
$workorder->status = 'charged';
WorkorderModel::update((array)$workorder);
WorkorderJournalModel::create([
'workorderId' => $workorder->id,
'text' => 'Arbeitsauftrag wurde als verrechnet markiert.',
'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('charged'),
'create' => time(),
'createBy' => $this->user->id,
]);
self::returnJson(['success' => true, 'message' => 'Arbeitsauftrag als verrechnet markiert.']);
}
protected function setToProblemSolvedAction()
{
if (empty($this->postData['workorderId']) || empty($this->postData['text'])) self::sendError("Arbeitsauftrags-ID und Text sind erforderlich.");
@@ -276,32 +311,5 @@ class WorkorderAdminController extends WorkorderBaseController
}
return true;
}
private function createWorkordersFromPreorders()
{
$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) {
if (!WorkorderModel::getFirst(['preorderId' => $preorder->id])) {
WorkorderModel::create([
'preorderId' => $preorder->id, 'clusterId' => $preorder->preordercampaign_id,
'status' => 'new', 'create' => time(), 'createBy' => 0 // System User
]);
}
}
}
}
//endregion
}
}

View File

@@ -20,7 +20,9 @@ class WorkorderBaseController extends TTCrud
['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'],
]]
];
@@ -50,6 +52,7 @@ class WorkorderBaseController extends TTCrud
$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');
@@ -136,4 +139,107 @@ class WorkorderBaseController extends TTCrud
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) {
if (!WorkorderModel::getFirst(['preorderId' => $preorder->id])) {
WorkorderModel::create([
'preorderId' => $preorder->id, 'clusterId' => $preorder->preordercampaign_id,
'status' => 'new', 'create' => time(), 'createBy' => 0 // System User
]);
}
}
}
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', '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
}

View File

@@ -16,7 +16,7 @@ class WorkorderCompanyController extends WorkorderBaseController {
['key' => 'deadlineDate', 'text' => 'Deadline', 'modal' => false, 'table' => ['filter' => 'date', 'sortable' => true]],
['key' => 'appointmentDate', 'text' => 'Termin', 'modal' => false, 'table' => ['filter' => 'date', 'sortable' => true]],
];
protected array $additionalJSVariables = ['COMPANY_ID' => '0'];
protected array $additionalJSVariables = ['COMPANY_ID' => '0', 'RML_COMPANY_MANAGER' => false];
protected function prepareCrudConfig() {
$preorderInfoColIdx = array_search('preorderInfo', array_column($this->columns, 'key'));
@@ -34,6 +34,14 @@ class WorkorderCompanyController extends WorkorderBaseController {
$this->additionalJSVariables['COMPANY_ID'] = $company ? $company->id : 0;
if ($this->user->can('RMLCompanyManager')) {
$this->additionalJSVariables['RML_COMPANY_MANAGER'] = true;
}
}
public function indexAction() {
$this->archiveWorkorders();
parent::indexAction();
}
protected function logout() {
@@ -185,7 +193,7 @@ class WorkorderCompanyController extends WorkorderBaseController {
$doc = WorkorderDocumentationModel::get($this->postData['id']);
if (!$doc) self::sendError("Dokument nicht gefunden.");
if (isset($this->postData['documentType'])) $doc->documentType = $this->postData['documentType'];
WorkorderDocumentationModel::update((array)$doc);
WorkorderModel::update((array)$doc);
self::returnJson(['success' => true, 'message' => 'Dokument aktualisiert.']);
}
@@ -213,4 +221,4 @@ class WorkorderCompanyController extends WorkorderBaseController {
self::returnJson(['success' => true, 'message' => 'Tiefbau erfolgreich abgeschlossen.']);
}
//endregion
}
}

View File

@@ -19,6 +19,7 @@ class WorkorderTenantConfigController extends TTCrud {
$data['documentationTypes'] = json_encode($data['documentationTypes'] ?? []);
$data['interventionTypes'] = json_encode($data['interventionTypes'] ?? []);
$data['workorderCreationFilters'] ??= '{}';
$data['workorderActiveFilters'] ??= '{}';
if (empty($data['id'])) {
$data['create'] = time();

View File

@@ -7,6 +7,7 @@ class WorkorderTenantConfigModel extends TTCrudBaseModel {
public string $name;
public string $documentationTypes; // JSON
public string $workorderCreationFilters; // JSON
public ?string $workorderActiveFilters; // JSON
public ?string $interventionTypes; // JSON
public int $civilEngineeringDocsRequired;
public int $create;
@@ -30,4 +31,4 @@ class WorkorderTenantConfigModel extends TTCrudBaseModel {
$row = $result ? $result->fetch_assoc() : null;
return $row ? new self($row) : null;
}}
}}