Projekttyp-Verwaltung reparieren
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 46s
Build and Push Docker Images / build-frontend (push) Successful in 1m12s
Build and Push Docker Images / build-website (push) Successful in 23s
Build and Push Docker Images / build-central-services-api (push) Successful in 22s
Build and Push Docker Images / build-central-services-admin (push) Successful in 21s
Build and Push Docker Images / build-docs (push) Successful in 21s
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 46s
Build and Push Docker Images / build-frontend (push) Successful in 1m12s
Build and Push Docker Images / build-website (push) Successful in 23s
Build and Push Docker Images / build-central-services-api (push) Successful in 22s
Build and Push Docker Images / build-central-services-admin (push) Successful in 21s
Build and Push Docker Images / build-docs (push) Successful in 21s
This commit is contained in:
@@ -28,6 +28,7 @@ const columnMap: Record<string, any> = {
|
|||||||
customerinventoryitems: historyitems.customerinventoryitem,
|
customerinventoryitems: historyitems.customerinventoryitem,
|
||||||
memberrelations: historyitems.memberrelation,
|
memberrelations: historyitems.memberrelation,
|
||||||
outgoingsepamandates: historyitems.outgoingsepamandate,
|
outgoingsepamandates: historyitems.outgoingsepamandate,
|
||||||
|
projecttypes: historyitems.projecttype,
|
||||||
};
|
};
|
||||||
|
|
||||||
const insertFieldMap: Record<string, string> = {
|
const insertFieldMap: Record<string, string> = {
|
||||||
@@ -55,6 +56,7 @@ const insertFieldMap: Record<string, string> = {
|
|||||||
customerinventoryitems: "customerinventoryitem",
|
customerinventoryitems: "customerinventoryitem",
|
||||||
memberrelations: "memberrelation",
|
memberrelations: "memberrelation",
|
||||||
outgoingsepamandates: "outgoingsepamandate",
|
outgoingsepamandates: "outgoingsepamandate",
|
||||||
|
projecttypes: "projecttype",
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseId = (value: string) => {
|
const parseId = (value: string) => {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ const HISTORY_ENTITY_LABELS: Record<string, string> = {
|
|||||||
memberrelations: "Mitgliedsverhältnisse",
|
memberrelations: "Mitgliedsverhältnisse",
|
||||||
teams: "Teams",
|
teams: "Teams",
|
||||||
outgoingsepamandates: "Ausgehende SEPA-Mandate",
|
outgoingsepamandates: "Ausgehende SEPA-Mandate",
|
||||||
|
projecttypes: "Projekttypen",
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getHistoryEntityLabel(entity: string) {
|
export function getHistoryEntityLabel(entity: string) {
|
||||||
@@ -96,6 +97,7 @@ export async function insertHistoryItem(
|
|||||||
files: "file",
|
files: "file",
|
||||||
memberrelations: "memberrelation",
|
memberrelations: "memberrelation",
|
||||||
outgoingsepamandates: "outgoingsepamandate",
|
outgoingsepamandates: "outgoingsepamandate",
|
||||||
|
projecttypes: "projecttype",
|
||||||
}
|
}
|
||||||
|
|
||||||
const fkColumn = columnMap[params.entity]
|
const fkColumn = columnMap[params.entity]
|
||||||
|
|||||||
@@ -1,314 +1,392 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
const defaultPhases = [
|
||||||
|
{ icon: 'i-heroicons-clipboard-document', label: 'Erstkontakt' },
|
||||||
|
{ icon: 'i-heroicons-wrench-screwdriver', label: 'Umsetzung' },
|
||||||
|
{ icon: 'i-heroicons-document-text', label: 'Rechnungsstellung' },
|
||||||
|
{ icon: 'i-heroicons-check', label: 'Abgeschlossen' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const quickActionOptions = [
|
||||||
|
{ label: '+ Angebot', title: 'Angebot erstellen', link: '/createDocument/edit/?type=quotes' },
|
||||||
|
{ label: '+ Kostenschätzung', title: 'Kostenschätzung erstellen', link: '/createDocument/edit/?type=costEstimates' },
|
||||||
|
{ label: '+ Auftrag', title: 'Auftrag erstellen', link: '/createDocument/edit/?type=confirmationOrders' },
|
||||||
|
{ label: '+ Lieferschein', title: 'Lieferschein erstellen', link: '/createDocument/edit/?type=deliveryNotes' },
|
||||||
|
{ label: '+ Packschein', title: 'Packschein erstellen', link: '/createDocument/edit/?type=packingSlips' },
|
||||||
|
{ label: '+ Rechnung', title: 'Rechnung erstellen', link: '/createDocument/edit/?type=invoices' },
|
||||||
|
{ label: '+ Aufgabe', title: 'Aufgabe erstellen', link: '/tasks?mode=create' },
|
||||||
|
{ label: '+ Termin', title: 'Termin erstellen', link: '/standardEntity/events/create' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const createPhase = (phase = {}, index = -1) => ({
|
||||||
defineShortcuts({
|
key: phase.key || uuidv4(),
|
||||||
'backspace': () => {
|
icon: phase.icon || '',
|
||||||
router.push("/projecttypes")
|
label: phase.label || '',
|
||||||
},
|
optional: Boolean(phase.optional),
|
||||||
'arrowleft': () => {
|
description: phase.description || '',
|
||||||
const currentIndex = Number(openTab.value)
|
quickactions: Array.isArray(phase.quickactions)
|
||||||
if(currentIndex > 0){
|
? phase.quickactions.map(action => ({ ...action }))
|
||||||
openTab.value = String(currentIndex - 1)
|
: [],
|
||||||
}
|
active: index === 0
|
||||||
},
|
})
|
||||||
'arrowright': () => {
|
|
||||||
const currentIndex = Number(openTab.value)
|
const createEmptyItem = () => ({
|
||||||
if(currentIndex < 3) {
|
name: '',
|
||||||
openTab.value = String(currentIndex + 1)
|
initialPhases: defaultPhases.map(createPhase)
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const openTab = ref("0")
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
const entities = useEntities('projecttypes')
|
||||||
|
|
||||||
const mode = ref(route.params.mode || "show")
|
const mode = computed(() => String(route.params.mode || 'show'))
|
||||||
const itemInfo = ref({
|
const itemInfo = ref(createEmptyItem())
|
||||||
name: "",
|
const loading = ref(false)
|
||||||
initialPhases: [{ "key": "f31f6fcb-34d5-41a0-9b8f-6c85062f19be", "icon": "i-heroicons-clipboard-document", "label": "Erstkontakt", "active": true, "quickactions": [] }, { "key": "41995d1f-78fa-448b-b6ea-e206645ffb89", "icon": "i-heroicons-wrench-screwdriver", "label": "Umsetzung", "quickactions": [] }, { "key": "267e78ac-9eab-4736-b9c8-4b94c1724494", "icon": "i-heroicons-document-text", "label": "Rechnungsstellung", "quickactions": [] }, { "key": "038df888-53f2-4985-b08e-776ab82df5d3", "icon": "i-heroicons-check", "label": "Abgeschlossen", "quickactions": [] } ]
|
const saving = ref(false)
|
||||||
})
|
const loadError = ref('')
|
||||||
const oldItemInfo = ref({})
|
|
||||||
|
|
||||||
const openQuickActionModal = ref(false)
|
const openQuickActionModal = ref(false)
|
||||||
const selectedKeyForQuickAction = ref("")
|
const selectedPhaseKey = ref(null)
|
||||||
|
const openArchiveModal = ref(false)
|
||||||
|
|
||||||
const setKeys = () => {
|
const isEditMode = computed(() => mode.value === 'edit' || mode.value === 'create')
|
||||||
itemInfo.value.initialPhases = itemInfo.value.initialPhases.map(i => {
|
const canSave = computed(() => {
|
||||||
return {
|
return itemInfo.value.name.trim().length > 0
|
||||||
...i,
|
&& itemInfo.value.initialPhases.length > 0
|
||||||
key: uuidv4(),
|
&& itemInfo.value.initialPhases.every(phase => phase.label.trim().length > 0)
|
||||||
quickactions: i.quickactions || []
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
itemInfo.value.initialPhases[0].active = true
|
const normalizeItem = (item) => ({
|
||||||
|
...item,
|
||||||
|
name: item?.name || '',
|
||||||
|
initialPhases: Array.isArray(item?.initialPhases)
|
||||||
|
? item.initialPhases.map(createPhase)
|
||||||
|
: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadItem = async () => {
|
||||||
|
loadError.value = ''
|
||||||
|
openQuickActionModal.value = false
|
||||||
|
openArchiveModal.value = false
|
||||||
|
|
||||||
|
if (mode.value === 'create') {
|
||||||
|
itemInfo.value = createEmptyItem()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const setupPage = async() => {
|
if (!route.params.id) {
|
||||||
|
loadError.value = 'Es wurde kein Projekttyp ausgewählt.'
|
||||||
if(mode.value === "show" ){
|
return
|
||||||
itemInfo.value = await useEntities("projecttypes").selectSingle(route.params.id,"*")
|
|
||||||
} else if (mode.value === "edit") {
|
|
||||||
itemInfo.value = await useEntities("projecttypes").selectSingle(route.params.id,"*")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(mode.value === "create") {
|
loading.value = true
|
||||||
let query = route.query
|
try {
|
||||||
|
const item = await entities.selectSingle(route.params.id, '*')
|
||||||
|
if (!item) {
|
||||||
|
loadError.value = 'Der Projekttyp wurde nicht gefunden.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
itemInfo.value = normalizeItem(item)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Projekttyp konnte nicht geladen werden', error)
|
||||||
|
loadError.value = 'Der Projekttyp konnte nicht geladen werden.'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
}
|
}
|
||||||
if(itemInfo.value) oldItemInfo.value = JSON.parse(JSON.stringify(itemInfo.value))
|
|
||||||
|
|
||||||
setKeys()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setupPage()
|
watch(
|
||||||
|
() => [route.params.mode, route.params.id],
|
||||||
|
loadItem,
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
defineShortcuts({
|
||||||
|
backspace: () => router.push('/projecttypes')
|
||||||
|
})
|
||||||
|
|
||||||
const addPhase = () => {
|
const addPhase = () => {
|
||||||
itemInfo.value.initialPhases.push({label: '', icon: ''}),
|
itemInfo.value.initialPhases.push(createPhase())
|
||||||
setKeys
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const removePhase = (phaseKey) => {
|
||||||
|
itemInfo.value.initialPhases = itemInfo.value.initialPhases
|
||||||
|
.filter(phase => phase.key !== phaseKey)
|
||||||
|
.map(createPhase)
|
||||||
|
}
|
||||||
|
|
||||||
|
const openQuickActions = (phaseKey) => {
|
||||||
|
selectedPhaseKey.value = phaseKey
|
||||||
|
openQuickActionModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const addQuickAction = (action) => {
|
||||||
|
const phase = itemInfo.value.initialPhases.find(item => item.key === selectedPhaseKey.value)
|
||||||
|
if (!phase) return
|
||||||
|
|
||||||
|
if (!phase.quickactions.some(item => item.link === action.link)) {
|
||||||
|
phase.quickactions.push({ label: action.label, link: action.link })
|
||||||
|
}
|
||||||
|
openQuickActionModal.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeQuickAction = (phase, actionIndex) => {
|
||||||
|
phase.quickactions.splice(actionIndex, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = () => ({
|
||||||
|
name: itemInfo.value.name.trim(),
|
||||||
|
initialPhases: itemInfo.value.initialPhases.map((phase, index) => ({
|
||||||
|
...createPhase(phase, index),
|
||||||
|
label: phase.label.trim(),
|
||||||
|
description: phase.description.trim()
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
if (!canSave.value || saving.value) return
|
||||||
|
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
if (mode.value === 'create') {
|
||||||
|
await entities.create(payload())
|
||||||
|
} else {
|
||||||
|
await entities.update(itemInfo.value.id, payload())
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Projekttyp konnte nicht gespeichert werden', error)
|
||||||
|
toast.add({ title: 'Projekttyp konnte nicht gespeichert werden', color: 'error' })
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const archiveItem = async () => {
|
||||||
|
if (!itemInfo.value.id || saving.value) return
|
||||||
|
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
await entities.archive(itemInfo.value.id)
|
||||||
|
toast.add({ title: 'Projekttyp archiviert' })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Projekttyp konnte nicht archiviert werden', error)
|
||||||
|
toast.add({ title: 'Projekttyp konnte nicht archiviert werden', color: 'error' })
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
openArchiveModal.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UDashboardNavbar :title="itemInfo ? itemInfo.name : (mode === 'create' ? 'Projekt erstellen' : 'Projekt bearbeiten')">
|
<UDashboardNavbar :title="itemInfo.name || (mode === 'create' ? 'Projekttyp erstellen' : 'Projekttyp')">
|
||||||
<template #left>
|
<template #left>
|
||||||
<UButton
|
<UButton icon="i-heroicons-chevron-left" variant="outline" @click="router.push('/projecttypes')">
|
||||||
icon="i-heroicons-chevron-left"
|
|
||||||
variant="outline"
|
|
||||||
@click="router.push(`/projecttypes`)"
|
|
||||||
>
|
|
||||||
Projekttypen
|
Projekttypen
|
||||||
</UButton>
|
</UButton>
|
||||||
</template>
|
</template>
|
||||||
<template #center>
|
|
||||||
<h1
|
|
||||||
v-if="itemInfo"
|
|
||||||
class="text-xl font-medium"
|
|
||||||
>{{itemInfo.name ? `Projekttyp: ${itemInfo.name}` : (mode === 'create' ? 'Projekttyp erstellen' : 'Projekttyp bearbeiten')}}</h1>
|
|
||||||
</template>
|
|
||||||
<template #right>
|
<template #right>
|
||||||
<UButton
|
<UButton v-if="isEditMode" :loading="saving" :disabled="!canSave" @click="save">
|
||||||
v-if="mode === 'edit'"
|
{{ mode === 'create' ? 'Erstellen' : 'Speichern' }}
|
||||||
@click="useEntities('projecttypes').update(itemInfo.id, itemInfo)"
|
|
||||||
>
|
|
||||||
Speichern
|
|
||||||
</UButton>
|
</UButton>
|
||||||
<UButton
|
<UButton
|
||||||
v-else-if="mode === 'create'"
|
v-if="isEditMode"
|
||||||
@click="useEntities('projecttypes').create( itemInfo)"
|
color="neutral"
|
||||||
>
|
variant="outline"
|
||||||
Erstellen
|
@click="router.push(itemInfo.id ? `/projecttypes/show/${itemInfo.id}` : '/projecttypes')"
|
||||||
</UButton>
|
|
||||||
<UButton
|
|
||||||
@click="router.push(itemInfo.id ? `/projecttypes/show/${itemInfo.id}` : `/projecttypes`)"
|
|
||||||
color="red"
|
|
||||||
class="ml-2"
|
|
||||||
v-if="mode === 'edit' || mode === 'create'"
|
|
||||||
>
|
>
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</UButton>
|
</UButton>
|
||||||
<UButton
|
<template v-if="mode === 'show' && itemInfo.id">
|
||||||
v-if="mode === 'show'"
|
<UButton variant="outline" @click="router.push(`/projecttypes/edit/${itemInfo.id}`)">
|
||||||
@click="router.push(`/projecttypes/edit/${itemInfo.id}`)"
|
|
||||||
>
|
|
||||||
Bearbeiten
|
Bearbeiten
|
||||||
</UButton>
|
</UButton>
|
||||||
|
<UButton color="error" variant="outline" @click="openArchiveModal = true">
|
||||||
|
Archivieren
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</UDashboardNavbar>
|
</UDashboardNavbar>
|
||||||
|
|
||||||
<UDashboardPanelContent>
|
<UDashboardPanelContent>
|
||||||
<UTabs
|
<div v-if="loading" class="flex justify-center py-12">
|
||||||
:items="[{label: 'Informationen'}]"
|
<UIcon name="i-heroicons-arrow-path" class="size-7 animate-spin" />
|
||||||
v-if="itemInfo.id && mode == 'show'"
|
|
||||||
v-model="openTab"
|
|
||||||
>
|
|
||||||
<template #content="{ item }">
|
|
||||||
<div v-if="item.label === 'Informationen'" class="flex flex-row">
|
|
||||||
<div class="w-1/2 mr-3">
|
|
||||||
<UCard class="mt-5">
|
|
||||||
{{itemInfo}}
|
|
||||||
</UCard>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/2">
|
|
||||||
<UCard class="mt-5">
|
<UAlert
|
||||||
|
v-else-if="loadError"
|
||||||
|
color="error"
|
||||||
|
variant="soft"
|
||||||
|
icon="i-heroicons-exclamation-triangle"
|
||||||
|
title="Fehler beim Laden"
|
||||||
|
:description="loadError"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-else-if="mode === 'show'" class="grid gap-5 lg:grid-cols-2">
|
||||||
|
<UCard>
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-lg font-medium">Phasenvorlage</h2>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="itemInfo.initialPhases.length" class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="(phase, index) in itemInfo.initialPhases"
|
||||||
|
:key="phase.key"
|
||||||
|
class="rounded-lg border border-default p-4"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<UIcon :name="phase.icon || 'i-heroicons-clipboard-document'" class="size-5" />
|
||||||
|
<span class="font-medium">{{ phase.label }}</span>
|
||||||
|
<UBadge v-if="index === 0" variant="soft">Startphase</UBadge>
|
||||||
|
<UBadge v-if="phase.optional" color="neutral" variant="soft">Optional</UBadge>
|
||||||
|
</div>
|
||||||
|
<p v-if="phase.description" class="mt-2 text-sm text-muted">{{ phase.description }}</p>
|
||||||
|
<div v-if="phase.quickactions.length" class="mt-3 flex flex-wrap gap-2">
|
||||||
|
<UBadge v-for="action in phase.quickactions" :key="action.link" color="neutral" variant="outline">
|
||||||
|
{{ action.label }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<TableEmptyState v-else label="Keine Phasen hinterlegt" />
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard>
|
||||||
<HistoryDisplay
|
<HistoryDisplay
|
||||||
type="project"
|
v-if="itemInfo.id"
|
||||||
v-if="itemInfo"
|
type="projecttypes"
|
||||||
:element-id="itemInfo.id"
|
:element-id="String(itemInfo.id)"
|
||||||
render-headline
|
render-headline
|
||||||
/>
|
/>
|
||||||
</UCard>
|
</UCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="isEditMode" class="space-y-5">
|
||||||
|
<UAlert
|
||||||
|
v-if="mode === 'edit'"
|
||||||
|
color="warning"
|
||||||
|
variant="outline"
|
||||||
|
description="Änderungen an diesem Projekttyp gelten nur für neu erstellte Projekte. Bestehende Projekte bleiben unverändert."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UCard>
|
||||||
|
<UFormField label="Name" required>
|
||||||
|
<UInput v-model="itemInfo.name" class="w-full" placeholder="Name des Projekttyps" />
|
||||||
|
</UFormField>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-medium">Initiale Phasen</h2>
|
||||||
|
<p class="text-sm text-muted">Diese Phasen werden in neue Projekte übernommen.</p>
|
||||||
|
</div>
|
||||||
|
<UButton icon="i-heroicons-plus" @click="addPhase">Phase hinzufügen</UButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UTabs>
|
|
||||||
|
|
||||||
<UForm v-else-if="mode === 'edit' || mode === 'create'">
|
|
||||||
<UAlert
|
|
||||||
color="error"
|
|
||||||
variant="outline"
|
|
||||||
class="mb-5"
|
|
||||||
v-if="mode === 'edit'"
|
|
||||||
description="Achtung Änderungen an diesem Projekttypen betreffen nur Projekte die damit neu erstellt werden. Bestehende Projekte bleiben unverändert."
|
|
||||||
/>
|
|
||||||
|
|
||||||
<UFormField
|
|
||||||
label="Name:"
|
|
||||||
>
|
|
||||||
<UInput
|
|
||||||
v-model="itemInfo.name"
|
|
||||||
/>
|
|
||||||
</UFormField>
|
|
||||||
|
|
||||||
<USeparator class="mt-5">
|
|
||||||
Initiale Phasen
|
|
||||||
</USeparator>
|
|
||||||
<UButton
|
|
||||||
class="mt-3"
|
|
||||||
@click="addPhase"
|
|
||||||
>
|
|
||||||
+ Phase
|
|
||||||
</UButton>
|
|
||||||
|
|
||||||
<table class="mt-3">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
<th class="text-left"><span class="ml-2">Name</span></th>
|
|
||||||
<th class="text-left"><span class="ml-2">Icon</span></th>
|
|
||||||
<th class="text-left"><span class="ml-2">Optional</span></th>
|
|
||||||
<th class="text-left"><span class="ml-2">Beschreibung</span></th>
|
|
||||||
<th class="text-left"><span class="ml-2">Schnellaktionen</span></th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<draggable
|
<draggable
|
||||||
v-model="itemInfo.initialPhases"
|
v-model="itemInfo.initialPhases"
|
||||||
handle=".handle"
|
handle=".phase-handle"
|
||||||
tag="tbody"
|
item-key="key"
|
||||||
itemKey="pos"
|
class="space-y-3"
|
||||||
@end="setKeys"
|
|
||||||
>
|
>
|
||||||
|
<template #item="{ element: phase, index }">
|
||||||
|
<div class="rounded-lg border border-default p-4">
|
||||||
|
<div class="mb-4 flex items-center gap-2">
|
||||||
|
<UIcon name="i-mdi-menu" class="phase-handle size-5 cursor-grab text-muted" />
|
||||||
|
<span class="font-medium">Phase {{ index + 1 }}</span>
|
||||||
|
<UBadge v-if="index === 0" variant="soft">Startphase</UBadge>
|
||||||
|
<UButton
|
||||||
|
class="ml-auto"
|
||||||
|
icon="i-heroicons-trash"
|
||||||
|
color="error"
|
||||||
|
variant="ghost"
|
||||||
|
:disabled="itemInfo.initialPhases.length === 1"
|
||||||
|
aria-label="Phase entfernen"
|
||||||
|
@click="removePhase(phase.key)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<template #item="{element: phase}">
|
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||||
<tr>
|
<UFormField label="Name" required>
|
||||||
<td>
|
<UInput v-model="phase.label" class="w-full" placeholder="Phasenname" />
|
||||||
<UIcon
|
</UFormField>
|
||||||
class="handle"
|
<UFormField label="Icon">
|
||||||
name="i-mdi-menu"
|
<UInput v-model="phase.icon" class="w-full" placeholder="i-heroicons-…" />
|
||||||
/>
|
</UFormField>
|
||||||
</td>
|
<UFormField label="Beschreibung" class="xl:col-span-2">
|
||||||
<td>
|
<UInput v-model="phase.description" class="w-full" placeholder="Optionale Beschreibung" />
|
||||||
<UInput
|
</UFormField>
|
||||||
class="my-2 ml-2"
|
</div>
|
||||||
v-model="phase.label"
|
|
||||||
placeholder="Name"
|
<div class="mt-4 flex flex-wrap items-center gap-2">
|
||||||
/>
|
<UCheckbox v-model="phase.optional" label="Optionale Phase" :disabled="index === 0" />
|
||||||
</td>
|
<UButton class="ml-auto" variant="outline" icon="i-heroicons-bolt" @click="openQuickActions(phase.key)">
|
||||||
<td>
|
Schnellaktion hinzufügen
|
||||||
<UInput
|
|
||||||
class="my-2 ml-2"
|
|
||||||
v-model="phase.icon"
|
|
||||||
placeholder="Icon"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<UCheckbox
|
|
||||||
class="my-2 ml-2"
|
|
||||||
v-model="phase.optional"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<UInput
|
|
||||||
class="my-2 ml-2"
|
|
||||||
v-model="phase.description"
|
|
||||||
placeholder="Beschreibung"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<UButton
|
|
||||||
class="my-2 ml-2"
|
|
||||||
variant="outline"
|
|
||||||
@click="openQuickActionModal = true,
|
|
||||||
selectedKeyForQuickAction = phase.key"
|
|
||||||
>+ Schnellaktion</UButton>
|
|
||||||
<UButton
|
|
||||||
@click="phase.quickactions = phase.quickactions.filter(i => i.label !== button.label)"
|
|
||||||
v-for="button in phase.quickactions"
|
|
||||||
class="ml-1"
|
|
||||||
>
|
|
||||||
{{ button.label }}
|
|
||||||
</UButton>
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
v-for="(action, actionIndex) in phase.quickactions"
|
||||||
|
:key="`${action.link}-${actionIndex}`"
|
||||||
|
color="neutral"
|
||||||
|
variant="soft"
|
||||||
|
trailing-icon="i-heroicons-x-mark"
|
||||||
|
@click="removeQuickAction(phase, actionIndex)"
|
||||||
|
>
|
||||||
|
{{ action.label }}
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
|
||||||
<UModal v-model:open="openQuickActionModal">
|
<p v-if="!canSave" class="mt-4 text-sm text-error">
|
||||||
|
Name und Phasennamen müssen ausgefüllt sein; mindestens eine Phase ist erforderlich.
|
||||||
|
</p>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
</UDashboardPanelContent>
|
||||||
|
|
||||||
|
<UModal v-model:open="openQuickActionModal" title="Schnellaktion hinzufügen">
|
||||||
<template #content>
|
<template #content>
|
||||||
<UCard>
|
<UCard>
|
||||||
|
<template #header>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
<h3 class="font-semibold">Schnellaktion hinzufügen</h3>
|
||||||
Schnellaktion hinzufügen
|
<UButton icon="i-heroicons-x-mark" color="neutral" variant="ghost" @click="openQuickActionModal = false" />
|
||||||
</h3>
|
|
||||||
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="openQuickActionModal = false" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
</template>
|
||||||
|
<div class="grid gap-2 sm:grid-cols-2">
|
||||||
<UButton
|
<UButton
|
||||||
class="my-1"
|
v-for="action in quickActionOptions"
|
||||||
@click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Angebot',link:'/createDocument/edit/?type=quotes'})">Angebot Erstellen</UButton>
|
:key="action.link"
|
||||||
<UButton
|
variant="outline"
|
||||||
class="my-1"
|
@click="addQuickAction(action)"
|
||||||
@click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Kostenschätzung',link:'/createDocument/edit/?type=costEstimates'})">Kostenschätzung Erstellen</UButton>
|
>
|
||||||
<UButton
|
{{ action.title }}
|
||||||
class="my-1"
|
</UButton>
|
||||||
@click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Auftrag',link:'/createDocument/edit/?type=confirmationOrders'})">Auftrag Erstellen</UButton>
|
|
||||||
<UButton
|
|
||||||
class="my-1"
|
|
||||||
@click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Lieferschein',link:'/createDocument/edit/?type=deliveryNotes'})">Lieferschein Erstellen</UButton>
|
|
||||||
<UButton
|
|
||||||
class="my-1"
|
|
||||||
@click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Packschein',link:'/createDocument/edit/?type=packingSlips'})">Packschein Erstellen</UButton>
|
|
||||||
<UButton
|
|
||||||
class="my-1"
|
|
||||||
@click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Rechnung',link:'/createDocument/edit/?type=invoices'})">Rechnung Erstellen</UButton>
|
|
||||||
<UButton
|
|
||||||
class="my-1"
|
|
||||||
@click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Aufgabe',link:'/tasks?mode=create'})">Aufgabe Erstellen</UButton>
|
|
||||||
<UButton
|
|
||||||
class="my-1"
|
|
||||||
@click="itemInfo.initialPhases[itemInfo.initialPhases.findIndex(i=> i.key === selectedKeyForQuickAction)].quickactions.push({label:'+ Termin',link:'/standardEntity/events/create'})">Termin Erstellen</UButton>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</UCard>
|
</UCard>
|
||||||
</template>
|
</template>
|
||||||
</UModal>
|
</UModal>
|
||||||
</td>
|
|
||||||
<td>
|
<UModal v-model:open="openArchiveModal" title="Projekttyp archivieren">
|
||||||
<UButton
|
<template #content>
|
||||||
class="my-2 ml-2"
|
<UCard>
|
||||||
variant="outline"
|
<template #header>
|
||||||
color="error"
|
<h3 class="font-semibold">Projekttyp archivieren?</h3>
|
||||||
@click="itemInfo.initialPhases = itemInfo.initialPhases.filter(i => i !== phase)"
|
|
||||||
>X</UButton>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
</template>
|
||||||
</draggable>
|
<p>„{{ itemInfo.name }}“ wird aus der Projekttyp-Auswahl ausgeblendet.</p>
|
||||||
</table>
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
</UForm>
|
<UButton color="neutral" variant="outline" @click="openArchiveModal = false">Abbrechen</UButton>
|
||||||
</UDashboardPanelContent>
|
<UButton color="error" :loading="saving" @click="archiveItem">Archivieren</UButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
</template>
|
||||||
|
</UModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -3,17 +3,16 @@
|
|||||||
|
|
||||||
defineShortcuts({
|
defineShortcuts({
|
||||||
'/': () => {
|
'/': () => {
|
||||||
//console.log(searchinput)
|
document.getElementById("searchinput")?.focus()
|
||||||
//searchinput.value.focus()
|
|
||||||
document.getElementById("searchinput").focus()
|
|
||||||
},
|
},
|
||||||
'+': () => {
|
'+': () => {
|
||||||
router.push("/projects/create")
|
router.push("/projecttypes/create")
|
||||||
},
|
},
|
||||||
'Enter': {
|
'Enter': {
|
||||||
usingInput: true,
|
usingInput: true,
|
||||||
handler: () => {
|
handler: () => {
|
||||||
router.push(`/projecttypes/show/${filteredRows.value[selectedItem.value].id}`)
|
const selected = filteredRows.value[selectedItem.value]
|
||||||
|
if (selected) router.push(`/projecttypes/show/${selected.id}`)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'arrowdown': () => {
|
'arrowdown': () => {
|
||||||
@@ -38,10 +37,21 @@ const tempStore = useTempStore()
|
|||||||
|
|
||||||
const items = ref([])
|
const items = ref([])
|
||||||
const selectedItem = ref(0)
|
const selectedItem = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
const loadError = ref('')
|
||||||
|
|
||||||
|
|
||||||
const setup = async () => {
|
const setup = async () => {
|
||||||
items.value = await useEntities("projecttypes").select()
|
loading.value = true
|
||||||
|
loadError.value = ''
|
||||||
|
try {
|
||||||
|
items.value = await useEntities("projecttypes").select('*', 'name', true)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Projekttypen konnten nicht geladen werden', error)
|
||||||
|
loadError.value = 'Die Projekttypen konnten nicht geladen werden.'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setup()
|
setup()
|
||||||
@@ -62,6 +72,11 @@ const filteredRows = computed(() => {
|
|||||||
return useListFilter(searchString.value, items.value)
|
return useListFilter(searchString.value, items.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const openProjecttype = (row) => {
|
||||||
|
const item = row?.original || row
|
||||||
|
if (item?.id) router.push(`/projecttypes/show/${item.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -87,11 +102,13 @@ const filteredRows = computed(() => {
|
|||||||
</UDashboardNavbar>
|
</UDashboardNavbar>
|
||||||
|
|
||||||
<UTable
|
<UTable
|
||||||
|
v-if="!loadError"
|
||||||
:data="filteredRows"
|
:data="filteredRows"
|
||||||
:columns="normalizeTableColumns(columns)"
|
:columns="normalizeTableColumns(columns)"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||||
:on-select="(i) => router.push(`/projecttypes/show/${i.id}`) "
|
:loading="loading"
|
||||||
|
:on-select="openProjecttype"
|
||||||
>
|
>
|
||||||
<template #name-cell="{row}">
|
<template #name-cell="{row}">
|
||||||
<span class="text-primary-500 font-bold" v-if="row.original === filteredRows[selectedItem]">{{ row.original.name }}</span>
|
<span class="text-primary-500 font-bold" v-if="row.original === filteredRows[selectedItem]">{{ row.original.name }}</span>
|
||||||
@@ -101,6 +118,15 @@ const filteredRows = computed(() => {
|
|||||||
<TableEmptyState label="Keine Projekttypen anzuzeigen" />
|
<TableEmptyState label="Keine Projekttypen anzuzeigen" />
|
||||||
</template>
|
</template>
|
||||||
</UTable>
|
</UTable>
|
||||||
|
<UAlert
|
||||||
|
v-else
|
||||||
|
class="m-5"
|
||||||
|
color="error"
|
||||||
|
variant="soft"
|
||||||
|
icon="i-heroicons-exclamation-triangle"
|
||||||
|
title="Fehler beim Laden"
|
||||||
|
:description="loadError"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
Reference in New Issue
Block a user