added new features

This commit is contained in:
Luca Haid
2025-08-26 09:57:56 +02:00
parent 8628e27b8e
commit 075aaaef5f
8 changed files with 186 additions and 74 deletions

View File

@@ -26,6 +26,25 @@ class RMLWorkorderAdminController extends TTCrud
['key' => 'deadlineDate', 'text' => 'Deadline', 'modal' => false, 'table' => ['filter' => 'date']],
];
private function getStatusText(string $statusKey): string {
$statusColumn = null;
foreach ($this->columns as $column) {
if ($column['key'] === 'status') {
$statusColumn = $column;
break;
}
}
if ($statusColumn) {
foreach ($statusColumn['table']['filterOptions'] as $option) {
if ($option['value'] === $statusKey) {
return $option['text'];
}
}
}
return ucfirst(str_replace('_', ' ', $statusKey)); // Fallback
}
protected function indexAction()
{
$campaigns = Helper::getPreorderCampaignFromUser($this->user, true);
@@ -105,20 +124,52 @@ class RMLWorkorderAdminController extends TTCrud
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']);
$docs = RMLWorkorderDocumentationModel::getAll(['workorderId' => $this->request->workorderId], null, 0, ['key' => 'create', 'order' => 'ASC']);
$journals = RMLWorkorderJournalModel::getAll(['workorderId' => $this->request->workorderId], null, 0, ['key' => 'create', 'order' => 'DESC']);
$users = UserModel::search();
$translationMap = [
'photo_hup_mounted' => 'Foto_montierter_HÜP',
'photo_hup_open' => 'Foto_offener_HÜP',
'photo_splice_cassette_hup' => 'Foto_Spleißkassette_HÜP',
'photo_splice_cassette_fcp' => 'Foto_Spleißkassette_FCP',
'photo_hup_closed_stickers' => 'Foto_geschlossener_HÜP_mit_Aufklebern',
'photo_fcp_labeled' => 'Foto_FCP_beschriftet',
'photo_patch_position_osp' => 'Foto_Patch-Position_OSP-Seite',
'photo_patch_position_anb' => 'Foto_Patch-Position_ANB-Seite',
'measurement_protocol_otdr' => 'ODTR_Messung',
'other' => 'Sonstiges_Dokument'
];
$responseDocs = [];
$typeCounts = [];
foreach($docs as $doc) {
$file = new File($doc->fileId);
$doc->fileName = $file->orig_filename ?? $file->filename;
$doc->userName = UserModel::getOne($doc->createBy)->name ?? 'Unbekannt';
$doc->mimetype = $file->mimetype ?? 'application/octet-stream';
$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',
];
}
foreach($journals as $journal) {
$journal->createByName = UserModel::getOne($journal->createBy)->name ?? 'Unbekannt';
}
self::returnJson(['docs' => $docs, 'journals' => $journals]);
self::returnJson(['docs' => $responseDocs, 'journals' => $journals]);
}
private function assignSingleWorkorder($workorderId, $companyId, $deadline, $userId) {
@@ -200,9 +251,9 @@ class RMLWorkorderAdminController extends TTCrud
RMLWorkorderJournalModel::create([
'workorderId' => $workorder->id,
'text' => $post['text'],
'text' => "Korrektur angefordert. Grund: " . $post['text'],
'fileIds' => !empty($post['fileIds']) ? json_encode($post['fileIds']) : null,
'statusChange' => "$oldStatus -> correction_requested",
'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('correction_requested'),
'create' => time(),
'createBy' => $this->user->id,
]);
@@ -283,7 +334,7 @@ class RMLWorkorderAdminController extends TTCrud
RMLWorkorderJournalModel::create([
'workorderId' => $workorder->id,
'text' => 'Dokumentation wurde akzeptiert und der Auftrag abgeschlossen.',
'statusChange' => "$oldStatus -> completed",
'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('completed'),
'create' => time(),
'createBy' => $this->user->id,
]);
@@ -292,11 +343,6 @@ class RMLWorkorderAdminController extends TTCrud
}
protected function setToProblemSolvedAction() {
// const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/setToProblemSolved`, {
// workorderId: row.id,
// text: text
// });
$post = json_decode(file_get_contents('php://input'), true);
if (empty($post['workorderId']) || empty($post['text'])) {
@@ -317,13 +363,10 @@ class RMLWorkorderAdminController extends TTCrud
$workorder->status = 'problem_solved';
RMLWorkorderModel::update((array)$workorder);
$oldStatusText = $oldStatus === 'intervention_required' ? 'Eingriff benötigt' : $oldStatus;
$problem_solved = 'Problem gelöst';
RMLWorkorderJournalModel::create([
'workorderId' => $workorder->id,
'text' => $post['text'],
'statusChange' => "$oldStatusText -> $problem_solved",
'text' => "Problem behoben: " . $post['text'],
'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('problem_solved'),
'create' => time(),
'createBy' => $this->user->id,
]);

View File

@@ -26,6 +26,25 @@ class RMLWorkorderCompanyController extends TTCrud
protected array $additionalJSVariables = ['COMPANY_ID' => '0'];
private function getStatusText(string $statusKey): string {
$statusColumn = null;
foreach ($this->columns as $column) {
if ($column['key'] === 'status') {
$statusColumn = $column;
break;
}
}
if ($statusColumn) {
foreach ($statusColumn['table']['filterOptions'] as $option) {
if ($option['value'] === $statusKey) {
return $option['text'];
}
}
}
return ucfirst(str_replace('_', ' ', $statusKey)); // Fallback
}
protected function prepareCrudConfig() {
$company = RMLWorkorderCompanyModel::getFirst(['addressId' => $this->user->address_id]);
if ($company) {
@@ -197,7 +216,7 @@ class RMLWorkorderCompanyController extends TTCrud
RMLWorkorderJournalModel::create([
'workorderId' => $workorder->id,
'text' => "Eingriff benötigt: " . $post['journalText'],
'statusChange' => "$oldStatus -> intervention_required",
'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('intervention_required'),
'create' => time(),
'createBy' => $this->user->id,
]);
@@ -205,7 +224,6 @@ class RMLWorkorderCompanyController extends TTCrud
self::returnJson(['success' => true, 'message' => 'Eingriff wurde angefordert.']);
}
protected function uploadDocumentationAction()
{
if (empty($_FILES['files']) || empty($_POST['workorderId'])) {
@@ -271,20 +289,21 @@ class RMLWorkorderCompanyController extends TTCrud
$translationMap = [
'photo_hup_mounted' => 'Foto_montierter_HÜP',
'photo_hup_open' => 'Foto_offener_HÜP',
'photo_splice_cassette' => 'Foto_Spleißkassette',
'photo_splice_cassette_hup' => 'Foto_Spleißkassette_HÜP',
'photo_splice_cassette_fcp' => 'Foto_Spleißkassette_FCP',
'photo_hup_closed_stickers' => 'Foto_geschlossener_HÜP_mit_Aufklebern',
'photo_fcp_labeled' => 'Foto_FCP_beschriftet',
'photo_patch_position_osp' => 'Foto_Patch-Position_OSP-Seite',
'photo_patch_position_anb' => 'Foto_Patch-Position_ANB-Seite',
'measurement_protocol_otdr' => 'ODTR_Messung',
'other' => 'Sonstiges_Dokument'
];
foreach($docs as $doc) {
$file = new File($doc->fileId);
$documentTypeKey = $doc->documentType;
if (!isset($typeCounts[$documentTypeKey])) {
$typeCounts[$documentTypeKey] = 1;
} else {
$typeCounts[$documentTypeKey]++;
}
$typeCounts[$documentTypeKey] = ($typeCounts[$documentTypeKey] ?? 0) + 1;
$originalFilename = $file->orig_filename ?? $file->filename;
$extension = pathinfo($originalFilename, PATHINFO_EXTENSION);
@@ -295,6 +314,7 @@ class RMLWorkorderCompanyController extends TTCrud
'id' => $doc->id,
'fileId' => $doc->fileId,
'fileName' => $newFilename,
'description' => $doc->description,
'documentType' => $documentTypeKey,
'mimetype' => $file->mimetype,
];

View File

@@ -1,33 +1,40 @@
/*
* CSS for Workorder Table Row Highlighting (Balanced Colors)
* CSS for Workorder Table Row Highlighting
*/
/* 🔴 Urgent: Deadline passed or less than 1 week away */
/* Urgent: Deadline passed or less than 1 week away */
.table-hover .tt-rml-workorder-urgent:hover,
.tt-rml-workorder-urgent {
background-color: #f8d7da !important; /* Balanced Red */
background-color: #fbe9e7 !important; /* Soft Red */
}
/* 🟠 High Priority: Deadline less than 2 weeks away */
.table-hover .tt-rml-workorder-high:hover,
.tt-rml-workorder-high {
background-color: #ffd5a1 !important; /* Balanced Orange */
}
/* 🟡 Medium: Deadline less than 3 weeks away */
/* Medium: Deadline less than 3 weeks away */
.table-hover .tt-rml-workorder-medium:hover,
.tt-rml-workorder-medium {
background-color: #fff3cd !important; /* Balanced Yellow */
background-color: #fff8e1 !important; /* Soft Yellow */
}
/* 🟢 On Track: Deadline more than 3 weeks away */
/* On Track: Deadline more than 3 weeks away */
.table-hover .tt-rml-workorder-ontrack:hover,
.tt-rml-workorder-ontrack {
background-color: #d4edda !important; /* Balanced Green */
background-color: #e8f5e9 !important; /* Soft Green */
}
/* Irrelevant: No deadline or status makes it not applicable */
/* Irrelevant: No deadline or status makes it not applicable */
.table-hover .tt-rml-workorder-irrelevant:hover,
.tt-rml-workorder-irrelevant {
background-color: #e9ecef !important; /* Balanced Grey */
}
background-color: #fafafa !important; /* Very light grey */
}
.table-hover .tt-rml-workorder-high:hover,
.tt-rml-workorder-high {
background-color: #f8d7da !important; /* A slightly more intense red for high priority issues */
}
.tt-file-gallery-item.border.border-danger {
border: 4px solid #f1556c!important;
}
.RMLWorkorderCompany-table .modal-body {
overflow-y: hidden;
}

View File

@@ -55,7 +55,7 @@ Vue.component('r-m-l-workorder-admin', {
additional-class="btn-link btn-sm p-0"
title="Auftrag auf Problem behoben setzen"
/>
</template>
<template v-slot:companyname="{ row }">
@@ -297,10 +297,10 @@ Vue.component('r-m-l-workorder-admin', {
}
},
async setToProblemSolved(row) {
// add a browser dialog to add some text
const text = prompt('Bitte geben Sie einen kurzen Text für den Eintrag ein:', '');
const text = prompt('Bitte geben Sie einen kurzen Text für den Journaleintrag ein:', '');
if (!text) {
if (text === null) return; // User cancelled
if (!text.trim()) {
window.notify('error', 'Bitte geben Sie einen Text ein.');
return;
}
@@ -364,7 +364,14 @@ Vue.component('rml-documentation-viewer-admin', {
<div class="card mb-3">
<div class="card-header"><h5><i class="fas fa-check-circle text-success mr-2"></i>Dokumentation akzeptieren</h5></div>
<div class="card-body">
<p class="small text-muted">Wenn die Dokumentation korrekt ist, können Sie sie hier akzeptieren.</p>
<p class="small text-muted">Prüfen Sie, ob alle erforderlichen Dokumente vorhanden und korrekt sind.</p>
<ul class="list-unstyled">
<li v-for="docType in requiredDocTypes" :key="docType.value" class="mb-2 d-flex align-items-center small">
<i :class="isUploaded(docType.value) ? 'fas fa-check-circle text-success' : 'far fa-circle text-muted'" class="fa-fw mr-2"></i>
<span>{{ docType.text }}</span>
</li>
</ul>
<hr>
<tt-button text="Dokumentation akzeptieren"
@click="$emit('accept-documentation', workorderId)"
additional-class="btn-success float-right"
@@ -409,9 +416,23 @@ Vue.component('rml-documentation-viewer-admin', {
correctionText: '',
newJournalMessage: '',
addingJournalEntry: false,
requiredDocTypes: [
{ value: 'photo_hup_mounted', text: 'Foto vom montierten HÜP' },
{ value: 'photo_hup_open', text: 'Foto von dem offenen HÜP' },
{ value: 'photo_splice_cassette_hup', text: 'Foto der Spleißkassette HÜP' },
{ value: 'photo_splice_cassette_fcp', text: 'Foto der Spleißkassette - FCP' },
{ value: 'photo_hup_closed_stickers', text: 'Foto vom geschlossenen HÜP mit allen Aufklebern' },
{ value: 'photo_fcp_labeled', text: 'Foto vom FCP beschriftet' },
{ value: 'photo_patch_position_osp', text: 'Foto der Patch-Position - OSP-Seite' },
{ value: 'photo_patch_position_anb', text: 'Foto der Patch-Position - ANB-Seite' },
{ value: 'measurement_protocol_otdr', text: 'ODTR Messung (1310nm & 1550nm)' },
]
}
},
methods: {
isUploaded(docType) {
return this.docs.some(doc => doc.documentType === docType);
},
async fetchData() {
this.loading = true;
try {

View File

@@ -25,6 +25,12 @@
.tt-rml-workorder-irrelevant {
background-color: #fafafa !important; /* Very light grey */
}
.table-hover .tt-rml-workorder-high:hover,
.tt-rml-workorder-high {
background-color: #f8d7da !important; /* A slightly more intense red for high priority issues */
}
.tt-file-gallery-item.border.border-danger {
border: 4px solid #f1556c!important;
}

View File

@@ -266,7 +266,7 @@ Vue.component('documentation-manager', {
<div class="card mb-3" v-if="workorder.status !== 'documented' && workorder.status !== 'completed'">
<div class="card-body">
<h5 class="card-title">Neues Dokument hochladen</h5>
<tt-select label="Dokumententyp" :options="requiredDocTypes" v-model="uploadData.documentType" sm row />
<tt-select label="Dokumententyp" :options="allDocTypes" v-model="uploadData.documentType" sm row />
<tt-input label="Beschreibung" v-model="uploadData.description" sm row placeholder="Optional"/>
<div class="form-group row">
<label class="col-form-label col-sm-4 col-form-label-sm">Dateien</label>
@@ -288,7 +288,7 @@ Vue.component('documentation-manager', {
<template v-slot:file-edit="{ file }">
<tt-select
label="Dokumententyp"
:options="requiredDocTypes"
:options="allDocTypes"
v-model="file.documentType"
sm
/>
@@ -334,15 +334,23 @@ Vue.component('documentation-manager', {
requiredDocTypes: [
{ value: 'photo_hup_mounted', text: 'Foto vom montierten HÜP' },
{ value: 'photo_hup_open', text: 'Foto von dem offenen HÜP' },
{ value: 'photo_splice_cassette', text: 'Foto der Spleißkassette' },
{ value: 'photo_splice_cassette_hup', text: 'Foto der Spleißkassette HÜP' },
{ value: 'photo_splice_cassette_fcp', text: 'Foto der Spleißkassette - FCP' },
{ value: 'photo_hup_closed_stickers', text: 'Foto vom geschlossenen HÜP mit allen Aufklebern' },
{ value: 'photo_fcp_labeled', text: 'Foto vom FCP beschriftet' },
{ value: 'photo_patch_position', text: 'Foto der Patch-Position' },
{ value: 'photo_patch_position_osp', text: 'Foto der Patch-Position - OSP-Seite' },
{ value: 'photo_patch_position_anb', text: 'Foto der Patch-Position - ANB-Seite' },
{ value: 'measurement_protocol_otdr', text: 'ODTR Messung (1310nm & 1550nm)' },
]
}
},
computed: {
allDocTypes() {
return [
...this.requiredDocTypes,
{ value: 'other', text: 'Sonstiges Dokument (optional)' }
];
},
canComplete() {
return this.requiredDocTypes.every(docType => this.isUploaded(docType.value));
},

View File

@@ -193,6 +193,26 @@
margin: 20px 0; /* Add some vertical margin for spacing */
}
.tt-file-gallery-filename {
padding: 4px 8px 0;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: flex;
justify-content: space-between;
align-items: center;
}
.tt-file-gallery-description {
padding: 0 8px 4px;
font-size: 0.75rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #6c757d; /* Bootstrap's text-muted color */
}
.tt-fullscreen-nav-btn {
position: absolute;

View File

@@ -40,7 +40,6 @@ Vue.component('tt-fullscreen-viewer', {
return this.currentItem?.fileId ? `/File/download?id=${this.currentItem.fileId}` : '#';
}
},
// NEW: Watcher to trigger PDF.js rendering when item changes in standalone mode
watch: {
currentItem(newItem) {
if (this.isPdf(newItem) && this.isStandalone) {
@@ -106,16 +105,12 @@ Vue.component('tt-fullscreen-viewer', {
onTouchStart(e) {},
onTouchMove(e) {},
onTouchEnd(e) {},
// NEW: Method to dynamically load the PDF.js library
loadPdfJsScript() {
return new Promise((resolve, reject) => {
if (document.getElementById('pdfjs-script')) {
// Script tag exists, check if library is loaded
if (window.pdfjsLib) {
resolve();
} else {
// Tag exists but script not loaded yet, wait for it
document.getElementById('pdfjs-script').addEventListener('load', () => resolve());
document.getElementById('pdfjs-script').addEventListener('error', (e) => reject(e));
}
@@ -134,8 +129,6 @@ Vue.component('tt-fullscreen-viewer', {
document.head.appendChild(script);
});
},
// NEW: Method to render PDF onto a canvas using PDF.js
async renderPdfWithJs(url) {
if (!url) return;
this.isLoading = true;
@@ -146,12 +139,11 @@ Vue.component('tt-fullscreen-viewer', {
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@5.4.54/build/pdf.worker.min.mjs';
const pdf = await pdfjsLib.getDocument(url).promise;
// For simplicity, we render the first page. Add pagination controls as needed.
const page = await pdf.getPage(1);
const canvas = this.$refs.pdfCanvas;
if (!canvas) return; // Exit if canvas is not available
const container = canvas.parentElement; // Get the container to calculate size from
if (!canvas) return;
const container = canvas.parentElement;
if (!container) return;
const context = canvas.getContext('2d');
@@ -170,17 +162,14 @@ Vue.component('tt-fullscreen-viewer', {
} catch (error) {
console.error('Error rendering PDF with PDF.js:', error);
// Optionally show an error message to the user
} finally {
this.onContentLoad(); // Use existing method to hide loader
this.onContentLoad();
}
},
},
created() {
this.currentItem = this.item;
this.currentImageIndex = this.initialIndex;
// NEW: Check for standalone mode on component creation
if (typeof window !== 'undefined' && window.matchMedia) {
this.isStandalone = window.matchMedia('(display-mode: standalone)').matches;
}
@@ -189,8 +178,6 @@ Vue.component('tt-fullscreen-viewer', {
document.body.style.overflow = 'hidden';
this.$nextTick(() => {
this.$refs.viewer?.focus();
// NEW: Trigger initial PDF render on mount if in standalone mode
if (this.isPdf(this.currentItem) && this.isStandalone) {
this.renderPdfWithJs(this.contentSrc);
}
@@ -199,7 +186,6 @@ Vue.component('tt-fullscreen-viewer', {
beforeDestroy() {
document.body.style.overflow = '';
},
//language=Vue
template: `
<div class="tt-fullscreen-overlay" @click.self="closeViewer" @keydown="handleKeyDown" tabindex="-1" ref="viewer">
<div class="tt-fullscreen-toolbar">
@@ -342,12 +328,13 @@ Vue.component('tt-file-gallery', {
</div>
</div>
<div class="tt-file-gallery-filename" :title="file.fileName">
<span>{{ file.fileName }}</span>
<i v-if="editMode" class="fas fa-edit text-primary ml-1 action-icon"
@click="startEdit(file, $event)"></i>
<i v-if="deleteMode" class="fas fa-trash text-danger ml-1 action-icon"
@click="deleteFile(file, $event)"></i>
<span style="overflow: hidden; text-overflow: ellipsis;">{{ file.fileName }}</span>
<div>
<i v-if="editMode" class="fas fa-edit text-primary ml-1 action-icon" @click="startEdit(file, $event)"></i>
<i v-if="deleteMode" class="fas fa-trash text-danger ml-1 action-icon" @click="deleteFile(file, $event)"></i>
</div>
</div>
<div v-if="file.description" class="tt-file-gallery-description" :title="file.description">{{ file.description }}</div>
</div>
</div>
</div>