myCRM/assets/js/views/ProjectStatusManagement.vue
olli ab4d2bf9f5 feat: Implement Project Status Management
- 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.
2025-11-11 16:25:54 +01:00

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>