-
-
-
Benötigte Dokumente
-
- -
-
- {{ docType.text }}
-
-
-
-
-
- Bitte laden Sie alle benötigten Dokumente hoch, um den Auftrag abzuschließen.
-
-
- Auftrag bereits abgeschlossen.
+
+
+
+
Benötigte Dokumente
+
+ -
+
+ {{ docType.text }}
+
+
+
+
+
+ Bitte laden Sie alle benötigten Dokumente hoch, um den Auftrag abzuschließen.
+
+
+ Auftrag bereits abgeschlossen.
+
+
+
+
+
+
+
+ - Keine Einträge vorhanden.
+ -
+ {{ formatDate(log.create) }} ({{ log.createByName }}):
+
{{ log.text }}
+
+
+
+
@@ -162,15 +181,30 @@ Vue.component('documentation-manager', {
-
+
+
+
+
+
@@ -182,6 +216,9 @@ Vue.component('documentation-manager', {
uploading: false,
completing: false,
uploadedFiles: [],
+ journals: [],
+ newJournalMessage: '',
+ addingJournalEntry: false,
uploadData: {
files: [],
documentType: 'photo_before',
@@ -199,9 +236,40 @@ Vue.component('documentation-manager', {
computed: {
canComplete() {
return this.requiredDocTypes.every(docType => this.isUploaded(docType.value));
+ },
+ filesWithStatus() {
+ if (!this.journals || this.journals.length === 0) {
+ return this.uploadedFiles;
+ }
+
+ const correctionJournal = [...this.journals]
+ .sort((a, b) => b.create - a.create)
+ .find(j => j.statusChange && j.statusChange.includes('correction_requested'));
+
+ if (!correctionJournal || !correctionJournal.fileIds) {
+ return this.uploadedFiles;
+ }
+
+ try {
+ const incorrectFileIds = JSON.parse(correctionJournal.fileIds);
+ if (!Array.isArray(incorrectFileIds)) return this.uploadedFiles;
+
+ return this.uploadedFiles.map(file => {
+ if (incorrectFileIds.includes(file.id)) {
+ return { ...file, class: 'border border-danger' };
+ }
+ return file;
+ });
+ } catch (e) {
+ return this.uploadedFiles;
+ }
}
},
methods: {
+ formatDate(timestamp) {
+ if (!timestamp) return '–';
+ return window.moment.unix(timestamp).format('DD.MM.YYYY HH:mm');
+ },
async loadWorkorder() {
this.loadingWorkorder = true;
try {
@@ -218,7 +286,8 @@ Vue.component('documentation-manager', {
async fetchDocs() {
try {
const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/getDocumentation`, { params: { workorderId: this.workorderId }});
- this.uploadedFiles = response.data;
+ this.uploadedFiles = response.data.docs;
+ this.journals = response.data.journals;
} catch(e) {
window.notify('error', 'Dokumente konnten nicht geladen werden.');
}
@@ -245,8 +314,9 @@ Vue.component('documentation-manager', {
this.$refs.fileInput.value = '';
this.uploadData.files = [];
this.uploadData.description = '';
- await this.fetchDocs();
- await this.loadWorkorder(); // Reload to get updated status
+
+ this.uploadedFiles = response.data.docs;
+ this.workorder = response.data.workorder;
} else {
window.notify('error', response.data.error || 'Upload fehlgeschlagen.');
}
@@ -271,9 +341,55 @@ Vue.component('documentation-manager', {
}
this.completing = false;
},
- getDocTypeText(type) {
- const found = this.requiredDocTypes.find(t => t.value === type);
- return found ? found.text : type;
+ async deleteDocumentation(file) {
+ try {
+ const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/deleteDocumentation`, { id: file.id });
+ if (response.data.success) {
+ window.notify('success', response.data.message);
+ this.uploadedFiles = response.data.docs;
+ } else {
+ window.notify('error', response.data.message || 'Löschen fehlgeschlagen.');
+ }
+ } catch (e) {
+ window.notify('error', 'Netzwerkfehler beim Löschen.');
+ }
+ },
+ async updateDocumentation(file) {
+ try {
+ const payload = { id: file.id, documentType: file.documentType };
+ const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/updateDocumentation`, payload);
+ if (response.data.success) {
+ window.notify('success', response.data.message);
+ this.uploadedFiles = response.data.docs;
+ } else {
+ window.notify('error', response.data.message || 'Update fehlgeschlagen.');
+ }
+ } catch (e) {
+ window.notify('error', 'Netzwerkfehler beim Aktualisieren.');
+ }
+ },
+ async addJournalEntry() {
+ if (!this.newJournalMessage.trim()) {
+ return window.notify('error', 'Bitte geben Sie eine Nachricht ein.');
+ }
+ this.addingJournalEntry = true;
+ try {
+ const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/addJournal`, {
+ workorderId: this.workorderId,
+ text: this.newJournalMessage
+ });
+ if (response.data.success) {
+ window.notify('success', response.data.message || 'Journal-Eintrag hinzugefügt.');
+ this.newJournalMessage = '';
+ this.journals = response.data.journals;
+ } else {
+ window.notify('error', response.data.message || 'Eintrag konnte nicht gespeichert werden.');
+ }
+ } catch (e) {
+ window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.');
+ } finally {
+ this.addingJournalEntry = false;
+ }
}
},
async mounted() {
diff --git a/public/plugins/vue/tt-components/css/tt-file-gallery.css b/public/plugins/vue/tt-components/css/tt-file-gallery.css
index d0632e44c..5b82055a7 100644
--- a/public/plugins/vue/tt-components/css/tt-file-gallery.css
+++ b/public/plugins/vue/tt-components/css/tt-file-gallery.css
@@ -1,3 +1,4 @@
+/* tt-file-gallery.css */
.tt-file-gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
@@ -13,11 +14,18 @@
cursor: pointer;
}
-.tt-file-gallery-thumbnail {
+.tt-file-gallery-thumbnail-wrapper {
+ position: relative;
width: 100%;
height: 100px;
- object-fit: cover;
border-radius: 0.25rem;
+ overflow: hidden; /* Hide the scaled part of the image */
+}
+
+.tt-file-gallery-thumbnail {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
border: 1px solid #dee2e6;
transition: transform 0.2s;
}
@@ -30,8 +38,8 @@
position: absolute;
top: 0;
left: 0;
- right: 0;
- bottom: 40px; /* Adjust to not cover filename */
+ width: 100%;
+ height: 100%;
background: rgba(0, 0, 0, 0.4);
color: white;
display: flex;
@@ -43,13 +51,13 @@
border-radius: 0.25rem;
}
-.tt-file-gallery-item:hover .tt-file-gallery-overlay {
+.tt-file-gallery-item:hover .tt-file-gallery-thumbnail-wrapper .tt-file-gallery-overlay {
opacity: 1;
}
.tt-file-gallery-icon-container {
width: 100%;
- height: 100px;
+ height: 100%;
display: flex;
justify-content: center;
align-items: center;
@@ -58,18 +66,40 @@
background-color: #f8f9fa;
text-decoration: none;
color: inherit;
+ transition: transform 0.2s;
}
+.tt-file-gallery-item:hover .tt-file-gallery-icon-container {
+ transform: scale(1.05);
+}
+
+
.tt-file-gallery-filename {
font-size: 0.8rem;
margin-top: 0.5rem;
width: 100%;
+ padding: 0 4px;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ align-items: center;
+ gap: 0 0.25rem;
+ line-height: 1.2;
+}
+
+.tt-file-gallery-filename > span {
+ flex: 1 1 auto;
+ min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
- padding: 0 4px;
}
+.tt-file-gallery-filename > i {
+ flex-shrink: 0;
+}
+
+
/* --- Fullscreen Viewer Styles --- */
.tt-fullscreen-overlay {
diff --git a/public/plugins/vue/tt-components/css/tt-select.css b/public/plugins/vue/tt-components/css/tt-select.css
new file mode 100644
index 000000000..a48a64045
--- /dev/null
+++ b/public/plugins/vue/tt-components/css/tt-select.css
@@ -0,0 +1,198 @@
+/* --- tt-select.css --- */
+.tt-select-modern {
+ position: relative;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+}
+
+.tt-select-modern .form-group {
+ margin-bottom: 0;
+}
+
+.tt-select-modern .tt-select-trigger {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ padding: 0.5rem 0.75rem;
+ font-size: 1rem;
+ line-height: 1.5;
+ color: #495057;
+ background-color: #fff;
+ background-clip: padding-box;
+ border: 1px solid #ced4da;
+ border-radius: 0.25rem;
+ transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+ cursor: pointer;
+ text-align: left;
+}
+
+.tt-select-modern .tt-select-trigger.sm {
+ padding: 0.25rem 0.5rem;
+ font-size: 0.875rem;
+ border-radius: 0.2rem;
+}
+
+
+.tt-select-modern .tt-select-trigger:hover {
+ border-color: #80bdff;
+}
+
+.tt-select-modern .tt-select-trigger.open {
+ border-color: #80bdff;
+ box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
+}
+
+
+.tt-select-modern .trigger-text {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ flex-grow: 1;
+}
+
+.tt-select-modern .trigger-text.placeholder {
+ color: #6c757d;
+}
+
+.tt-select-modern .trigger-arrow {
+ margin-left: 8px;
+ transition: transform 0.2s ease;
+}
+
+.tt-select-modern .tt-select-trigger.open .trigger-arrow {
+ transform: rotate(180deg);
+}
+
+.tt-select-modern .tt-select-dropdown {
+ position: absolute;
+ top: calc(100% + 4px);
+ left: 0;
+ min-width: 100%;
+ width: max-content;
+ z-index: 1050;
+ background-color: #fff;
+ border-radius: 0.3rem;
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
+ opacity: 0;
+ transform: translateY(-10px);
+ visibility: hidden;
+ transition: opacity 0.15s ease, transform 0.15s ease;
+}
+
+.tt-select-modern .tt-select-dropdown.show {
+ opacity: 1;
+ transform: translateY(0);
+ visibility: visible;
+}
+
+.tt-select-modern .dropdown-header {
+ display: none; /* Hidden on desktop */
+}
+
+.tt-select-modern .search-input-wrapper {
+ padding: 0.5rem;
+ border-bottom: 1px solid #e9ecef;
+}
+
+.tt-select-modern .search-input {
+ width: 100%;
+ padding: 0.375rem 0.75rem;
+ border: 1px solid #ced4da;
+ border-radius: 0.25rem;
+}
+
+.tt-select-modern .options-list {
+ list-style: none;
+ padding: 0.5rem 0;
+ margin: 0;
+ max-height: 250px;
+ overflow-y: auto;
+}
+
+.tt-select-modern .option-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.5rem 1rem;
+ cursor: pointer;
+ transition: background-color 0.15s ease;
+ user-select: none;
+ white-space: nowrap;
+}
+
+.tt-select-modern .option-label {
+ display: flex;
+ align-items: center;
+}
+
+.tt-select-modern .option-item input {
+ display: none;
+}
+
+
+.tt-select-modern .option-item:hover {
+ background-color: #f8f9fa;
+}
+
+.tt-select-modern .option-item.selected {
+ background-color: #e2f0ff;
+ font-weight: 600;
+ color: #0056b3;
+}
+
+.tt-select-modern .option-checkmark {
+ color: #0056b3;
+}
+
+.tt-select-modern .no-results {
+ padding: 0.5rem 1rem;
+ color: #6c757d;
+ text-align: center;
+}
+
+/* Mobile Compliance */
+@media (max-width: 768px) {
+ .tt-select-modern .tt-select-trigger,
+ .tt-select-modern .option-item {
+ font-size: 1.1rem; /* Bigger font for readability */
+ }
+
+ .tt-select-modern .tt-select-dropdown.show {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ width: 100%;
+ height: 100%;
+ border-radius: 0;
+ display: flex;
+ flex-direction: column;
+ background-color: #f8f9fa;
+ transform: none;
+ }
+
+ .tt-select-modern .dropdown-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 1rem;
+ background-color: #fff;
+ border-bottom: 1px solid #dee2e6;
+ font-size: 1.2rem;
+ font-weight: 600;
+ }
+
+ .tt-select-modern .close-btn {
+ font-size: 1.5rem;
+ cursor: pointer;
+ line-height: 1;
+ border: none;
+ background: none;
+ }
+
+ .tt-select-modern .options-list {
+ max-height: none;
+ flex-grow: 1;
+ }
+}
\ No newline at end of file
diff --git a/public/plugins/vue/tt-components/tt-file-gallery.js b/public/plugins/vue/tt-components/tt-file-gallery.js
index 0a4e677f6..019e6ead0 100644
--- a/public/plugins/vue/tt-components/tt-file-gallery.js
+++ b/public/plugins/vue/tt-components/tt-file-gallery.js
@@ -1,13 +1,17 @@
+// tt-file-gallery.js
Vue.component('tt-file-gallery', {
props: {
- files: { type: Array, default: () => [] }
+ files: { type: Array, default: () => [] },
+ editMode: { type: Boolean, default: false },
+ deleteMode: { type: Boolean, default: false },
+ selectable: { type: Boolean, default: false },
},
data() {
return {
- fullscreenItem: null, // Holds the file being viewed
+ fullscreenItem: null,
currentImageIndex: 0,
-
- // Zoom & Pan state
+ editingFile: null,
+ selectedFiles: [],
zoom: 1,
pan: { x: 0, y: 0 },
isPanning: false,
@@ -23,7 +27,6 @@ Vue.component('tt-file-gallery', {
return this.fullscreenItem && this.isImage(this.fullscreenItem);
},
imageTransformStyle() {
- // Apply CSS transform for zoom and pan
const { x, y } = this.pan;
return {
transform: `translate(${x}px, ${y}px) scale(${this.zoom})`,
@@ -33,34 +36,33 @@ Vue.component('tt-file-gallery', {
},
fullscreenDownloadUrl() {
if (!this.fullscreenItem) return '#';
- return `/File/download?id=${this.fullscreenItem.id}`;
+ return `/File/download?id=${this.fullscreenItem.fileId}`;
}
},
methods: {
- // File type checks
- isImage(file) {
- return file.mimetype && file.mimetype.startsWith('image/');
- },
- isPdf(file) {
- return file.mimetype === 'application/pdf';
- },
-
- // Get icon for non-image/pdf files
+ isImage: file => file.mimetype && file.mimetype.startsWith('image/'),
+ isPdf: file => file.mimetype === 'application/pdf',
getFileIcon(file) {
const extension = file.fileName?.split('.').pop().toLowerCase();
switch (extension) {
- case 'doc':
- case 'docx': return 'fas fa-file-word text-primary';
- case 'xls':
- case 'xlsx': return 'fas fa-file-excel text-success';
- case 'zip':
- case 'rar': return 'fas fa-file-archive text-warning';
+ case 'doc': case 'docx': return 'fas fa-file-word text-primary';
+ case 'xls': case 'xlsx': return 'fas fa-file-excel text-success';
+ case 'zip': case 'rar': return 'fas fa-file-archive text-warning';
default: return 'fas fa-file text-secondary';
}
},
-
- // Viewer controls
+ toggleSelection(fileId) {
+ if (!this.selectable) return;
+ const index = this.selectedFiles.indexOf(fileId);
+ if (index > -1) {
+ this.selectedFiles.splice(index, 1);
+ } else {
+ this.selectedFiles.push(fileId);
+ }
+ this.$emit('selection-changed', this.selectedFiles);
+ },
openViewer(file) {
+ if(this.editingFile) return;
this.fullscreenItem = file;
if (this.isImage(file)) {
this.currentImageIndex = this.imageFiles.findIndex(img => img.id === file.id);
@@ -68,9 +70,7 @@ Vue.component('tt-file-gallery', {
this.resetZoomAndPan();
this.$nextTick(() => { this.$refs.viewer?.focus(); });
},
- closeViewer() {
- this.fullscreenItem = null;
- },
+ closeViewer() { this.fullscreenItem = null; },
navigateImage(direction) {
const newIndex = this.currentImageIndex + direction;
if (newIndex >= 0 && newIndex < this.imageFiles.length) {
@@ -79,9 +79,8 @@ Vue.component('tt-file-gallery', {
this.resetZoomAndPan();
}
},
-
- // Event handlers for keyboard and clicks
handleKeyDown(event) {
+ event.stopPropagation();
if (!this.fullscreenItem) return;
switch (event.key) {
case 'Escape': this.closeViewer(); break;
@@ -89,24 +88,16 @@ Vue.component('tt-file-gallery', {
case 'ArrowRight': this.isViewingImage && this.navigateImage(1); break;
}
},
-
- // --- Zoom and Pan Methods ---
resetZoomAndPan() {
- this.zoom = 1;
- this.pan = { x: 0, y: 0 };
- this.isPanning = false;
+ this.zoom = 1; this.pan = { x: 0, y: 0 }; this.isPanning = false;
},
-
- // Mouse Wheel Zoom
handleWheel(e) {
if (!this.isViewingImage) return;
e.preventDefault();
const scaleFactor = 0.2;
const newZoom = this.zoom - (e.deltaY > 0 ? scaleFactor : -scaleFactor);
- this.zoom = Math.max(1, Math.min(newZoom, 5)); // Clamp zoom between 1x and 5x
+ this.zoom = Math.max(1, Math.min(newZoom, 5));
},
-
- // Mouse Drag to Pan
onPanStart(e) {
if (this.zoom <= 1) return;
e.preventDefault();
@@ -119,79 +110,81 @@ Vue.component('tt-file-gallery', {
this.pan.x = e.clientX - this.panStart.x;
this.pan.y = e.clientY - this.panStart.y;
},
- onPanEnd() {
- this.isPanning = false;
- },
+ onPanEnd() { this.isPanning = false; },
+ onTouchStart(e) { /* ... touch logic ... */ },
+ onTouchMove(e) { /* ... touch logic ... */ },
+ onTouchEnd(e) { /* ... touch logic ... */ },
- // Touch Events for Mobile (Pinch-to-Zoom & Pan)
- onTouchStart(e) {
- if (this.zoom <= 1 && e.touches.length === 1) return;
- e.preventDefault();
- if (e.touches.length === 1) { // Pan
- this.isPanning = true;
- this.panStart.x = e.touches[0].clientX - this.pan.x;
- this.panStart.y = e.touches[0].clientY - this.pan.y;
- } else if (e.touches.length === 2) { // Zoom
- this.lastPinchDist = Math.hypot(
- e.touches[0].clientX - e.touches[1].clientX,
- e.touches[0].clientY - e.touches[1].clientY
- );
+ startEdit(file, event) {
+ event?.stopPropagation();
+ this.editingFile = { ...file }; // create a copy for editing
+ },
+ cancelEdit(event) {
+ console.log('Edit cancelled');
+ event?.stopPropagation();
+ this.editingFile = null;
+ },
+ saveEdit(event) {
+ event?.stopPropagation();
+ this.$emit('update-file', this.editingFile);
+ this.editingFile = null;
+ },
+ deleteFile(file, event) {
+ event?.stopPropagation();
+ if (confirm(`Sind Sie sicher, dass Sie die Datei "${file.fileName}" löschen möchten?`)) {
+ this.$emit('delete-file', file);
}
},
- onTouchMove(e) {
- if (!this.isPanning && e.touches.length !== 2) return;
- e.preventDefault();
- if (e.touches.length === 1 && this.isPanning) { // Pan
- this.pan.x = e.touches[0].clientX - this.panStart.x;
- this.pan.y = e.touches[0].clientY - this.panStart.y;
- } else if (e.touches.length === 2) { // Zoom
- const pinchDist = Math.hypot(
- e.touches[0].clientX - e.touches[1].clientX,
- e.touches[0].clientY - e.touches[1].clientY
- );
- const scaleFactor = 0.01;
- const newZoom = this.zoom + (pinchDist - this.lastPinchDist) * scaleFactor;
- this.zoom = Math.max(1, Math.min(newZoom, 5)); // Clamp zoom
- this.lastPinchDist = pinchDist;
- }
- },
- onTouchEnd(e) {
- this.isPanning = false;
- if (e.touches.length < 2) {
- this.lastPinchDist = 0;
- }
- }
},
watch: {
fullscreenItem(newItem) {
- // Prevent body scroll when viewer is open
document.body.style.overflow = newItem ? 'hidden' : '';
}
},
template: `
-
+
Keine Dokumente vorhanden.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{{ file.fileName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ file.fileName }}
+
+
+
+
+
+
+
-
-
-
![]()
+
+
-
+
-
-
-
+
+
diff --git a/public/plugins/vue/tt-components/tt-modal.js b/public/plugins/vue/tt-components/tt-modal.js
index 5d99493f9..de8eaff7d 100644
--- a/public/plugins/vue/tt-components/tt-modal.js
+++ b/public/plugins/vue/tt-components/tt-modal.js
@@ -12,7 +12,6 @@ Vue.component('tt-modal', {
if (!newVal) {
this.$emit('close')
}
- // if show now is true then focus the first input element
if (newVal) {
this.$nextTick(() => {
const input = this.$refs.modal.querySelector('input')
diff --git a/public/plugins/vue/tt-components/tt-select.js b/public/plugins/vue/tt-components/tt-select.js
index cd19e9eda..3f32bf2c4 100644
--- a/public/plugins/vue/tt-components/tt-select.js
+++ b/public/plugins/vue/tt-components/tt-select.js
@@ -1,50 +1,42 @@
Vue.component('tt-select', {
props: {
- options: { type: Array, required: true },
- label: { type: String, default: null },
- required: { type: Boolean, default: false },
- value: { type: [String, Number, Array], default: null },
- disabled: { type: Boolean, default: false },
- suffix: { type: String, default: null },
- sm: { type: Boolean, default: false },
- row: { type: Boolean, default: false },
- multiple: { type: Boolean, default: false },
- searchable: { type: Boolean, default: true }
+ options: {type: Array, required: true},
+ label: {type: String, default: null},
+ required: {type: Boolean, default: false},
+ value: {type: [String, Number, Array], default: null},
+ disabled: {type: Boolean, default: false},
+ suffix: {type: String, default: null},
+ sm: {type: Boolean, default: false},
+ row: {type: Boolean, default: false},
+ multiple: {type: Boolean, default: false},
+ searchable: {type: Boolean, default: true}
},
data() {
return {
open: false,
searchQuery: '',
- // internal model: array if multiple, else primitive
- selected: this.multiple
- ? (Array.isArray(this.value) ? [...this.value] : [])
- : this.value
+ selected: this.multiple ? (Array.isArray(this.value) ? [...this.value] : []) : this.value
};
},
watch: {
- // keep internal model in sync with external v-model
value(newVal) {
- this.selected = this.multiple
- ? (Array.isArray(newVal) ? [...newVal] : [])
- : newVal;
+ this.selected = this.multiple ? (Array.isArray(newVal) ? [...newVal] : []) : newVal;
},
-
- // Enhancement: Handle focus and scroll on open
open(isOpen) {
- if (isOpen) {
- this.$nextTick(() => {
- if (this.$refs.dropdownMenu) {
- // 2. Scroll to the top when opened
- this.$refs.dropdownMenu.scrollTop = 0;
- }
- if (this.searchable && this.$refs.searchInput) {
- // 1. Focus the input when opened
- this.$refs.searchInput.focus();
- }
- });
+ if (!isOpen) {
+ this.searchQuery = ''; // Reset search on close
+ return;
}
+ this.$nextTick(() => {
+ if (this.searchable && this.$refs.searchInput) {
+ this.$refs.searchInput.focus();
+ }
+ if (this.$refs.optionsList) {
+ this.$refs.optionsList.scrollTop = 0;
+ }
+ });
}
},
@@ -57,55 +49,42 @@ Vue.component('tt-select', {
},
computed: {
- // normalize all options to { value, text, disabled }
normalizedOptions() {
- return this.options.map(opt => {
- if (opt !== null && typeof opt === 'object') {
- return {
- value: opt.value,
- text: opt.text,
- disabled: !!opt.disabled
- };
- }
- return {
- value: opt,
- text: String(opt),
- disabled: false
- };
- });
- },
-
- // filter by searchQuery
- filteredOptions() {
- if (!this.searchable || !this.searchQuery) {
- return this.normalizedOptions;
- }
- const q = this.searchQuery.toLowerCase();
- return this.normalizedOptions.filter(o =>
- o.text.toLowerCase().includes(q)
+ return this.options.map(opt =>
+ (opt !== null && typeof opt === 'object')
+ ? {value: opt.value, text: opt.text, disabled: !!opt.disabled}
+ : {value: opt, text: String(opt), disabled: false}
);
},
- // what to show on the button
+ filteredOptions() {
+ if (!this.searchable || !this.searchQuery) return this.normalizedOptions;
+ const q = this.searchQuery.toLowerCase();
+ return this.normalizedOptions.filter(o => o.text.toLowerCase().includes(q));
+ },
+
displayText() {
- if (this.multiple) {
- const arr = Array.isArray(this.selected) ? this.selected : [];
- const count = arr.length;
- if (count === 0) return '';
- if (count === 1) {
- const o = this.normalizedOptions.find(x => x.value === arr[0]);
- return o
- ? o.text + (this.suffix ? ' ' + this.suffix : '')
- : '';
- }
- // more than one selected
- return `${count} ausgewählt`;
+ const addSuffix = text => text ? `${text}${this.suffix ? ` ${this.suffix}` : ''}` : '';
+
+ if (!this.multiple) {
+ const opt = this.normalizedOptions.find(o => o.value === this.selected);
+ return opt ? addSuffix(opt.text) : 'Auswählen...';
}
- // single select
- const sel = this.normalizedOptions.find(o => o.value === this.selected);
- return sel
- ? sel.text + (this.suffix ? ' ' + this.suffix : '')
- : '';
+
+ const count = Array.isArray(this.selected) ? this.selected.length : 0;
+ if (count === 0) return 'Auswählen...';
+ if (count === 1) {
+ const opt = this.normalizedOptions.find(o => o.value === this.selected[0]);
+ return opt ? addSuffix(opt.text) : '';
+ }
+ return `${count} ausgewählt`;
+ },
+
+ hasSelection() {
+ if (this.multiple) {
+ return Array.isArray(this.selected) && this.selected.length > 0;
+ }
+ return this.selected !== null && this.selected !== undefined && this.selected !== '';
}
},
@@ -116,7 +95,7 @@ Vue.component('tt-select', {
},
onClickOutside(e) {
- if (!this.$el.contains(e.target)) {
+ if (this.$el && !this.$el.contains(e.target)) {
this.open = false;
}
},
@@ -129,111 +108,80 @@ Vue.component('tt-select', {
selectOption(opt) {
if (opt.disabled) return;
- if (this.multiple) {
- const arr = Array.isArray(this.selected) ? [...this.selected] : [];
- const idx = arr.indexOf(opt.value);
- if (idx > -1) arr.splice(idx, 1);
- else arr.push(opt.value);
- this.selected = arr;
- this.$emit('input', arr);
+ if (this.multiple) {
+ const newSelection = Array.isArray(this.selected) ? [...this.selected] : [];
+ const index = newSelection.indexOf(opt.value);
+ if (index > -1) {
+ newSelection.splice(index, 1);
+ } else {
+ newSelection.push(opt.value);
+ }
+ this.selected = newSelection;
} else {
this.selected = opt.value;
- this.$emit('input', opt.value);
- this.open = false;
+ this.open = false; // Close on selection for single mode
}
+ this.$emit('input', this.selected);
},
- // Enhancement: Handle Enter key press on search input
handleEnter() {
- // 4. If search is active and only one result is left, select it
if (this.searchQuery && this.filteredOptions.length === 1) {
this.selectOption(this.filteredOptions[0]);
- // Ensure dropdown closes for both single and multi-select mode
- this.open = false;
+ if (!this.multiple) this.open = false;
}
}
},
template: `
-