- Added ProjectStatusManagement.vue for managing project statuses with CRUD operations. - Created migration scripts for projects and project_statuses tables. - Defined Project and ProjectStatus entities with necessary fields and relationships. - Implemented repositories for Project and ProjectStatus with custom query methods. - Enhanced API resource configurations for Project and ProjectStatus entities.
711 lines
23 KiB
Vue
711 lines
23 KiB
Vue
<template>
|
|
<div class="project-management">
|
|
<CrudDataTable
|
|
ref="tableRef"
|
|
title="Projekte"
|
|
entity-name="Projekt"
|
|
entity-name-article="ein"
|
|
:columns="projectColumns"
|
|
data-source="/api/projects"
|
|
storage-key="projectTableColumns"
|
|
:show-view-button="canView"
|
|
:show-edit-button="canEdit"
|
|
:show-delete-button="canDelete"
|
|
:show-export-button="canExport"
|
|
@view="viewProject"
|
|
@create="openNewProjectDialog"
|
|
@edit="editProject"
|
|
@delete="confirmDelete"
|
|
@data-loaded="onDataLoaded"
|
|
>
|
|
<!-- Custom Filter Buttons -->
|
|
<template #filter-buttons="{ loadData }">
|
|
<div class="flex gap-2 mb-4">
|
|
<Button
|
|
label="Alle"
|
|
:outlined="typeFilter !== 'all'"
|
|
@click="filterByType('all', loadData)"
|
|
size="small"
|
|
/>
|
|
<Button
|
|
label="Beruflich"
|
|
:outlined="typeFilter !== 'business'"
|
|
@click="filterByType('business', loadData)"
|
|
size="small"
|
|
/>
|
|
<Button
|
|
label="Privat"
|
|
:outlined="typeFilter !== 'private'"
|
|
@click="filterByType('private', loadData)"
|
|
size="small"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Custom Column Templates -->
|
|
<template #body-name="{ data }">
|
|
<div class="font-semibold">{{ data.name }}</div>
|
|
</template>
|
|
|
|
<template #body-projectNumber="{ data }">
|
|
{{ data.projectNumber }}
|
|
</template>
|
|
|
|
<template #body-customer="{ data }">
|
|
<div v-if="data.customer">{{ data.customer.companyName }}</div>
|
|
<span v-else class="text-500">Kein Kunde zugewiesen</span>
|
|
</template>
|
|
|
|
<template #body-status="{ data }">
|
|
<Tag v-if="data.status" :value="data.status.name" :style="{ backgroundColor: data.status.color }" />
|
|
<span v-else class="text-500">Kein Status</span>
|
|
</template>
|
|
|
|
<template #body-orderNumber="{ data }">
|
|
{{ data.orderNumber }}
|
|
</template>
|
|
|
|
<template #body-orderDate="{ data }">
|
|
{{ formatDate(data.orderDate) }}
|
|
</template>
|
|
|
|
<template #body-startDate="{ data }">
|
|
{{ formatDate(data.startDate) }}
|
|
</template>
|
|
|
|
<template #body-endDate="{ data }">
|
|
{{ formatDate(data.endDate) }}
|
|
</template>
|
|
|
|
<template #body-budget="{ data }">
|
|
<div v-if="data.budget" class="text-right">{{ formatCurrency(data.budget) }}</div>
|
|
</template>
|
|
|
|
<template #body-hourContingent="{ data }">
|
|
<div v-if="data.hourContingent" class="text-right">{{ data.hourContingent }} h</div>
|
|
</template>
|
|
|
|
<template #body-type="{ data }">
|
|
<Tag :value="data.isPrivate ? 'Privat' : 'Beruflich'" :severity="data.isPrivate ? 'info' : 'success'" />
|
|
</template>
|
|
|
|
<template #body-createdAt="{ data }">
|
|
{{ formatDate(data.createdAt) }}
|
|
</template>
|
|
|
|
<template #body-updatedAt="{ data }">
|
|
{{ formatDate(data.updatedAt) }}
|
|
</template>
|
|
</CrudDataTable>
|
|
|
|
<!-- Project Dialog -->
|
|
<Dialog
|
|
v-model:visible="projectDialog"
|
|
:header="editingProject?.id ? 'Projekt bearbeiten' : 'Neues Projekt'"
|
|
:modal="true"
|
|
:style="{ width: '900px' }"
|
|
:closable="!saving"
|
|
>
|
|
<div class="flex flex-col gap-4">
|
|
<!-- Basic Information -->
|
|
<div class="font-semibold text-lg">Grunddaten</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="flex flex-col gap-2 col-span-2">
|
|
<label for="name">Projektname *</label>
|
|
<InputText
|
|
id="name"
|
|
v-model="editingProject.name"
|
|
:class="{ 'p-invalid': submitted && !editingProject.name }"
|
|
:disabled="saving"
|
|
/>
|
|
<small v-if="submitted && !editingProject.name" class="p-error">Projektname ist erforderlich</small>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2 col-span-2">
|
|
<label for="description">Beschreibung</label>
|
|
<Textarea
|
|
id="description"
|
|
v-model="editingProject.description"
|
|
rows="3"
|
|
:disabled="saving"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="customer">Kunde</label>
|
|
<Select
|
|
id="customer"
|
|
v-model="editingProject.customer"
|
|
:options="customers"
|
|
option-label="companyName"
|
|
placeholder="Kunde auswählen"
|
|
filter
|
|
:disabled="saving"
|
|
show-clear
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="status">Status</label>
|
|
<Select
|
|
id="status"
|
|
v-model="editingProject.status"
|
|
:options="statuses"
|
|
option-label="name"
|
|
placeholder="Status auswählen"
|
|
:disabled="saving"
|
|
show-clear
|
|
>
|
|
<template #option="slotProps">
|
|
<div class="flex align-items-center gap-2">
|
|
<span
|
|
class="inline-block w-3 h-3 border-round"
|
|
:style="{ backgroundColor: slotProps.option.color }"
|
|
></span>
|
|
<span>{{ slotProps.option.name }}</span>
|
|
</div>
|
|
</template>
|
|
</Select>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="isPrivate">Typ</label>
|
|
<div class="flex align-items-center gap-3 mt-2">
|
|
<div class="flex align-items-center">
|
|
<RadioButton
|
|
id="business"
|
|
v-model="editingProject.isPrivate"
|
|
:value="false"
|
|
:disabled="saving"
|
|
/>
|
|
<label for="business" class="ml-2">Beruflich</label>
|
|
</div>
|
|
<div class="flex align-items-center">
|
|
<RadioButton
|
|
id="private"
|
|
v-model="editingProject.isPrivate"
|
|
:value="true"
|
|
:disabled="saving"
|
|
/>
|
|
<label for="private" class="ml-2">Privat</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Project Numbers & Dates -->
|
|
<div class="font-semibold text-lg mt-4">Nummern & Daten</div>
|
|
<div class="grid grid-cols-3 gap-4">
|
|
<div class="flex flex-col gap-2">
|
|
<label for="projectNumber">Projektnummer</label>
|
|
<InputText
|
|
id="projectNumber"
|
|
v-model="editingProject.projectNumber"
|
|
:disabled="saving"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="orderNumber">Bestellnummer</label>
|
|
<InputText
|
|
id="orderNumber"
|
|
v-model="editingProject.orderNumber"
|
|
:disabled="saving"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="orderDate">Bestelldatum</label>
|
|
<DatePicker
|
|
id="orderDate"
|
|
v-model="editingProject.orderDate"
|
|
date-format="dd.mm.yy"
|
|
:disabled="saving"
|
|
show-icon
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="startDate">Startdatum</label>
|
|
<DatePicker
|
|
id="startDate"
|
|
v-model="editingProject.startDate"
|
|
date-format="dd.mm.yy"
|
|
:disabled="saving"
|
|
show-icon
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="endDate">Enddatum</label>
|
|
<DatePicker
|
|
id="endDate"
|
|
v-model="editingProject.endDate"
|
|
date-format="dd.mm.yy"
|
|
:disabled="saving"
|
|
show-icon
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Budget & Hours -->
|
|
<div class="font-semibold text-lg mt-4">Budget & Kontingent</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="flex flex-col gap-2">
|
|
<label for="budget">Budget (€)</label>
|
|
<InputNumber
|
|
id="budget"
|
|
v-model="editingProject.budget"
|
|
mode="currency"
|
|
currency="EUR"
|
|
locale="de-DE"
|
|
:min="0"
|
|
:disabled="saving"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="hourContingent">Stundenkontingent</label>
|
|
<InputNumber
|
|
id="hourContingent"
|
|
v-model="editingProject.hourContingent"
|
|
suffix=" h"
|
|
:min-fraction-digits="0"
|
|
:max-fraction-digits="2"
|
|
:min="0"
|
|
:disabled="saving"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<Button label="Abbrechen" @click="projectDialog = false" text :disabled="saving" />
|
|
<Button label="Speichern" @click="saveProject" :loading="saving" />
|
|
</template>
|
|
</Dialog>
|
|
|
|
<!-- Delete Confirmation Dialog -->
|
|
<Dialog
|
|
v-model:visible="deleteDialog"
|
|
header="Projekt löschen"
|
|
:modal="true"
|
|
:style="{ width: '450px' }"
|
|
>
|
|
<div class="flex align-items-center gap-3">
|
|
<i class="pi pi-exclamation-triangle text-4xl text-red-500" />
|
|
<span>Möchten Sie das Projekt <b>{{ projectToDelete?.name }}</b> wirklich löschen?</span>
|
|
</div>
|
|
<template #footer>
|
|
<Button label="Abbrechen" @click="deleteDialog = false" text :disabled="deleting" />
|
|
<Button label="Löschen" @click="deleteProject" severity="danger" :loading="deleting" />
|
|
</template>
|
|
</Dialog>
|
|
|
|
<!-- View Project Dialog -->
|
|
<Dialog
|
|
v-model:visible="viewDialog"
|
|
header="Projekt anzeigen"
|
|
:modal="true"
|
|
:style="{ width: '900px' }"
|
|
>
|
|
<div class="flex flex-col gap-4">
|
|
<!-- Basic Information -->
|
|
<div class="font-semibold text-lg">Grunddaten</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Projektname</label>
|
|
<div class="text-900">{{ viewingProject.name || '-' }}</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Projektnummer</label>
|
|
<div class="text-900">{{ viewingProject.projectNumber || '-' }}</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2 col-span-2">
|
|
<label class="font-medium text-sm text-500">Beschreibung</label>
|
|
<div class="text-900 whitespace-pre-wrap">{{ viewingProject.description || '-' }}</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Kunde</label>
|
|
<div class="text-900">{{ viewingProject.customer?.companyName || '-' }}</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Status</label>
|
|
<Tag v-if="viewingProject.status" :value="viewingProject.status.name" :style="{ backgroundColor: viewingProject.status.color }" />
|
|
<div v-else class="text-900">-</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Typ</label>
|
|
<Tag :value="viewingProject.isPrivate ? 'Privat' : 'Beruflich'" :severity="viewingProject.isPrivate ? 'info' : 'success'" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Project Numbers & Dates -->
|
|
<div class="font-semibold text-lg mt-4">Nummern & Daten</div>
|
|
<div class="grid grid-cols-3 gap-4">
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Bestellnummer</label>
|
|
<div class="text-900">{{ viewingProject.orderNumber || '-' }}</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Bestelldatum</label>
|
|
<div class="text-900">{{ formatDate(viewingProject.orderDate) || '-' }}</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Startdatum</label>
|
|
<div class="text-900">{{ formatDate(viewingProject.startDate) || '-' }}</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Enddatum</label>
|
|
<div class="text-900">{{ formatDate(viewingProject.endDate) || '-' }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Budget & Hours -->
|
|
<div class="font-semibold text-lg mt-4">Budget & Kontingent</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Budget</label>
|
|
<div class="text-900">{{ viewingProject.budget ? formatCurrency(viewingProject.budget) : '-' }}</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Stundenkontingent</label>
|
|
<div class="text-900">{{ viewingProject.hourContingent ? `${viewingProject.hourContingent} h` : '-' }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Timestamps -->
|
|
<div class="font-semibold text-lg mt-4">Metadaten</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Erstellt am</label>
|
|
<div class="text-900">{{ formatDate(viewingProject.createdAt) || '-' }}</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label class="font-medium text-sm text-500">Zuletzt geändert</label>
|
|
<div class="text-900">{{ formatDate(viewingProject.updatedAt) || '-' }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<Button label="Schließen" @click="viewDialog = false" />
|
|
<Button v-if="canEdit" label="Bearbeiten" @click="editFromView" icon="pi pi-pencil" />
|
|
</template>
|
|
</Dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, computed } from 'vue'
|
|
import { useToast } from 'primevue/usetoast'
|
|
import CrudDataTable from '../components/CrudDataTable.vue'
|
|
import Dialog from 'primevue/dialog'
|
|
import Button from 'primevue/button'
|
|
import InputText from 'primevue/inputtext'
|
|
import Textarea from 'primevue/textarea'
|
|
import Select from 'primevue/select'
|
|
import DatePicker from 'primevue/datepicker'
|
|
import InputNumber from 'primevue/inputnumber'
|
|
import RadioButton from 'primevue/radiobutton'
|
|
import Tag from 'primevue/tag'
|
|
|
|
const toast = useToast()
|
|
const tableRef = ref(null)
|
|
const projectDialog = ref(false)
|
|
const viewDialog = ref(false)
|
|
const deleteDialog = ref(false)
|
|
const editingProject = ref({})
|
|
const viewingProject = ref({})
|
|
const projectToDelete = ref(null)
|
|
const submitted = ref(false)
|
|
const saving = ref(false)
|
|
const deleting = ref(false)
|
|
const typeFilter = ref('all')
|
|
const customers = ref([])
|
|
const statuses = ref([])
|
|
|
|
// Permission checks (will be replaced with actual permission checks)
|
|
const canView = computed(() => true)
|
|
const canEdit = computed(() => true)
|
|
const canDelete = computed(() => true)
|
|
const canExport = computed(() => true)
|
|
|
|
// Column definitions
|
|
const projectColumns = ref([
|
|
{ key: 'name', label: 'Projektname', field: 'name', sortable: true, visible: true },
|
|
{ key: 'projectNumber', label: 'Projektnummer', field: 'projectNumber', sortable: true, visible: true },
|
|
{ key: 'customer', label: 'Kunde', field: 'customer', sortable: false, visible: true },
|
|
{ key: 'status', label: 'Status', field: 'status', sortable: false, visible: true },
|
|
{ key: 'orderNumber', label: 'Bestellnummer', field: 'orderNumber', sortable: true, visible: false },
|
|
{ key: 'orderDate', label: 'Bestelldatum', field: 'orderDate', sortable: true, visible: false },
|
|
{ key: 'startDate', label: 'Start', field: 'startDate', sortable: true, visible: true },
|
|
{ key: 'endDate', label: 'Ende', field: 'endDate', sortable: true, visible: true },
|
|
{ key: 'budget', label: 'Budget', field: 'budget', sortable: true, visible: true },
|
|
{ key: 'hourContingent', label: 'Stunden', field: 'hourContingent', sortable: true, visible: false },
|
|
{ key: 'type', label: 'Typ', field: 'type', sortable: false, visible: true },
|
|
{ key: 'createdAt', label: 'Erstellt am', field: 'createdAt', sortable: true, visible: false },
|
|
{ key: 'updatedAt', label: 'Geändert am', field: 'updatedAt', sortable: true, visible: false }
|
|
])
|
|
|
|
onMounted(async () => {
|
|
await Promise.all([loadCustomers(), loadStatuses()])
|
|
})
|
|
|
|
async function loadCustomers() {
|
|
try {
|
|
const response = await fetch('/api/contacts?pagination=false')
|
|
if (!response.ok) throw new Error('Fehler beim Laden der Kunden')
|
|
|
|
const data = await response.json()
|
|
const customersList = data['hydra:member'] || data.member || data
|
|
|
|
// Ensure it's an array
|
|
customers.value = Array.isArray(customersList) ? customersList : []
|
|
} catch (error) {
|
|
console.error('Error loading customers:', error)
|
|
customers.value = []
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Fehler',
|
|
detail: 'Kunden konnten nicht geladen werden',
|
|
life: 3000
|
|
})
|
|
}
|
|
}
|
|
|
|
async function loadStatuses() {
|
|
try {
|
|
const response = await fetch('/api/project_statuses?pagination=false&isActive=true')
|
|
if (!response.ok) throw new Error('Fehler beim Laden der Status')
|
|
|
|
const data = await response.json()
|
|
const statusesList = data['hydra:member'] || data.member || data
|
|
|
|
// Ensure it's an array
|
|
statuses.value = Array.isArray(statusesList) ? statusesList : []
|
|
} catch (error) {
|
|
console.error('Error loading statuses:', error)
|
|
statuses.value = []
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Fehler',
|
|
detail: 'Status konnten nicht geladen werden',
|
|
life: 3000
|
|
})
|
|
}
|
|
}
|
|
|
|
function filterByType(type, loadData) {
|
|
typeFilter.value = type
|
|
|
|
let filters = {}
|
|
if (type === 'business') {
|
|
filters.isPrivate = false
|
|
} else if (type === 'private') {
|
|
filters.isPrivate = true
|
|
}
|
|
|
|
loadData(filters)
|
|
}
|
|
|
|
function onDataLoaded(data) {
|
|
console.log('Projects loaded:', data.length)
|
|
}
|
|
|
|
function viewProject(project) {
|
|
viewingProject.value = { ...project }
|
|
viewDialog.value = true
|
|
}
|
|
|
|
function editFromView() {
|
|
viewDialog.value = false
|
|
editProject(viewingProject.value)
|
|
}
|
|
|
|
function openNewProjectDialog() {
|
|
// Find default status
|
|
const defaultStatus = statuses.value.find(s => s.isDefault) || statuses.value[0] || null
|
|
|
|
editingProject.value = {
|
|
name: '',
|
|
description: '',
|
|
customer: null,
|
|
status: defaultStatus,
|
|
projectNumber: '',
|
|
orderNumber: '',
|
|
orderDate: null,
|
|
startDate: null,
|
|
endDate: null,
|
|
budget: null,
|
|
hourContingent: null,
|
|
isPrivate: false
|
|
}
|
|
submitted.value = false
|
|
projectDialog.value = true
|
|
}
|
|
|
|
function editProject(project) {
|
|
// Convert date strings to Date objects for Calendar component
|
|
editingProject.value = {
|
|
...project,
|
|
orderDate: project.orderDate ? new Date(project.orderDate) : null,
|
|
startDate: project.startDate ? new Date(project.startDate) : null,
|
|
endDate: project.endDate ? new Date(project.endDate) : null,
|
|
budget: project.budget ? parseFloat(project.budget) : null,
|
|
hourContingent: project.hourContingent ? parseFloat(project.hourContingent) : null
|
|
}
|
|
submitted.value = false
|
|
projectDialog.value = true
|
|
}
|
|
|
|
async function saveProject() {
|
|
submitted.value = true
|
|
|
|
if (!editingProject.value.name) {
|
|
return
|
|
}
|
|
|
|
saving.value = true
|
|
|
|
try {
|
|
// Prepare data for API
|
|
const projectData = {
|
|
name: editingProject.value.name,
|
|
description: editingProject.value.description || null,
|
|
customer: editingProject.value.customer ? `/api/contacts/${editingProject.value.customer.id}` : null,
|
|
status: editingProject.value.status ? `/api/project_statuses/${editingProject.value.status.id}` : null,
|
|
projectNumber: editingProject.value.projectNumber || null,
|
|
orderNumber: editingProject.value.orderNumber || null,
|
|
orderDate: editingProject.value.orderDate ? formatDateForAPI(editingProject.value.orderDate) : null,
|
|
startDate: editingProject.value.startDate ? formatDateForAPI(editingProject.value.startDate) : null,
|
|
endDate: editingProject.value.endDate ? formatDateForAPI(editingProject.value.endDate) : null,
|
|
budget: editingProject.value.budget ? editingProject.value.budget.toString() : null,
|
|
hourContingent: editingProject.value.hourContingent ? editingProject.value.hourContingent.toString() : null,
|
|
isPrivate: editingProject.value.isPrivate
|
|
}
|
|
|
|
const isNew = !editingProject.value.id
|
|
const url = isNew ? '/api/projects' : `/api/projects/${editingProject.value.id}`
|
|
const method = isNew ? 'POST' : 'PUT'
|
|
|
|
const response = await fetch(url, {
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/ld+json',
|
|
'Accept': 'application/ld+json'
|
|
},
|
|
body: JSON.stringify(projectData)
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json()
|
|
throw new Error(error['hydra:description'] || 'Fehler beim Speichern')
|
|
}
|
|
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: 'Erfolg',
|
|
detail: `Projekt wurde ${isNew ? 'erstellt' : 'aktualisiert'}`,
|
|
life: 3000
|
|
})
|
|
|
|
projectDialog.value = false
|
|
tableRef.value?.loadData()
|
|
} catch (error) {
|
|
console.error('Error saving project:', error)
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Fehler',
|
|
detail: error.message || 'Projekt konnte nicht gespeichert werden',
|
|
life: 3000
|
|
})
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
function confirmDelete(project) {
|
|
projectToDelete.value = project
|
|
deleteDialog.value = true
|
|
}
|
|
|
|
async function deleteProject() {
|
|
deleting.value = true
|
|
|
|
try {
|
|
const response = await fetch(`/api/projects/${projectToDelete.value.id}`, {
|
|
method: 'DELETE'
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Fehler beim Löschen')
|
|
}
|
|
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: 'Erfolg',
|
|
detail: 'Projekt wurde gelöscht',
|
|
life: 3000
|
|
})
|
|
|
|
deleteDialog.value = false
|
|
projectToDelete.value = null
|
|
tableRef.value?.loadData()
|
|
} catch (error) {
|
|
console.error('Error deleting project:', error)
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Fehler',
|
|
detail: 'Projekt konnte nicht gelöscht werden',
|
|
life: 3000
|
|
})
|
|
} finally {
|
|
deleting.value = false
|
|
}
|
|
}
|
|
|
|
function formatDate(dateString) {
|
|
if (!dateString) return ''
|
|
const date = new Date(dateString)
|
|
return date.toLocaleDateString('de-DE', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric'
|
|
})
|
|
}
|
|
|
|
function formatDateForAPI(date) {
|
|
if (!date) return null
|
|
const d = new Date(date)
|
|
const year = d.getFullYear()
|
|
const month = String(d.getMonth() + 1).padStart(2, '0')
|
|
const day = String(d.getDate()).padStart(2, '0')
|
|
return `${year}-${month}-${day}`
|
|
}
|
|
|
|
function formatCurrency(value) {
|
|
if (!value) return ''
|
|
return new Intl.NumberFormat('de-DE', {
|
|
style: 'currency',
|
|
currency: 'EUR'
|
|
}).format(value)
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.project-management {
|
|
height: 100%;
|
|
}
|
|
</style>
|