514 lines
12 KiB
Vue
514 lines
12 KiB
Vue
<template>
|
|
<div class="timefix-container">
|
|
<Modal
|
|
:show="showModal"
|
|
:title="modalConfig.title"
|
|
:message="modalConfig.message"
|
|
:type="modalConfig.type"
|
|
:confirmText="modalConfig.confirmText"
|
|
:cancelText="modalConfig.cancelText"
|
|
@confirm="onConfirm"
|
|
@cancel="onCancel"
|
|
/>
|
|
<!-- Formular für neue Zeitkorrektur -->
|
|
<div class="card form-card">
|
|
<h2>Neue Zeitkorrektur</h2>
|
|
|
|
<form @submit.prevent="createTimefix">
|
|
<!-- Erste Zeile: Original-Eintrag -->
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="originalDate">Datum des Original-Eintrags</label>
|
|
<input
|
|
type="date"
|
|
id="originalDate"
|
|
v-model="form.originalDate"
|
|
@change="loadWorklogEntries"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="worklogEntry">Eintrag</label>
|
|
<select
|
|
id="worklogEntry"
|
|
v-model="form.worklogId"
|
|
@change="onEntrySelected"
|
|
required
|
|
:disabled="!availableEntries.length"
|
|
>
|
|
<option value="">{{ availableEntries.length === 0 ? '-- Keine Einträge für dieses Datum --' : '-- Bitte wählen --' }}</option>
|
|
<option
|
|
v-for="entry in availableEntries"
|
|
:key="entry.id"
|
|
:value="entry.id"
|
|
>
|
|
{{ entry.time }} - {{ formatAction(entry.action) }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Zweite Zeile: Neue Werte -->
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="newDate">Neues Datum</label>
|
|
<input
|
|
type="date"
|
|
id="newDate"
|
|
v-model="form.newDate"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="newTime">Neue Uhrzeit</label>
|
|
<input
|
|
type="time"
|
|
id="newTime"
|
|
v-model="form.newTime"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="newAction">Neuer Eintrags-Typ</label>
|
|
<select
|
|
id="newAction"
|
|
v-model="form.newAction"
|
|
required
|
|
>
|
|
<option value="">-- Bitte wählen --</option>
|
|
<option value="start work">Arbeit beginnen</option>
|
|
<option value="stop work">Arbeit beenden</option>
|
|
<option value="start pause">Pause beginnen</option>
|
|
<option value="stop pause">Pause beenden</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<button type="submit" class="btn btn-primary" :disabled="loading">
|
|
{{ loading ? 'Wird gespeichert...' : 'Korrektur speichern' }}
|
|
</button>
|
|
<button type="button" class="btn btn-secondary" @click="resetForm">
|
|
Zurücksetzen
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Tabelle der heutigen Zeitkorrekturen -->
|
|
<div class="card table-card">
|
|
<h2>Zeitkorrekturen von heute</h2>
|
|
|
|
<div v-if="loading && timefixes.length === 0" class="loading">
|
|
Lade Zeitkorrekturen...
|
|
</div>
|
|
|
|
<div v-else-if="timefixes.length === 0" class="empty-state">
|
|
Keine Zeitkorrekturen für heute vorhanden.
|
|
</div>
|
|
|
|
<table v-else class="timefix-table">
|
|
<thead>
|
|
<tr>
|
|
<th colspan="3">Neue Werte</th>
|
|
<th colspan="3">Originalwerte</th>
|
|
<th>Aktionen</th>
|
|
</tr>
|
|
<tr>
|
|
<th>Zeit</th>
|
|
<th>Datum</th>
|
|
<th>Aktion</th>
|
|
<th>Zeit</th>
|
|
<th>Datum</th>
|
|
<th>Aktion</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="timefix in timefixes" :key="timefix.id">
|
|
<td>{{ timefix.newTime }}</td>
|
|
<td>{{ formatDate(timefix.newDate) }}</td>
|
|
<td>{{ formatAction(timefix.newAction) }}</td>
|
|
<td>{{ timefix.originalTime }}</td>
|
|
<td>{{ formatDate(timefix.originalDate) }}</td>
|
|
<td>{{ formatAction(timefix.originalAction) }}</td>
|
|
<td class="actions">
|
|
<button
|
|
@click="deleteTimefix(timefix.id)"
|
|
class="btn btn-danger btn-small"
|
|
title="Korrektur rückgängig machen"
|
|
>
|
|
Löschen
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { useAuthStore } from '../stores/authStore'
|
|
import { useModal } from '../composables/useModal'
|
|
import Modal from '../components/Modal.vue'
|
|
|
|
const authStore = useAuthStore()
|
|
const timefixes = ref([])
|
|
const availableEntries = ref([])
|
|
const loading = ref(false)
|
|
|
|
const { showModal, modalConfig, alert, confirm, onConfirm, onCancel } = useModal()
|
|
|
|
const form = ref({
|
|
originalDate: new Date().toISOString().split('T')[0],
|
|
worklogId: '',
|
|
newDate: new Date().toISOString().split('T')[0],
|
|
newTime: '',
|
|
newAction: ''
|
|
})
|
|
|
|
// Lade Worklog-Einträge für das ausgewählte Datum
|
|
async function loadWorklogEntries() {
|
|
try {
|
|
const date = form.value.originalDate
|
|
if (!date) return
|
|
|
|
// Hole alle Worklog-Einträge für das Datum vom Backend
|
|
const response = await fetch(`${API_URL}/timefix/worklog-entries?date=${date}`, {
|
|
headers: authStore.getAuthHeaders()
|
|
})
|
|
|
|
if (!response.ok) throw new Error('Fehler beim Laden der Einträge')
|
|
|
|
const entries = await response.json()
|
|
|
|
// Formatiere die Einträge für das Dropdown
|
|
availableEntries.value = entries.map(entry => ({
|
|
id: entry.id,
|
|
time: entry.time,
|
|
action: entry.action,
|
|
tstamp: entry.tstamp
|
|
}))
|
|
|
|
// Wenn keine Einträge gefunden wurden
|
|
if (availableEntries.value.length === 0) {
|
|
console.log('Keine Worklog-Einträge für dieses Datum gefunden')
|
|
}
|
|
|
|
// Reset worklogId wenn Datum geändert wird
|
|
form.value.worklogId = ''
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden der Worklog-Einträge:', error)
|
|
availableEntries.value = []
|
|
}
|
|
}
|
|
|
|
// Wenn ein Eintrag ausgewählt wird, fülle die Felder vor
|
|
function onEntrySelected() {
|
|
const selectedEntry = availableEntries.value.find(e => e.id === form.value.worklogId)
|
|
|
|
if (selectedEntry) {
|
|
// Fülle Datum und Aktion vor (aber nicht die Uhrzeit)
|
|
form.value.newDate = form.value.originalDate
|
|
form.value.newAction = selectedEntry.action
|
|
// form.value.newTime bleibt leer
|
|
}
|
|
}
|
|
|
|
// Lade alle Zeitkorrekturen für heute
|
|
async function loadTimefixes() {
|
|
try {
|
|
loading.value = true
|
|
const response = await fetch('${API_URL}/timefix', {
|
|
headers: authStore.getAuthHeaders()
|
|
})
|
|
|
|
if (!response.ok) throw new Error('Fehler beim Laden der Zeitkorrekturen')
|
|
|
|
timefixes.value = await response.json()
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden der Zeitkorrekturen:', error)
|
|
alert('Fehler beim Laden der Zeitkorrekturen')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
// Erstelle neue Zeitkorrektur
|
|
async function createTimefix() {
|
|
if (!form.value.worklogId || !form.value.newDate || !form.value.newTime || !form.value.newAction) {
|
|
await alert('Bitte füllen Sie alle Felder aus')
|
|
return
|
|
}
|
|
|
|
try {
|
|
loading.value = true
|
|
const response = await fetch('${API_URL}/timefix', {
|
|
method: 'POST',
|
|
headers: {
|
|
...authStore.getAuthHeaders(),
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
worklogId: form.value.worklogId,
|
|
newDate: form.value.newDate,
|
|
newTime: form.value.newTime,
|
|
newAction: form.value.newAction
|
|
})
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json()
|
|
throw new Error(error.message || 'Fehler beim Erstellen der Zeitkorrektur')
|
|
}
|
|
|
|
resetForm()
|
|
await Promise.all([
|
|
loadTimefixes(),
|
|
loadWorklogEntries() // Lade auch die Dropdown-Liste neu
|
|
])
|
|
} catch (error) {
|
|
console.error('Fehler beim Erstellen der Zeitkorrektur:', error)
|
|
await alert(`Fehler: ${error.message}`, 'Fehler')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
// Lösche Zeitkorrektur
|
|
async function deleteTimefix(id) {
|
|
const confirmed = await confirm('Möchten Sie diese Zeitkorrektur wirklich löschen?', 'Zeitkorrektur löschen')
|
|
|
|
if (!confirmed) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
loading.value = true
|
|
const response = await fetch(`${API_URL}/timefix/${id}`, {
|
|
method: 'DELETE',
|
|
headers: authStore.getAuthHeaders()
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json()
|
|
throw new Error(error.message || 'Fehler beim Löschen der Zeitkorrektur')
|
|
}
|
|
|
|
await loadTimefixes()
|
|
} catch (error) {
|
|
console.error('Fehler beim Löschen der Zeitkorrektur:', error)
|
|
await alert(`Fehler: ${error.message}`, 'Fehler')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
// Formatiere Datum
|
|
function formatDate(dateString) {
|
|
if (!dateString) return '-'
|
|
const date = new Date(dateString)
|
|
return date.toLocaleDateString('de-DE', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric'
|
|
})
|
|
}
|
|
|
|
// Formatiere Aktion
|
|
function formatAction(action) {
|
|
const actions = {
|
|
'start work': 'Arbeit beginnen',
|
|
'stop work': 'Arbeit beenden',
|
|
'start pause': 'Pause beginnen',
|
|
'stop pause': 'Pause beenden'
|
|
}
|
|
return actions[action] || action
|
|
}
|
|
|
|
// Formular zurücksetzen
|
|
function resetForm() {
|
|
form.value = {
|
|
originalDate: new Date().toISOString().split('T')[0],
|
|
worklogId: '',
|
|
newDate: new Date().toISOString().split('T')[0],
|
|
newTime: '',
|
|
newAction: ''
|
|
}
|
|
availableEntries.value = []
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadTimefixes()
|
|
loadWorklogEntries()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.timefix-container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 28px;
|
|
margin-bottom: 8px;
|
|
color: #2c3e50;
|
|
}
|
|
|
|
.subtitle {
|
|
color: #7f8c8d;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.card {
|
|
background: white;
|
|
border-radius: 8px;
|
|
padding: 24px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
h2 {
|
|
font-size: 20px;
|
|
margin-bottom: 20px;
|
|
color: #34495e;
|
|
}
|
|
|
|
/* Formular */
|
|
.form-row {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 16px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.form-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
label {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
margin-bottom: 6px;
|
|
color: #555;
|
|
}
|
|
|
|
input, select {
|
|
padding: 8px 12px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
font-size: 14px;
|
|
transition: border-color 0.2s;
|
|
}
|
|
|
|
input:focus, select:focus {
|
|
outline: none;
|
|
border-color: #4CAF50;
|
|
}
|
|
|
|
input:disabled, select:disabled {
|
|
background: #f5f5f5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.form-actions {
|
|
display: flex;
|
|
gap: 12px;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.btn {
|
|
padding: 10px 20px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: #4CAF50;
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover:not(:disabled) {
|
|
background: #45a049;
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: #ecf0f1;
|
|
color: #555;
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: #d5dbdd;
|
|
}
|
|
|
|
.btn:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
/* Tabelle */
|
|
.timefix-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.timefix-table thead {
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.timefix-table th {
|
|
padding: 12px;
|
|
text-align: left;
|
|
font-weight: 600;
|
|
color: #555;
|
|
border-bottom: 2px solid #ddd;
|
|
}
|
|
|
|
.timefix-table td {
|
|
padding: 12px;
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
|
|
.timefix-table tbody tr:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.timefix-table .actions {
|
|
text-align: center;
|
|
}
|
|
|
|
.btn-small {
|
|
padding: 6px 12px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.btn-danger {
|
|
background: #e74c3c;
|
|
color: white;
|
|
}
|
|
|
|
.btn-danger:hover {
|
|
background: #c0392b;
|
|
}
|
|
|
|
.loading, .empty-state {
|
|
text-align: center;
|
|
padding: 40px;
|
|
color: #7f8c8d;
|
|
}
|
|
</style>
|
|
|