- 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.
330 lines
9.4 KiB
Vue
330 lines
9.4 KiB
Vue
<template>
|
|
<div class="project-status-management">
|
|
<CrudDataTable
|
|
ref="tableRef"
|
|
title="Projekt-Status"
|
|
entity-name="Status"
|
|
entity-name-article="einen"
|
|
:columns="statusColumns"
|
|
data-source="/api/project_statuses"
|
|
storage-key="projectStatusTableColumns"
|
|
:show-view-button="false"
|
|
:show-edit-button="canEdit"
|
|
:show-delete-button="canDelete"
|
|
:show-export-button="false"
|
|
@create="openNewStatusDialog"
|
|
@edit="editStatus"
|
|
@delete="confirmDelete"
|
|
>
|
|
<!-- Custom Column Templates -->
|
|
<template #body-name="{ data }">
|
|
<div class="flex align-items-center gap-2">
|
|
<span
|
|
class="inline-block w-3 h-3 border-round"
|
|
:style="{ backgroundColor: data.color }"
|
|
></span>
|
|
<span class="font-semibold">{{ data.name }}</span>
|
|
</div>
|
|
</template>
|
|
|
|
<template #body-color="{ data }">
|
|
<div class="flex align-items-center gap-2">
|
|
<span
|
|
class="inline-block border-round"
|
|
:style="{
|
|
backgroundColor: data.color,
|
|
width: '16px',
|
|
height: '16px',
|
|
border: '1px solid var(--surface-300)'
|
|
}"
|
|
></span>
|
|
<code class="text-sm">{{ data.color }}</code>
|
|
</div>
|
|
</template>
|
|
|
|
<template #body-sortOrder="{ data }">
|
|
{{ data.sortOrder }}
|
|
</template>
|
|
|
|
<template #body-isDefault="{ data }">
|
|
<Tag v-if="data.isDefault" value="Standard" severity="info" />
|
|
</template>
|
|
|
|
<template #body-isActive="{ data }">
|
|
<Tag :value="data.isActive ? 'Aktiv' : 'Inaktiv'" :severity="data.isActive ? 'success' : 'secondary'" />
|
|
</template>
|
|
|
|
<template #body-createdAt="{ data }">
|
|
{{ formatDate(data.createdAt) }}
|
|
</template>
|
|
</CrudDataTable>
|
|
|
|
<!-- Status Dialog -->
|
|
<Dialog
|
|
v-model:visible="statusDialog"
|
|
:header="editingStatus?.id ? 'Status bearbeiten' : 'Neuer Status'"
|
|
:modal="true"
|
|
:style="{ width: '600px' }"
|
|
:closable="!saving"
|
|
>
|
|
<div class="flex flex-col gap-4">
|
|
<div class="flex flex-col gap-2">
|
|
<label for="name">Name *</label>
|
|
<InputText
|
|
id="name"
|
|
v-model="editingStatus.name"
|
|
:class="{ 'p-invalid': submitted && !editingStatus.name }"
|
|
:disabled="saving"
|
|
/>
|
|
<small v-if="submitted && !editingStatus.name" class="p-error">Name ist erforderlich</small>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="color">Farbe</label>
|
|
<div class="flex gap-2">
|
|
<ColorPicker
|
|
v-model="editingStatus.color"
|
|
format="hex"
|
|
:disabled="saving"
|
|
/>
|
|
<InputText
|
|
id="color"
|
|
v-model="editingStatus.color"
|
|
placeholder="#3B82F6"
|
|
:disabled="saving"
|
|
class="flex-1"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<label for="sortOrder">Sortierung</label>
|
|
<InputNumber
|
|
id="sortOrder"
|
|
v-model="editingStatus.sortOrder"
|
|
:min="0"
|
|
:disabled="saving"
|
|
/>
|
|
<small class="text-500">Niedrigere Werte werden zuerst angezeigt</small>
|
|
</div>
|
|
|
|
<div class="flex align-items-center gap-2">
|
|
<Checkbox
|
|
id="isDefault"
|
|
v-model="editingStatus.isDefault"
|
|
:binary="true"
|
|
:disabled="saving"
|
|
/>
|
|
<label for="isDefault" class="mb-0">Als Standard-Status verwenden</label>
|
|
</div>
|
|
|
|
<div class="flex align-items-center gap-2">
|
|
<Checkbox
|
|
id="isActive"
|
|
v-model="editingStatus.isActive"
|
|
:binary="true"
|
|
:disabled="saving"
|
|
/>
|
|
<label for="isActive" class="mb-0">Status ist aktiv</label>
|
|
</div>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<Button label="Abbrechen" @click="statusDialog = false" text :disabled="saving" />
|
|
<Button label="Speichern" @click="saveStatus" :loading="saving" />
|
|
</template>
|
|
</Dialog>
|
|
|
|
<!-- Delete Confirmation Dialog -->
|
|
<Dialog
|
|
v-model:visible="deleteDialog"
|
|
header="Status 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 den Status <b>{{ statusToDelete?.name }}</b> wirklich löschen?</span>
|
|
</div>
|
|
<template #footer>
|
|
<Button label="Abbrechen" @click="deleteDialog = false" text :disabled="deleting" />
|
|
<Button label="Löschen" @click="deleteStatus" severity="danger" :loading="deleting" />
|
|
</template>
|
|
</Dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, 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 InputNumber from 'primevue/inputnumber'
|
|
import Checkbox from 'primevue/checkbox'
|
|
import ColorPicker from 'primevue/colorpicker'
|
|
import Tag from 'primevue/tag'
|
|
|
|
const toast = useToast()
|
|
const tableRef = ref(null)
|
|
const statusDialog = ref(false)
|
|
const deleteDialog = ref(false)
|
|
const editingStatus = ref({})
|
|
const statusToDelete = ref(null)
|
|
const submitted = ref(false)
|
|
const saving = ref(false)
|
|
const deleting = ref(false)
|
|
|
|
// Permission checks
|
|
const canEdit = computed(() => true)
|
|
const canDelete = computed(() => true)
|
|
|
|
// Column definitions
|
|
const statusColumns = ref([
|
|
{ key: 'name', label: 'Name', field: 'name', sortable: true, visible: true },
|
|
{ key: 'color', label: 'Farbe', field: 'color', sortable: false, visible: true, style: { width: '150px' } },
|
|
{ key: 'sortOrder', label: 'Sortierung', field: 'sortOrder', sortable: true, visible: true, style: { width: '120px' } },
|
|
{ key: 'isDefault', label: 'Standard', field: 'isDefault', sortable: true, visible: true, style: { width: '120px' } },
|
|
{ key: 'isActive', label: 'Aktiv', field: 'isActive', sortable: true, visible: true, style: { width: '100px' } },
|
|
{ key: 'createdAt', label: 'Erstellt am', field: 'createdAt', sortable: true, visible: false }
|
|
])
|
|
|
|
function openNewStatusDialog() {
|
|
editingStatus.value = {
|
|
name: '',
|
|
color: '#3B82F6',
|
|
sortOrder: 0,
|
|
isDefault: false,
|
|
isActive: true
|
|
}
|
|
submitted.value = false
|
|
statusDialog.value = true
|
|
}
|
|
|
|
function editStatus(status) {
|
|
editingStatus.value = { ...status }
|
|
submitted.value = false
|
|
statusDialog.value = true
|
|
}
|
|
|
|
async function saveStatus() {
|
|
submitted.value = true
|
|
|
|
if (!editingStatus.value.name) {
|
|
return
|
|
}
|
|
|
|
saving.value = true
|
|
|
|
try {
|
|
// Ensure color has # prefix
|
|
if (editingStatus.value.color && !editingStatus.value.color.startsWith('#')) {
|
|
editingStatus.value.color = '#' + editingStatus.value.color
|
|
}
|
|
|
|
const statusData = {
|
|
name: editingStatus.value.name,
|
|
color: editingStatus.value.color || null,
|
|
sortOrder: editingStatus.value.sortOrder || 0,
|
|
isDefault: editingStatus.value.isDefault || false,
|
|
isActive: editingStatus.value.isActive !== false
|
|
}
|
|
|
|
const isNew = !editingStatus.value.id
|
|
const url = isNew ? '/api/project_statuses' : `/api/project_statuses/${editingStatus.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(statusData)
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json()
|
|
throw new Error(error['hydra:description'] || 'Fehler beim Speichern')
|
|
}
|
|
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: 'Erfolg',
|
|
detail: `Status wurde ${isNew ? 'erstellt' : 'aktualisiert'}`,
|
|
life: 3000
|
|
})
|
|
|
|
statusDialog.value = false
|
|
tableRef.value?.loadData()
|
|
} catch (error) {
|
|
console.error('Error saving status:', error)
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Fehler',
|
|
detail: error.message || 'Status konnte nicht gespeichert werden',
|
|
life: 3000
|
|
})
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
function confirmDelete(status) {
|
|
statusToDelete.value = status
|
|
deleteDialog.value = true
|
|
}
|
|
|
|
async function deleteStatus() {
|
|
deleting.value = true
|
|
|
|
try {
|
|
const response = await fetch(`/api/project_statuses/${statusToDelete.value.id}`, {
|
|
method: 'DELETE'
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Fehler beim Löschen')
|
|
}
|
|
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: 'Erfolg',
|
|
detail: 'Status wurde gelöscht',
|
|
life: 3000
|
|
})
|
|
|
|
deleteDialog.value = false
|
|
statusToDelete.value = null
|
|
tableRef.value?.loadData()
|
|
} catch (error) {
|
|
console.error('Error deleting status:', error)
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Fehler',
|
|
detail: 'Status 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'
|
|
})
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.project-status-management {
|
|
height: 100%;
|
|
}
|
|
</style>
|