diff --git a/Layout/default/User/Form.php b/Layout/default/User/Form.php index dfe077e05..d2aa08e8a 100644 --- a/Layout/default/User/Form.php +++ b/Layout/default/User/Form.php @@ -491,6 +491,15 @@ $siteTitle = "Benutzer"; + +
+
+ can("RMLAdmin")) ? "checked='checked'" : ""?> /> + +
+

diff --git a/application/RMLWorkorder/RMLWorkorderController.php b/application/RMLWorkorder/RMLWorkorderController.php new file mode 100644 index 000000000..22e68b388 --- /dev/null +++ b/application/RMLWorkorder/RMLWorkorderController.php @@ -0,0 +1,267 @@ + '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/RMLWorkorder/RMLWorkorderModel.php b/application/RMLWorkorder/RMLWorkorderModel.php new file mode 100644 index 000000000..e9924eeed --- /dev/null +++ b/application/RMLWorkorder/RMLWorkorderModel.php @@ -0,0 +1,59 @@ + $greenDate"; + break; + default: + return []; + } + + $sql = "SELECT * FROM $table $whereClause ORDER BY deadlineDate ASC"; + $result = $db->query($sql); + $orders = []; + while ($row = $result->fetch_assoc()) { + $orders[] = new self($row); + } + return $orders; + } + +} \ No newline at end of file diff --git a/application/RMLWorkorderCompany/RMLWorkorderCompanyModel.php b/application/RMLWorkorderCompany/RMLWorkorderCompanyModel.php new file mode 100644 index 000000000..a608e38c8 --- /dev/null +++ b/application/RMLWorkorderCompany/RMLWorkorderCompanyModel.php @@ -0,0 +1,10 @@ +permissions->canWarehouseUser = "false"; $user->permissions->canADBExtended = "false"; $user->permissions->canAssetAdmin = "false"; + $user->permissions->canRMLAdmin = "false"; if($r->get("can") && is_array($r->can)) { foreach($r->can as $key => $can) { diff --git a/db/migrations/20250629140000_rml_workorder_create.php b/db/migrations/20250629140000_rml_workorder_create.php new file mode 100644 index 000000000..9d151b58d --- /dev/null +++ b/db/migrations/20250629140000_rml_workorder_create.php @@ -0,0 +1,66 @@ +getEnvironment() == "thetool") { + $table = $this->table("WorkerPermission"); + $table->addColumn("canRMLAdmin", "enum", ["null" => false, "values" => ['false', 'true'], "default" => "false", "after" => "canSuperexpert"]); + $table->update(); + } + + if ($this->getEnvironment() == "addressdb") { + // Create RMLWorkorderCompany table + $rmlWorkorderCompany = $this->table('RMLWorkorderCompany', ['id' => 'id', 'primary_key' => 'id']); + $rmlWorkorderCompany->addColumn('addressId', 'integer', ['null' => false, 'comment' => 'FK to the Address table, identifying the company.']) + ->addColumn('name', 'string', ['limit' => 255, 'null' => false, 'comment' => 'Cached company name for easy display.']) + ->addColumn('create', 'integer', ['null' => false]) + ->addColumn('createBy', 'integer', ['null' => false]) + ->addIndex(['addressId'], ['unique' => true, 'name' => 'addressId_unique']) + ->create(); + + // Create RMLWorkorder table + $rmlWorkorder = $this->table('RMLWorkorder', ['id' => 'id', 'primary_key' => 'id']); + $rmlWorkorder->addColumn('preorderId', 'integer', ['null' => false, 'comment' => 'FK to the Preorder that triggered this work order.']) + ->addColumn('companyId', 'integer', ['null' => true, 'comment' => 'FK to RMLWorkorderCompany, assigned by an RML admin.']) + ->addColumn('status', 'string', ['limit' => 50, 'null' => false, 'default' => 'new', 'comment' => 'Workflow status: new, assigned, scheduled, documented, completed.']) + ->addColumn('assignmentDate', 'integer', ['null' => true, 'comment' => 'Timestamp when the order was assigned.']) + ->addColumn('deadlineDate', 'integer', ['null' => true, 'comment' => 'Timestamp for the 6-week completion deadline.']) + ->addColumn('appointmentDate', 'integer', ['null' => true, 'comment' => 'The date scheduled by the company for the work.']) + ->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(); + + // Create RMLWorkorderDocumentation table + $rmlWorkorderDocumentation = $this->table('RMLWorkorderDocumentation', ['id' => 'id', 'primary_key' => 'id']); + $rmlWorkorderDocumentation->addColumn('workorderId', 'integer', ['null' => false, 'comment' => 'FK to the RMLWorkorder.']) + ->addColumn('fileId', 'integer', ['null' => false, 'comment' => 'FK to the main File table after upload.']) + ->addColumn('description', 'text', ['null' => true, 'comment' => 'User-provided description for the file.']) + ->addColumn('documentType', 'string', ['limit' => 100, 'null' => false, 'comment' => 'Categorizes the upload, e.g., photo_before, measurement_protocol.']) + ->addColumn('create', 'integer', ['null' => false]) + ->addColumn('createBy', 'integer', ['null' => false]) + ->addIndex(['workorderId'], ['name' => 'workorderId_idx']) + ->create(); + } + } + + public function down(): void { + if ($this->getEnvironment() == "thetool") { + $table = $this->table("WorkerPermission"); + $table->removeColumn("canRMLAdmin"); + $table->save(); + } + + if ($this->getEnvironment() == "addressdb") { + // Drop tables in reverse order of creation (or dependency) + $this->table('RMLWorkorderDocumentation')->drop(); + $this->table('RMLWorkorder')->drop(); + $this->table('RMLWorkorderCompany')->drop(); + } + } +} diff --git a/lib/TTCrudBaseModel/TTCrudBaseModel.php b/lib/TTCrudBaseModel/TTCrudBaseModel.php index d02243fb3..906d5d0c8 100644 --- a/lib/TTCrudBaseModel/TTCrudBaseModel.php +++ b/lib/TTCrudBaseModel/TTCrudBaseModel.php @@ -203,4 +203,26 @@ class TTCrudBaseModel { return $db->affected_rows; } + public static function getFirst($filter = [], $order = ["key" => null]): ?TTCrudBaseModel { + $db = self::getDB(); + $table = self::getFullyQualifiedTable(); + $filter = self::getSQLFilter($filter); + $sql = "SELECT * FROM $table $filter"; + + if ($order['key'] !== null) { + $sql .= " ORDER BY `" . $order['key'] . "` " . $order['order']; + } else { + $sql .= " ORDER BY `id` ASC"; + } + + $sql .= " LIMIT 1"; + $result = $db->query($sql); + + if ($result->num_rows === 0) { + return null; + } + + return new static($result->fetch_assoc()); + } + } \ No newline at end of file diff --git a/public/js/pages/RMLWorkorder/RMLWorkorder.js b/public/js/pages/RMLWorkorder/RMLWorkorder.js new file mode 100644 index 000000000..0d7792235 --- /dev/null +++ b/public/js/pages/RMLWorkorder/RMLWorkorder.js @@ -0,0 +1,423 @@ +// RMLWorkorder.js + +// ================================================================================= +// Main Component - Switches between Admin and Company View +// ================================================================================= +Vue.component('r-m-l-workorder', { + template: ` +
+ + +
+ `, + data() { return { window: window } } +}); + + +// ================================================================================= +// RML Admin View +// ================================================================================= +Vue.component('rml-workorder-admin-view', { + template: ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + `, + data() { + return { + assignModalWorkorderId: null, + docsModalWorkorderId: null, + crudConfig: { + ...window.TT_CONFIG.CRUD_CONFIG, + additionalActions: [ + { + "key": "assign", + "title": "Firma zuweisen", + "class": "fas fa-user-plus text-primary", + "condition": (row) => row.status === 'new', + }, + { + "key": "view_docs", + "title": "Dokumentation ansehen", + "class": "fas fa-folder-open text-info", + "condition": (row) => ['documented', 'completed'].includes(row.status), + }, + ] + } + } + }, + methods: { + getStatusColumn(status) { + const column = this.crudConfig.columns.find(c => c.key === 'status'); + return column.table.filterOptions.find(opt => opt.value === status) || {}; + }, + formatDate(timestamp) { + if (!timestamp) return '–'; + return window.moment.unix(timestamp).format('DD.MM.YYYY'); + } + } +}); + +// ================================================================================= +// RML Company View +// ================================================================================= +Vue.component('rml-workorder-company-view', { + template: ` + + + + + + + + + + + + + + + + `, + data() { + return { + scheduleModalWorkorderId: null, + documentModalWorkorder: null, + crudConfig: { + ...window.TT_CONFIG.CRUD_CONFIG, + additionalActions: [ + { + "key": "schedule", + "title": "Termin festlegen", + "class": "fas fa-calendar-plus text-primary", + "condition": (row) => row.status === 'assigned', + }, + { + "key": "document", + "title": "Dokumentieren & Abschließen", + "class": "fas fa-camera text-success", + "condition": (row) => ['assigned', 'scheduled', 'documented'].includes(row.status), + }, + ] + } + } + }, + methods: { + getStatusColumn(status) { + const column = this.crudConfig.columns.find(c => c.key === 'status'); + return column.table.filterOptions.find(opt => opt.value === status) || {}; + }, + formatDate(timestamp) { + if (!timestamp) return '–'; + return window.moment.unix(timestamp).format('DD.MM.YYYY'); + } + } +}); + + +// ================================================================================= +// Modals and Helper Components +// ================================================================================= + +// Traffic Light Component +Vue.component('traffic-light', { + props: ['deadline', 'status'], + computed: { + lightColor() { + if (this.status === 'completed') return '#cccccc'; // Grey for completed + const now = moment(); + const deadlineDate = moment.unix(this.deadline); + if (!deadlineDate.isValid()) return '#cccccc'; // Grey for invalid date + + if (deadlineDate.isBefore(now)) return '#dc3545'; // Red for overdue + if (deadlineDate.isBefore(now.clone().add(1, 'weeks'))) return '#dc3545'; // Red + if (deadlineDate.isBefore(now.clone().add(3, 'weeks'))) return '#ffc107'; // Yellow + return '#28a745'; // Green + } + }, + template: `` +}); + +// Modal for RML Admin to assign a company +Vue.component('assign-company-modal', { + props: ['workorderId'], + template: ` + + + + `, + data() { return { companies: [], selectedCompanyId: null } }, + async mounted() { + const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorder/getCompanies`); + this.companies = response.data; + }, + methods: { + async submit() { + if (!this.selectedCompanyId) return window.notify('error', 'Bitte eine Firma auswählen.'); + const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorder/assignWorkorder`, { + workorderId: this.workorderId, + companyId: this.selectedCompanyId + }); + if(response.data.success) { + window.notify('success', response.data.message); + this.$emit('close'); + } else { + window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten.'); + } + } + } +}); + +// Modal for Company to schedule an appointment +Vue.component('schedule-appointment-modal', { + props: ['workorderId'], + template: ` + + + + `, + data() { return { appointmentDate: null } }, + methods: { + async submit() { + if (!this.appointmentDate) return window.notify('error', 'Bitte ein Datum auswählen.'); + const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorder/scheduleAppointment`, { + workorderId: this.workorderId, + appointmentDate: this.appointmentDate + }); + if(response.data.success) { + window.notify('success', response.data.message); + this.$emit('close'); + } else { + window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten.'); + } + } + } +}); + + +// Documentation Upload Modal for Companies +Vue.component('documentation-modal', { + props: ['workorder'], + template: ` + +
+
Benötigte Dokumente
+ +
+ +
+
+
Neues Dokument hochladen
+ + +
+ +
+ +
+
+ +
+
+ + + + +
+ `, + data() { + return { + uploading: false, + viewerKey: 0, + uploadedFiles: [], + uploadData: { + file: null, + documentType: 'photo_before', + description: '' + }, + requiredDocTypes: [ + { value: 'photo_before', text: 'Foto: Zustand vorher' }, + { value: 'photo_during', text: 'Foto: Während der Arbeit' }, + { value: 'photo_after', text: 'Foto: Zustand nachher' }, + { value: 'measurement_protocol', text: 'Messprotokoll (z.B. OTDR)' }, + { value: 'customer_signature', text: 'Unterschriebenes Arbeitsprotokoll' }, + ] + } + }, + computed: { + canComplete() { + // Check if at least one of each required document type is uploaded. + return this.requiredDocTypes.every(docType => this.isUploaded(docType.value)); + } + }, + methods: { + isUploaded(docType) { + return this.uploadedFiles.some(file => file.documentType === docType); + }, + handleFileUpload(event) { + this.uploadData.file = event.target.files[0]; + }, + async uploadFile() { + if(!this.uploadData.file) return window.notify('error', 'Bitte eine Datei auswählen.'); + this.uploading = true; + + const formData = new FormData(); + formData.append('file', this.uploadData.file); + formData.append('workorderId', this.workorder.id); + formData.append('documentType', this.uploadData.documentType); + formData.append('description', this.uploadData.description); + + const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorder/uploadDocumentation`, formData); + if(response.data.success) { + window.notify('success', `Datei "${response.data.fileName}" wurde hochgeladen.`); + this.$refs.fileInput.value = ''; // Clear file input + this.uploadData.file = null; + this.uploadData.description = ''; + this.viewerKey++; // Refresh the viewer + } else { + window.notify('error', response.data.error || 'Upload fehlgeschlagen.'); + } + this.uploading = false; + }, + async completeWorkorder() { + if(!confirm('Möchten Sie diesen Auftrag wirklich abschließen? Diese Aktion kann nicht rückgängig gemacht werden.')) return; + const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorder/completeWorkorder`, { workorderId: this.workorder.id }); + if(response.data.success) { + window.notify('success', response.data.message); + this.$emit('close'); + } else { + window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten.'); + } + } + }, + async mounted() { + const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorder/getDocumentation`, { params: { workorderId: this.workorder.id }}); + this.uploadedFiles = response.data; + } +}); + + +// Read-only viewer for documentation, used by both Admins and Companies +Vue.component('documentation-viewer-modal', { + props: ['workorderId'], + template: ` +
+
+
Hochgeladene Dokumente
+
+
+ +
+
+ Keine Dokumente vorhanden. +
+ +
+ `, + data() { + return { loading: false, docs: [] } + }, + methods: { + async fetchDocs() { + this.loading = true; + const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorder/getDocumentation`, { params: { workorderId: this.workorderId }}); + this.docs = response.data; + this.loading = false; + }, + formatDate(timestamp) { + return window.moment.unix(timestamp).format('DD.MM.YYYY HH:mm'); + }, + getDocTypeText(type) { + const types = [ + { value: 'photo_before', text: 'Foto: Zustand vorher' }, + { value: 'photo_during', text: 'Foto: Während der Arbeit' }, + { value: 'photo_after', text: 'Foto: Zustand nachher' }, + { value: 'measurement_protocol', text: 'Messprotokoll (z.B. OTDR)' }, + { value: 'customer_signature', text: 'Unterschriebenes Arbeitsprotokoll' }, + ]; + return types.find(t => t.value === type)?.text || type; + } + }, + mounted() { + this.fetchDocs(); + } +}); \ No newline at end of file diff --git a/public/js/pages/RMLWorkorderCompanyDashboardView/RMLWorkorderCompanyDashboardView.js b/public/js/pages/RMLWorkorderCompanyDashboardView/RMLWorkorderCompanyDashboardView.js new file mode 100644 index 000000000..47f09d012 --- /dev/null +++ b/public/js/pages/RMLWorkorderCompanyDashboardView/RMLWorkorderCompanyDashboardView.js @@ -0,0 +1,53 @@ +// RMLWorkorderCompanyDashboardView.js +// This would be a separate file and view. + +Vue.component('rml-workorder-company-dashboard', { + template: ` +
+
+
+
+
+

Meine offenen Aufträge

+

{{ stats.assigned || 0 }}

+
+
+
+
+
+
+

Meine dringenden Aufträge

+

{{ stats.urgent || 0 }}

+
+
+
+
+
+
+

Meine terminierten Aufträge

+

{{ stats.scheduled || 0 }}

+
+
+
+
+ +
+
+ + + +
+
+
+ `, + data() { + return { + stats: {} + } + }, + async mounted() { + // You would create a new controller action e.g., /RMLWorkorder/getCompanyDashboardStats + // const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorder/getCompanyDashboardStats`); + // this.stats = response.data; + } +}) \ No newline at end of file diff --git a/public/js/pages/RMLWorkorderDashboardView/RMLWorkorderDashboardView.js b/public/js/pages/RMLWorkorderDashboardView/RMLWorkorderDashboardView.js new file mode 100644 index 000000000..15cd84e01 --- /dev/null +++ b/public/js/pages/RMLWorkorderDashboardView/RMLWorkorderDashboardView.js @@ -0,0 +1,61 @@ +// RMLWorkorderAdminDashboardView.js +// This would be a separate file and view. + +Vue.component('rml-workorder-admin-dashboard', { + template: ` +
+
+
+
+
+

Neue Aufträge

+

{{ stats.new || 0 }}

+
+
+
+
+
+
+

In Arbeit

+

{{ stats.in_progress || 0 }}

+
+
+
+
+
+
+

Überfällig

+

{{ stats.overdue || 0 }}

+
+
+
+
+
+
+

Abgeschlossen (30T)

+

{{ stats.completed_30d || 0 }}

+
+
+
+
+ +
+
+ + + +
+
+
+ `, + data() { + return { + stats: {} + } + }, + async mounted() { + // You would create a new controller action e.g., /RMLWorkorder/getDashboardStats + // const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorder/getDashboardStats`); + // this.stats = response.data; + } +}) \ No newline at end of file