diff --git a/Layout/default/VueViews/Vue.php b/Layout/default/VueViews/Vue.php
index 5869d53b1..23f7bd02e 100644
--- a/Layout/default/VueViews/Vue.php
+++ b/Layout/default/VueViews/Vue.php
@@ -23,6 +23,7 @@ $additionalCSS = [
'plugins/vue/tt-components/css/tt-table.css',
'plugins/vue/tt-components/css/tt-tooltip.css',
'plugins/vue/tt-components/css/tt-loader.css',
+ 'plugins/vue/tt-components/css/tt-file-gallery.css',
'plugins/vue/tt-components/css/tt-position-manager.css',
];
diff --git a/Layout/default/footer.php b/Layout/default/footer.php
index f39026bcd..2fa965d51 100644
--- a/Layout/default/footer.php
+++ b/Layout/default/footer.php
@@ -22,11 +22,13 @@
-
diff --git a/application/File/FileController.php b/application/File/FileController.php
index 705600ab7..158913a55 100644
--- a/application/File/FileController.php
+++ b/application/File/FileController.php
@@ -92,27 +92,24 @@ class FileController extends mfBaseController {
$id = $this->request->id;
$size = $this->request->size;
- if (!is_numeric($id) || $id < 1) {
- http_response_code(400);
- self::returnJson(["error" => "Invalid File ID"]);
- return;
- }
+ if (!is_numeric($id) || $id < 1) self::sendError("Invalid File ID");
$file = new File($id);
- if (!$file->id) {
- http_response_code(404);
- self::returnJson(["error" => "File record not found"]);
- return;
- }
+ if (!$file->id) self::sendError("File record not found");
$originalPath = MFUPLOAD_FILE_SAVE_PATH . ($file->subfolder ? "/{$file->subfolder}" : "") . "/{$file->store_filename}";
- if (!is_readable($originalPath)) {
- http_response_code(404);
- self::returnJson(["error" => "Physical file not found"]);
- return;
- }
+ if (!is_readable($originalPath)) self::sendError("Physical file not found");
+
$imageInfo = @getimagesize($originalPath);
+
+ if ($imageInfo === false && mime_content_type($originalPath) === 'application/pdf') {
+ header('Content-Type: application/pdf');
+ header('Content-Disposition: inline; filename="' . ($file->orig_filename ?: $file->store_filename) . '"');
+ readfile($originalPath);
+ exit;
+ }
+
if ($imageInfo === false) {
$this->downloadAction();
return;
@@ -129,18 +126,13 @@ class FileController extends mfBaseController {
$cacheDir = TEMP_DIR . "/thumbnails";
@mkdir($cacheDir, 0775, true);
-
$cachedPath = "{$cacheDir}/{$id}_{$size}." . pathinfo($originalPath, PATHINFO_EXTENSION);
if (!file_exists($cachedPath)) {
$command = "convert " . escapeshellarg($originalPath) . " -resize " . escapeshellarg($sizeDimensions[$size]) . " " . escapeshellarg($cachedPath);
exec($command, $output, $return_var);
- if ($return_var !== 0) {
- http_response_code(500);
- self::returnJson(["error" => "Failed to create thumbnail."]);
- return;
- }
+ if ($return_var !== 0) self::sendError("Failed to create thumbnail.");
}
header('Content-Type: ' . $imageInfo['mime']);
@@ -148,5 +140,4 @@ class FileController extends mfBaseController {
readfile($cachedPath);
exit;
}
-
}
\ No newline at end of file
diff --git a/application/RMLWorkorder/RMLWorkorderController.php b/application/RMLWorkorder/RMLWorkorderController.php
deleted file mode 100644
index 22e68b388..000000000
--- a/application/RMLWorkorder/RMLWorkorderController.php
+++ /dev/null
@@ -1,267 +0,0 @@
- 'id', 'text' => 'Auftrag-Nr.'],
- ['key' => 'preorderInfo', 'text' => 'Kunde / Projekt', 'modal' => false, 'table' => ['sortable' => false, 'filter' => false]],
- ['key' => 'companyName', 'text' => 'Zuständige Firma', 'modal' => false, 'table' => ['filter' => 'search']],
- ['key' => 'status', 'text' => 'Status', 'modal' => ['items' => [
- ['value' => 'new', 'text' => 'Neu', 'icon' => 'fas fa-star text-primary'],
- ['value' => 'assigned', 'text' => 'Zugewiesen', 'icon' => 'fas fa-user-check text-info'],
- ['value' => 'scheduled', 'text' => 'Terminiert', 'icon' => 'fas fa-calendar-check text-warning'],
- ['value' => 'documented', 'text' => 'Dokumentiert', 'icon' => 'fas fa-file-alt text-success'],
- ['value' => 'completed', 'text' => 'Abgeschlossen', 'icon' => 'fas fa-check-double text-secondary'],
- ]], 'table' => ['filter' => 'iconSelect']],
- ['key' => 'deadlineDate', 'text' => 'Deadline', 'modal' => false, 'table' => ['filter' => 'date']],
- ['key' => 'appointmentDate', 'text' => 'Termin', 'modal' => false, 'table' => ['filter' => 'date']],
- ['key' => 'actions', 'text' => 'Aktionen', 'modal' => false, 'table' => ['filter' => false, 'sortable' => false]],
- ];
-
- protected array $additionalJSVariables = ['RML_ADMIN' => '0', 'COMPANY_ID' => '0'];
-
- protected function prepareCrudConfig() {
- // Assume 'RMLAdmin' is a permission.
- if ($this->user->can('RMLAdmin')) {
- $this->additionalJSVariables['RML_ADMIN'] = '1';
- } else {
- // If not an admin, find the user's associated company ID
- $company = RMLWorkorderCompanyModel::getAll(['addressId' => $this->user->address_id], 1);
- if ($company) {
- $this->additionalJSVariables['COMPANY_ID'] = $company[0]->id;
- } else {
- // If user is not an RML admin and not linked to a company, they see nothing.
- $this->sendError('Access Denied. You are not associated with a registered RML company.', 403);
- }
- }
- }
-
- protected function getAction()
- {
- // First, automatically create workorders for any new preorders with status 220.
- // In a production environment, this might be a separate cron job.
- $this->createWorkordersFromPreorders();
-
- $json = json_decode(file_get_contents('php://input'), true);
- $pagination = $json['pagination'] ?? ['page' => 1, 'per_page' => 10];
- $filters = $json['filters'] ?? [];
- $order = $json['order'] ?? ['key' => 'id', 'order' => 'DESC'];
-
- // If user is a company, filter by their companyId
- if ($this->user->can('RMLAdmin') === false) {
- $company = RMLWorkorderCompanyModel::getAll(['addressId' => $this->user->address_id], 1);
- if($company) {
- $filters['companyId'] = $company[0]->id;
- }
- }
-
- $workorders = RMLWorkorderModel::getAll($filters, $pagination['per_page'], ($pagination['page'] - 1) * $pagination['per_page'], $order);
- $totalCount = RMLWorkorderModel::count($filters);
-
- // Enhance rows with data from other tables
- $rows = [];
- foreach($workorders as $workorder) {
- $row = (array)$workorder;
-
- $preorder = new Preorder($workorder->preorderId); // Placeholder for actual Preorder retrieval
- $anschlussadresse = '';
- if ($preorder->building_id) {
- $anschlussadresse = "{$preorder->building->street}
{$preorder->building->zip} {$preorder->building->city}";
- } elseif ($preorder->adb_hausnummer_id) {
- $anschlussadresse = "{$preorder->adb_hausnummer->strasse->name} {$preorder->adb_hausnummer->hausnummer}";
- if ($preorder->adb_hausnummer->stiege) {
- $anschlussadresse .= "/{$preorder->adb_hausnummer->stiege}";
- }
- if ($preorder->adb_wohneinheit_id && (string)$preorder->adb_wohneinheit) {
- $anschlussadresse .= "
{$preorder->adb_wohneinheit}";
- }
- $anschlussadresse .= "
{$preorder->adb_hausnummer->plz->plz} {$preorder->adb_hausnummer->ortschaft->name}";
- }
-
- $kunde = ($preorder->company) ? $preorder->company : "{$preorder->firstname} {$preorder->lastname}";
- $kunde .= "
{$preorder->street}";
- if ($preorder->housenumber) {
- $kunde .= " {$preorder->housenumber}";
- }
- $kunde .= "
{$preorder->zip} {$preorder->city}";
-
- $kontakt = ($preorder->phone) ? "{$preorder->phone}
" : '';
- $kontakt .= ($preorder->email) ? $preorder->email : '';
-
- $row['preorderInfo'] = "Anschlussadresse: {$anschlussadresse}
" .
- "Kunde: {$kunde}
" .
- "Kontakt: {$kontakt}
" .
- "OAID: {$preorder->oaid}";
-
- // Get Company Name
- if($workorder->companyId) {
- $company = RMLWorkorderCompanyModel::get($workorder->companyId);
- $row['companyName'] = $company->name ?? 'N/A';
- } else {
- $row['companyName'] = 'Nicht zugewiesen';
- }
-
- $rows[] = $row;
- }
-
- $pagination = [
- 'page' => $pagination['page'],
- 'per_page' => $pagination['per_page'],
- 'filtered_available' => $totalCount,
- 'total_rows' => $totalCount,
- ];
-
- self::returnJson([
- 'rows' => $rows,
- 'pagination' => $pagination
- ]);
- }
-
- private function createWorkordersFromPreorders() {
- // Fetch all active preorders where the status code is 220
- $newPreorders = PreorderModel::searchActive(['status_code' => 220]);
-
- // If no new preorders are found, there's nothing to do
- if (empty($newPreorders)) {
- return;
- }
-
- // Iterate through each preorder that needs a workorder
- foreach ($newPreorders as $preorder) {
- // Check if a workorder for this preorder already exists to prevent duplicates
- $existingWorkorder = RMLWorkorderModel::getFirst(['preorderId' => $preorder->id]);
-
- // If no workorder exists, create a new one
- if (!$existingWorkorder) {
- RMLWorkorderModel::create([
- 'preorderId' => $preorder->id,
- 'status' => 'new',
- 'create' => time(),
- 'createBy' => $this->user->id // The logged-in user creating the record
- ]);
- }
- }
- }
-
- protected function assignWorkorderAction() {
- if (!$this->user->can('RMLAdmin')) self::sendError("Permission denied.", 403);
-
- $post = json_decode(file_get_contents('php://input'), true);
- if (empty($post['workorderId']) || empty($post['companyId'])) {
- self::sendError("Required fields are missing.");
- }
-
- if (!$rmlWorkorder = RMLWorkorderModel::get($post['workorderId'])) self::sendError("Workorder not found.");
-
- RMLWorkorderModel::update(
- array_merge((array) $rmlWorkorder, [
- 'id' => $post['workorderId'],
- 'companyId' => $post['companyId'],
- 'status' => 'assigned',
- 'assignmentDate' => time(),
- 'deadlineDate' => strtotime('+6 weeks')
- ])
- );
-
- self::returnJson(['success' => true, 'message' => 'Auftrag erfolgreich zugewiesen.']);
- }
-
- protected function scheduleAppointmentAction() {
- $post = json_decode(file_get_contents('php://input'), true);
- if (empty($post['workorderId']) || empty($post['appointmentDate'])) {
- self::sendError("Required fields are missing.");
- }
-
- RMLWorkorderModel::update([
- 'id' => $post['workorderId'],
- 'appointmentDate' => $post['appointmentDate'],
- 'status' => 'scheduled'
- ]);
-
- self::returnJson(['success' => true, 'message' => 'Termin erfolgreich gespeichert.']);
- }
-
- protected function uploadDocumentationAction()
- {
- $file = $_FILES['file'] ?? null;
- if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
- self::returnJson(['error' => 'File upload failed']);
- return;
- }
-
- $workorderId = $_POST['workorderId'] ?? null;
- $description = $_POST['description'] ?? '';
- $documentType = $_POST['documentType'] ?? 'general';
-
- if(!$workorderId) {
- self::returnJson(['error' => 'Workorder ID is missing.']);
- return;
- }
-
- try {
- $uploaded = mfUpload::handleFormUpload("file", false, "/RMLWorkorder");
-
- RMLWorkorderDocumentationModel::create([
- 'workorderId' => $workorderId,
- 'fileId' => $uploaded->id,
- 'description' => $description,
- 'documentType' => $documentType,
- 'create' => time(),
- 'createBy' => $this->user->id
- ]);
-
- // Set status to 'documented' if it was 'scheduled' or 'assigned'
- $workorder = RMLWorkorderModel::get($workorderId);
- if(in_array($workorder->status, ['assigned', 'scheduled'])) {
- RMLWorkorderModel::update(['id' => $workorderId, 'status' => 'documented']);
- }
-
- self::returnJson(['success' => true, 'fileId' => $uploaded->id, 'fileName' => $file['name']]);
- } catch (Exception $e) {
- self::returnJson(['error' => 'Upload error: ' . $e->getMessage()]);
- }
- }
-
- protected function getDocumentationAction() {
- if(empty($this->request->workorderId)) self::sendError("Workorder ID missing.");
-
- $docs = RMLWorkorderDocumentationModel::getAll(['workorderId' => $this->request->workorderId]);
- // Enhance with file names
- foreach($docs as $doc) {
- $file = new File($doc->fileId);
- $doc->fileName = $file->filename;
- }
- self::returnJson($docs);
- }
-
- protected function completeWorkorderAction() {
- $post = json_decode(file_get_contents('php://input'), true);
- if(empty($post['workorderId'])) self::sendError("Workorder ID missing.");
-
- $workorder = RMLWorkorderModel::get($post['workorderId']);
- if(!$workorder) self::sendError("Workorder not found.");
-
- // Update Preorder status to 245
- // PreorderModel::update(['id' => $workorder->preorderId, 'status_code' => 245]);
-
- // Update Workorder status
- RMLWorkorderModel::update([
- 'id' => $workorder->id,
- 'status' => 'completed'
- ]);
-
- self::returnJson(['success' => true, 'message' => 'Auftrag abgeschlossen. Preorder wurde aktualisiert.']);
- }
-
- // Action to get companies for the assignment modal
- protected function getCompaniesAction() {
- if(!$this->user->can('RMLAdmin')) self::sendError("Permission denied.", 403);
- $companies = RMLWorkorderCompanyModel::getAll();
- $items = array_map(fn($c) => ['value' => $c->id, 'text' => $c->name], $companies);
- self::returnJson($items);
- }
-}
\ No newline at end of file
diff --git a/application/RMLWorkorderAdmin/RMLWorkorderAdminController.php b/application/RMLWorkorderAdmin/RMLWorkorderAdminController.php
new file mode 100644
index 000000000..e4a87014e
--- /dev/null
+++ b/application/RMLWorkorderAdmin/RMLWorkorderAdminController.php
@@ -0,0 +1,157 @@
+ 'id', 'text' => 'Auftrag-Nr.', 'table' => ['sortable' => true]],
+ ['key' => 'preorderInfo', 'text' => 'Kunde / Projekt', 'modal' => false, 'table' => ['sortable' => false, 'filter' => 'search']],
+ ['key' => 'companyName', 'text' => 'Zuständige Firma', 'modal' => false, 'table' => ['filter' => 'search']],
+ ['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' => 'Terminiert', 'icon' => 'fas fa-calendar-check text-warning'],
+ ['value' => 'documented', 'text' => 'Dokumentiert', 'icon' => 'fas fa-file-alt text-success'],
+ ['value' => 'completed', 'text' => 'Abgeschlossen', 'icon' => 'fas fa-check-double text-secondary'],
+ ]]],
+ ['key' => 'deadlineDate', 'text' => 'Deadline', 'modal' => false, 'table' => ['filter' => 'date']],
+ ['key' => 'appointmentDate', 'text' => 'Termin', 'modal' => false, 'table' => ['filter' => 'date']],
+ ['key' => 'actions', 'text' => 'Aktionen', 'modal' => false, 'table' => ['filter' => false, 'sortable' => false]],
+ ];
+
+ protected function indexAction()
+ {
+ $this->createWorkordersFromPreorders();
+ Helper::renderVue($this, 'RMLWorkorderAdmin', $this->headerTitle, [
+ "CRUD_CONFIG" => $this->getCrudConfig(),
+ "TABLE_URL" => $this::getUrl("RMLWorkorderAdmin/get"),
+ ]);
+ }
+
+ protected function getAction()
+ {
+ $json = json_decode(file_get_contents('php://input'), true);
+ $pagination = $json['pagination'] ?? ['page' => 1, 'per_page' => 10];
+ $filters = $json['filters'] ?? [];
+ $order = $json['order'] ?? ['key' => 'id', 'order' => 'DESC'];
+
+ // Custom filter logic for preorderInfo
+ if (!empty($filters['preorderInfo'])) {
+ $searchTerm = $filters['preorderInfo'];
+ unset($filters['preorderInfo']);
+
+ // This is a simplified search. A more robust implementation might involve a full-text search or a more complex query.
+ $preorders = PreorderModel::getAll(['firstname|lastname|company|oaid' => $searchTerm]);
+ $preorderIds = array_map(fn($p) => $p->id, $preorders);
+
+ if (!empty($preorderIds)) {
+ $filters['preorderId'] = $preorderIds;
+ } else {
+ // No preorders found, so no workorders will be found
+ $filters['id'] = -1;
+ }
+ }
+
+ $workorders = RMLWorkorderModel::getAll($filters, $pagination['per_page'], ($pagination['page'] - 1) * $pagination['per_page'], $order);
+ $totalCount = RMLWorkorderModel::count($filters);
+
+ $rows = [];
+ foreach($workorders as $workorder) {
+ $row = (array)$workorder;
+
+ $preorder = new Preorder($workorder->preorderId);
+ $anschlussadresse = 'N/A';
+ if ($preorder->adb_hausnummer_id) {
+ $hn = $preorder->adb_hausnummer;
+ $anschlussadresse = "{$hn->strasse->name} {$hn->hausnummer}";
+ if ($hn->stiege) $anschlussadresse .= "/{$hn->stiege}";
+ if ($preorder->adb_wohneinheit_id) $anschlussadresse .= " / WE: {$preorder->adb_wohneinheit->bezeichner}";
+ $anschlussadresse .= ", {$hn->plz->plz} {$hn->ortschaft->name}";
+ }
+
+ $kunde = ($preorder->company) ?: "{$preorder->firstname} {$preorder->lastname}";
+
+ $row['preorderInfo'] = "Kunde: {$kunde}
" .
+ "Anschluss: {$anschlussadresse}
" .
+ "OAID: {$preorder->oaid}";
+
+ if($workorder->companyId) {
+ $company = RMLWorkorderCompanyModel::get($workorder->companyId);
+ $row['companyName'] = $company->name ?? 'N/A';
+ } else {
+ $row['companyName'] = 'Nicht zugewiesen';
+ }
+
+ $rows[] = $row;
+ }
+
+ self::returnJson([
+ 'rows' => $rows,
+ 'pagination' => [
+ 'page' => $pagination['page'],
+ 'per_page' => $pagination['per_page'],
+ 'total_rows' => $totalCount,
+ 'total_pages' => ceil($totalCount / $pagination['per_page']),
+ 'filtered_available' => $totalCount
+ ]
+ ]);
+ }
+
+ private function createWorkordersFromPreorders() {
+ $newPreorders = PreorderModel::searchActive(['status_code' => 220]);
+ if (empty($newPreorders)) return;
+
+ foreach ($newPreorders as $preorder) {
+ if (!RMLWorkorderModel::getFirst(['preorderId' => $preorder->id])) {
+ RMLWorkorderModel::create([
+ 'preorderId' => $preorder->id,
+ 'status' => 'new',
+ 'create' => time(),
+ 'createBy' => $this->user->id
+ ]);
+ }
+ }
+ }
+
+ protected function assignWorkorderAction() {
+ $post = json_decode(file_get_contents('php://input'), true);
+ if (empty($post['workorderId']) || empty($post['companyId'])) self::sendError("Required fields are missing.");
+
+ $workorder = RMLWorkorderModel::get($post['workorderId']);
+ if (!$workorder) self::sendError("Workorder not found.");
+
+ $workorder->companyId = $post['companyId'];
+ $workorder->status = 'assigned';
+ $workorder->assignmentDate = time();
+ $workorder->deadlineDate = strtotime('+6 weeks');
+
+ RMLWorkorderModel::update((array)$workorder);
+
+ self::returnJson(['success' => true, 'message' => 'Auftrag erfolgreich zugewiesen.']);
+ }
+
+ protected function getDocumentationAction() {
+ if(empty($this->request->workorderId)) self::sendError("Workorder ID missing.");
+
+ $docs = RMLWorkorderDocumentationModel::getAll(['workorderId' => $this->request->workorderId], null, 0, ['key' => 'create', 'order' => 'DESC']);
+ $users = UserModel::search(['employee' => true]);
+ $userMap = array_reduce($users, fn($carry, $user) => $carry + [$user->id => $user->name], []);
+
+ foreach($docs as $doc) {
+ $file = new File($doc->fileId);
+ $doc->fileName = $file->orig_filename ?? $file->filename;
+ $doc->userName = $userMap[$doc->createBy] ?? 'Unbekannt';
+ }
+ self::returnJson($docs);
+ }
+
+ protected function getCompaniesAction() {
+ $companies = RMLWorkorderCompanyModel::getAll([], null, 0, ['key' => 'name', 'order' => 'ASC']);
+ $items = array_map(fn($c) => ['value' => $c->id, 'text' => $c->name], $companies);
+ self::returnJson($items);
+ }
+}
\ No newline at end of file
diff --git a/application/RMLWorkorderCompany/RMLWorkorderCompanyController.php b/application/RMLWorkorderCompany/RMLWorkorderCompanyController.php
new file mode 100644
index 000000000..3829864ea
--- /dev/null
+++ b/application/RMLWorkorderCompany/RMLWorkorderCompanyController.php
@@ -0,0 +1,244 @@
+ 'id', 'text' => 'Auftrag-Nr.', 'table' => ['sortable' => true]],
+ ['key' => 'preorderInfo', 'text' => 'Kunde / Projekt', 'modal' => false, 'table' => ['sortable' => false, 'filter' => 'search']],
+ ['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' => 'Terminiert', 'icon' => 'fas fa-calendar-check text-warning'],
+ ['value' => 'documented', 'text' => 'Dokumentiert', 'icon' => 'fas fa-file-alt text-success'],
+ ['value' => 'completed', 'text' => 'Abgeschlossen', 'icon' => 'fas fa-check-double text-secondary'],
+ ]]],
+ ['key' => 'deadlineDate', 'text' => 'Deadline', 'modal' => false, 'table' => ['filter' => 'date']],
+ ['key' => 'appointmentDate', 'text' => 'Termin', 'modal' => false, 'table' => ['filter' => 'date']],
+ ['key' => 'actions', 'text' => 'Aktionen', 'modal' => false, 'table' => ['filter' => false, 'sortable' => false]],
+ ];
+
+ protected array $additionalJSVariables = ['COMPANY_ID' => '0'];
+
+ protected function prepareCrudConfig() {
+ $company = RMLWorkorderCompanyModel::getFirst(['addressId' => $this->user->address_id]);
+ if ($company) {
+ $this->additionalJSVariables['COMPANY_ID'] = $company->id;
+ } else {
+ $this->sendError('Access Denied. You are not associated with a registered RML company.', 403);
+ }
+ }
+
+ protected function indexAction()
+ {
+ Helper::renderVue($this, 'RMLWorkorderCompany', $this->headerTitle, [
+ "CRUD_CONFIG" => $this->getCrudConfig(),
+ "TABLE_URL" => $this::getUrl("RMLWorkorderCompany/get"),
+ "COMPANY_ID" => $this->additionalJSVariables['COMPANY_ID'],
+ ]);
+ }
+
+ protected function getAction()
+ {
+ $json = json_decode(file_get_contents('php://input'), true);
+ $pagination = $json['pagination'] ?? ['page' => 1, 'per_page' => 10];
+ $filters = $json['filters'] ?? [];
+ $order = $json['order'] ?? ['key' => 'id', 'order' => 'DESC'];
+
+ $company = RMLWorkorderCompanyModel::getFirst(['addressId' => $this->user->address_id]);
+ if(!$company) self::sendError("Company not found for user.", 403);
+ $filters['companyId'] = $company->id;
+
+ if (!empty($filters['preorderInfo'])) {
+ $searchTerm = $filters['preorderInfo'];
+
+ //todo: fix this preordermodel search shit
+ $preorders = PreorderModel::getAll(['firstname|lastname|company|oaid' => $searchTerm]);
+ $preorderIds = array_map(fn($p) => $p->id, $preorders);
+
+ if (!empty($preorderIds)) {
+ $filters['preorderId'] = $preorderIds;
+ } else {
+ $filters['id'] = -1;
+ }
+ }
+ unset($filters['preorderInfo']);
+ // only show workorders that are assigned to the company and have the status assigned or scheduled
+ $filters['status'] = ['assigned', 'scheduled'];
+ $filters['companyId'] = $company->id;
+
+
+ $workorders = RMLWorkorderModel::getAll($filters, $pagination['per_page'], ($pagination['page'] - 1) * $pagination['per_page'], $order);
+ $totalCount = RMLWorkorderModel::count($filters);
+
+ $rows = [];
+ foreach($workorders as $workorder) {
+ $row = (array)$workorder;
+ $row['preorderInfo'] = $this->getPreorderInfoText($workorder->preorderId);
+ $rows[] = $row;
+ }
+
+ self::returnJson([
+ 'rows' => $rows,
+ 'pagination' => [
+ 'page' => $pagination['page'],
+ 'per_page' => $pagination['per_page'],
+ 'total_rows' => $totalCount,
+ 'total_pages' => ceil($totalCount / $pagination['per_page']),
+ 'filtered_available' => $totalCount
+ ]
+ ]);
+ }
+
+ public function getWorkorderByIdAction() {
+ $id = $this->request->id;
+ if(!$id) self::sendError("ID missing");
+
+ $workorder = RMLWorkorderModel::get($id);
+ if(!$workorder) self::sendError("Workorder not found");
+
+ $workorder->preorderInfo = $this->getPreorderInfoText($workorder->preorderId);
+
+ self::returnJson((array) $workorder);
+ }
+
+ private function getPreorderInfoText($preorderId) {
+ $preorder = new Preorder($preorderId);
+ $anschlussadresse = 'N/A';
+ if ($preorder->adb_hausnummer_id) {
+ $hn = $preorder->adb_hausnummer;
+ $anschlussadresse = "{$hn->strasse->name} {$hn->hausnummer}";
+ if ($hn->stiege) $anschlussadresse .= "/{$hn->stiege}";
+ if ($preorder->adb_wohneinheit_id) $anschlussadresse .= " / WE: {$preorder->adb_wohneinheit->bezeichner}";
+ $anschlussadresse .= ", {$hn->plz->plz} {$hn->ortschaft->name}";
+ }
+
+ $kunde = ($preorder->company) ?: "{$preorder->firstname} {$preorder->lastname}";
+
+ return "Kunde: {$kunde}
" .
+ "Anschluss: {$anschlussadresse}
" .
+ "Kontakt: {$preorder->phone} / {$preorder->email}
" .
+ "OAID: {$preorder->oaid}";
+ }
+
+ protected function scheduleAppointmentAction() {
+ $post = json_decode(file_get_contents('php://input'), true);
+ if (empty($post['workorderId']) || empty($post['appointmentDate'])) self::sendError("Required fields are missing.");
+
+ $workorder = RMLWorkorderModel::get($post['workorderId']);
+ if(!$workorder) self::sendError("Workorder not found");
+
+ $workorder->appointmentDate = $post['appointmentDate'];
+ $workorder->status = 'scheduled';
+ RMLWorkorderModel::update((array)$workorder);
+
+ self::returnJson(['success' => true, 'message' => 'Termin erfolgreich gespeichert.']);
+ }
+
+ protected function uploadDocumentationAction()
+ {
+ if (empty($_FILES['files']) || empty($_POST['workorderId'])) {
+ self::returnJson(['error' => 'Required data is missing.']);
+ return;
+ }
+
+ $workorderId = $_POST['workorderId'];
+ $description = $_POST['description'] ?? '';
+ $documentType = $_POST['documentType'] ?? 'general';
+ $files = $_FILES['files'];
+ $uploadCount = 0;
+
+ foreach ($files['name'] as $index => $name) {
+ if ($files['error'][$index] === UPLOAD_ERR_OK) {
+ $_FILES['file'] = [
+ 'name' => $files['name'][$index],
+ 'type' => $files['type'][$index],
+ 'tmp_name' => $files['tmp_name'][$index],
+ 'error' => $files['error'][$index],
+ 'size' => $files['size'][$index]
+ ];
+
+ try {
+ $uploaded = mfUpload::handleFormUpload("file", false, "/RMLWorkorder");
+ RMLWorkorderDocumentationModel::create([
+ 'workorderId' => $workorderId,
+ 'fileId' => $uploaded->id,
+ 'description' => $description,
+ 'documentType' => $documentType,
+ 'create' => time(),
+ 'createBy' => $this->user->id
+ ]);
+ $uploadCount++;
+ } catch (Exception $e) {
+ var_dump($e->getMessage());exit;
+ // Log error but continue with other files
+ error_log("File upload failed for $name: " . $e->getMessage());
+ }
+ }
+ }
+
+ self::returnJson(['success' => true, 'message' => "$uploadCount Datei(en) erfolgreich hochgeladen."]);
+ }
+
+ protected function getDocumentationAction() {
+ if(empty($this->request->workorderId)) self::sendError("Workorder ID missing.");
+
+ // Order by creation date to ensure consistent numbering (_1, _2, etc.)
+ $docs = RMLWorkorderDocumentationModel::getAll(['workorderId' => $this->request->workorderId], null, 0, ['key' => 'create', 'order' => 'ASC']);
+
+ $responseDocs = [];
+ $typeCounts = [];
+
+ $translationMap = [
+ 'photo_before' => 'Foto vorher',
+ 'photo_during' => 'Foto währenddessen',
+ 'photo_after' => 'Foto nachher',
+ 'measurement_protocol' => 'Messprotokoll',
+ 'customer_signature' => 'Kundenunterschrift',
+ ];
+
+ foreach($docs as $doc) {
+ $file = new File($doc->fileId);
+
+ // Increment counter for the specific document type
+ $documentTypeKey = $doc->documentType;
+ if (!isset($typeCounts[$documentTypeKey])) {
+ $typeCounts[$documentTypeKey] = 1;
+ } else {
+ $typeCounts[$documentTypeKey]++;
+ }
+
+ // Construct the new filename using the original key
+ $originalFilename = $file->orig_filename ?? $file->filename;
+ $extension = pathinfo($originalFilename, PATHINFO_EXTENSION);
+ $translatedType = $translationMap[$documentTypeKey] ?? $documentTypeKey;
+ $newFilename = "{$translatedType} {$typeCounts[$documentTypeKey]}." . strtolower($extension);
+
+ // Get the translated text, with a fallback to the original key
+
+ // Build the response object with 'id' mapped from 'fileId' and the translated type
+ $responseDocs[] = [
+ 'id' => $doc->fileId,
+ 'fileName' => $newFilename,
+ 'documentType' => $documentTypeKey,
+ 'mimetype' => $file->mimetype,
+ ];
+ }
+ self::returnJson($responseDocs);
+ }
+ protected function completeWorkorderAction() {
+ $post = json_decode(file_get_contents('php://input'), true);
+ if(empty($post['workorderId'])) self::sendError("Workorder ID missing.");
+
+ $workorder = RMLWorkorderModel::get($post['workorderId']);
+ if(!$workorder) self::sendError("Workorder not found.");
+
+ $workorder->status = 'documented';
+ RMLWorkorderModel::update((array)$workorder);
+
+ self::returnJson(['success' => true, 'message' => 'Auftrag abgeschlossen.']);
+ }
+}
\ No newline at end of file
diff --git a/db/migrations/20250723204000_CreateRmlWorkorderTables.php b/db/migrations/20250723204000_CreateRmlWorkorderTables.php
new file mode 100644
index 000000000..99d4cfe42
--- /dev/null
+++ b/db/migrations/20250723204000_CreateRmlWorkorderTables.php
@@ -0,0 +1,59 @@
+getEnvironment() == "thetool") {
+ $this->execute('DROP TABLE IF EXISTS `RMLWorkorderDocumentation`');
+ $this->execute('DROP TABLE IF EXISTS `RMLWorkorderCompany`');
+ $this->execute('DROP TABLE IF EXISTS `RMLWorkorder`');
+
+ $workorder = $this->table('RMLWorkorder', ['id' => false, 'primary_key' => ['id']]);
+ $workorder->addColumn('id', 'integer', ['identity' => true, 'signed' => true])
+ ->addColumn('preorderId', 'integer', ['null' => false])
+ ->addColumn('companyId', 'integer', ['null' => true])
+ ->addColumn('status', 'string', ['limit' => 50, 'null' => false, 'default' => 'new'])
+ ->addColumn('assignmentDate', 'integer', ['null' => true])
+ ->addColumn('deadlineDate', 'integer', ['null' => true])
+ ->addColumn('appointmentDate', 'integer', ['null' => true])
+ ->addColumn('create', 'integer', ['null' => false])
+ ->addColumn('createBy', 'integer', ['null' => false])
+ ->addIndex(['preorderId'], ['name' => 'preorderId_idx'])
+ ->addIndex(['companyId'], ['name' => 'companyId_idx'])
+ ->addIndex(['status'], ['name' => 'status_idx'])
+ ->create();
+
+ $company = $this->table('RMLWorkorderCompany', ['id' => false, 'primary_key' => ['id']]);
+ $company->addColumn('id', 'integer', ['identity' => true, 'signed' => true])
+ ->addColumn('addressId', 'integer', ['null' => false])
+ ->addColumn('name', 'string', ['limit' => 255, 'null' => false])
+ ->addColumn('create', 'integer', ['null' => false])
+ ->addColumn('createBy', 'integer', ['null' => false])
+ ->create();
+
+ $documentation = $this->table('RMLWorkorderDocumentation', ['id' => false, 'primary_key' => ['id']]);
+ $documentation->addColumn('id', 'integer', ['identity' => true, 'signed' => true])
+ ->addColumn('workorderId', 'integer', ['null' => false])
+ ->addColumn('fileId', 'integer', ['null' => false])
+ ->addColumn('description', 'text', ['null' => true])
+ ->addColumn('documentType', 'string', ['limit' => 100, 'null' => false])
+ ->addColumn('create', 'integer', ['null' => false])
+ ->addColumn('createBy', 'integer', ['null' => false])
+ ->addIndex(['workorderId'], ['name' => 'workorderId_idx'])
+ ->create();
+ }
+ }
+
+ public function down(): void
+ {
+ if ($this->getEnvironment() == "thetool") {
+ $this->table('RMLWorkorderDocumentation')->drop()->save();
+ $this->table('RMLWorkorderCompany')->drop()->save();
+ $this->table('RMLWorkorder')->drop()->save();
+ }
+ }
+}
\ No newline at end of file
diff --git a/lib/mvcfronk/mfBase/mfBaseController.php b/lib/mvcfronk/mfBase/mfBaseController.php
index 6def4f80c..a9cece530 100644
--- a/lib/mvcfronk/mfBase/mfBaseController.php
+++ b/lib/mvcfronk/mfBase/mfBaseController.php
@@ -393,7 +393,7 @@ class mfBaseController
public static function sendError(string $message): void {
http_response_code(500);
- self::returnJson(['success' => false, 'message' => $message]);
+ self::returnJson(['success' => false, 'message' => $message, 'error' => $message]);
exit;
}
diff --git a/public/bundler.php b/public/bundler.php
index 80c904b65..ba195ce24 100644
--- a/public/bundler.php
+++ b/public/bundler.php
@@ -51,6 +51,7 @@ $jsFiles = [
"plugins/vue/tt-components/tt-position-manager.js",
"plugins/vue/tt-components/tt-tooltip.js",
"plugins/vue/tt-components/tt-map.js",
+ "plugins/vue/tt-components/tt-file-gallery.js",
];
diff --git a/public/index.php b/public/index.php
index 04591c2ed..2d0040714 100755
--- a/public/index.php
+++ b/public/index.php
@@ -4,6 +4,15 @@
*/
// phpinfo(); exit;
define('mfUI',"web");
+// set max mem to 4GB
+ini_set('memory_limit', '4096M');
+
+//display errors
+ini_set('display_errors', 1);
+ini_set('display_startup_errors', 1);
+ini_set('html_errors', 1);
+
+
if(file_exists("../config/config.php")) {
require("../config/config.php");
diff --git a/public/js/pages/RMLWorkorder/RMLWorkorder.js b/public/js/pages/RMLWorkorder/RMLWorkorder.js
deleted file mode 100644
index 0d7792235..000000000
--- a/public/js/pages/RMLWorkorder/RMLWorkorder.js
+++ /dev/null
@@ -1,423 +0,0 @@
-// RMLWorkorder.js
-
-// =================================================================================
-// Main Component - Switches between Admin and Company View
-// =================================================================================
-Vue.component('r-m-l-workorder', {
- template: `
-