This commit is contained in:
529
frontend/pages/organisation/plantafel.vue
Normal file
529
frontend/pages/organisation/plantafel.vue
Normal file
@@ -0,0 +1,529 @@
|
|||||||
|
<script setup>
|
||||||
|
import deLocale from "@fullcalendar/core/locales/de"
|
||||||
|
import FullCalendar from "@fullcalendar/vue3"
|
||||||
|
import interactionPlugin from "@fullcalendar/interaction"
|
||||||
|
import resourceTimelinePlugin from "@fullcalendar/resource-timeline"
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const profileStore = useProfileStore()
|
||||||
|
const toast = useToast()
|
||||||
|
const { $dayjs } = useNuxtApp()
|
||||||
|
const { list: listStaffTimeSpans, createEntry, update: updateStaffTimeEntry } = useStaffTime()
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
const savingAbsence = ref(false)
|
||||||
|
const selectedType = ref("all")
|
||||||
|
const visibleRange = ref({
|
||||||
|
from: $dayjs().startOf("month").format("YYYY-MM-DD"),
|
||||||
|
to: $dayjs().endOf("month").format("YYYY-MM-DD")
|
||||||
|
})
|
||||||
|
const lastRangeKey = ref("")
|
||||||
|
const resources = ref([])
|
||||||
|
const events = ref([])
|
||||||
|
const profiles = ref([])
|
||||||
|
const inventoryitems = ref([])
|
||||||
|
|
||||||
|
const isAbsenceModalOpen = ref(false)
|
||||||
|
const absenceForm = reactive({
|
||||||
|
mode: "create",
|
||||||
|
entry: null,
|
||||||
|
userId: "",
|
||||||
|
type: "vacation",
|
||||||
|
startDate: $dayjs().format("YYYY-MM-DD"),
|
||||||
|
endDate: $dayjs().format("YYYY-MM-DD"),
|
||||||
|
description: ""
|
||||||
|
})
|
||||||
|
|
||||||
|
const setAbsenceDateToToday = (field) => {
|
||||||
|
absenceForm[field] = $dayjs().format("YYYY-MM-DD")
|
||||||
|
}
|
||||||
|
|
||||||
|
const resourceTypeOptions = [
|
||||||
|
{ label: "Alle Ressourcen", value: "all" },
|
||||||
|
{ label: "Profile", value: "Profile" },
|
||||||
|
{ label: "Inventarartikel", value: "Inventarartikel" }
|
||||||
|
]
|
||||||
|
|
||||||
|
const absenceTypeOptions = [
|
||||||
|
{ label: "Urlaub", value: "vacation" },
|
||||||
|
{ label: "Krank", value: "sick" }
|
||||||
|
]
|
||||||
|
|
||||||
|
const profileOptions = computed(() =>
|
||||||
|
profiles.value
|
||||||
|
.filter((profile) => !profile.archived && profile.user_id)
|
||||||
|
.map((profile) => ({
|
||||||
|
label: profile.full_name || profile.fullName || [profile.first_name, profile.last_name].filter(Boolean).join(" ") || profile.email,
|
||||||
|
value: profile.user_id
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const absenceModalTitle = computed(() =>
|
||||||
|
absenceForm.mode === "edit"
|
||||||
|
? (absenceForm.type === "sick" ? "Krankmeldung bearbeiten" : "Urlaub bearbeiten")
|
||||||
|
: (absenceForm.type === "sick" ? "Krank eintragen" : "Urlaub eintragen")
|
||||||
|
)
|
||||||
|
|
||||||
|
const visibleResources = computed(() => {
|
||||||
|
if (selectedType.value === "all") return resources.value
|
||||||
|
return resources.value.filter((resource) => resource.type === selectedType.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const visibleResourceIds = computed(() => new Set(visibleResources.value.map((resource) => resource.id)))
|
||||||
|
|
||||||
|
const visibleEvents = computed(() => {
|
||||||
|
if (selectedType.value === "all") return events.value
|
||||||
|
|
||||||
|
return events.value.filter((event) =>
|
||||||
|
(event.resourceIds || []).some((resourceId) => visibleResourceIds.value.has(resourceId))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const calendarOptions = computed(() => ({
|
||||||
|
schedulerLicenseKey: "CC-Attribution-NonCommercial-NoDerivatives",
|
||||||
|
locale: deLocale,
|
||||||
|
plugins: [resourceTimelinePlugin, interactionPlugin],
|
||||||
|
initialView: "resourceTimelineWeek",
|
||||||
|
headerToolbar: {
|
||||||
|
left: "prev,next today",
|
||||||
|
center: "title",
|
||||||
|
right: "resourceTimelineDay,resourceTimelineWeek,resourceTimelineMonth"
|
||||||
|
},
|
||||||
|
resourceAreaWidth: "280px",
|
||||||
|
resourceGroupField: "type",
|
||||||
|
resourceOrder: "type,title",
|
||||||
|
resources: visibleResources.value,
|
||||||
|
events: visibleEvents.value,
|
||||||
|
nowIndicator: true,
|
||||||
|
selectable: true,
|
||||||
|
height: "calc(100vh - 250px)",
|
||||||
|
slotMinWidth: 80,
|
||||||
|
resourceAreaColumns: [
|
||||||
|
{
|
||||||
|
field: "title",
|
||||||
|
headerContent: "Ressource"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
views: {
|
||||||
|
resourceTimelineDay: {
|
||||||
|
type: "resourceTimeline",
|
||||||
|
duration: { days: 1 },
|
||||||
|
buttonText: "Tag",
|
||||||
|
slotDuration: { hours: 1 }
|
||||||
|
},
|
||||||
|
resourceTimelineWeek: {
|
||||||
|
type: "resourceTimeline",
|
||||||
|
duration: { days: 7 },
|
||||||
|
buttonText: "Woche",
|
||||||
|
slotDuration: { hours: 3 },
|
||||||
|
weekends: false,
|
||||||
|
slotMinTime: "06:00:00",
|
||||||
|
slotMaxTime: "21:00:00"
|
||||||
|
},
|
||||||
|
resourceTimelineMonth: {
|
||||||
|
type: "resourceTimeline",
|
||||||
|
duration: { months: 1 },
|
||||||
|
buttonText: "Monat"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
select(info) {
|
||||||
|
const resourceIds = info.resource?.id ? [info.resource.id] : []
|
||||||
|
router.push(`/standardEntity/events/create?startDate=${encodeURIComponent(info.startStr)}&endDate=${encodeURIComponent(info.endStr)}&resources=${encodeURIComponent(JSON.stringify(resourceIds))}&source=timeline`)
|
||||||
|
},
|
||||||
|
eventClick(info) {
|
||||||
|
if (info.event.extendedProps.entrytype === "staff-absence") {
|
||||||
|
openAbsenceModal(info.event.extendedProps.absenceType, {
|
||||||
|
entry: info.event.extendedProps.absenceEntry,
|
||||||
|
userId: info.event.extendedProps.absenceEntry?.user_id || "",
|
||||||
|
startDate: $dayjs(info.event.extendedProps.absenceEntry?.started_at).format("YYYY-MM-DD"),
|
||||||
|
endDate: $dayjs(info.event.extendedProps.absenceEntry?.stopped_at || info.event.extendedProps.absenceEntry?.started_at).format("YYYY-MM-DD"),
|
||||||
|
description: info.event.extendedProps.absenceEntry?.description || ""
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push(`/standardEntity/events/show/${info.event.extendedProps.eventId}`)
|
||||||
|
},
|
||||||
|
datesSet(info) {
|
||||||
|
const nextFrom = $dayjs(info.start).format("YYYY-MM-DD")
|
||||||
|
const nextTo = $dayjs(info.end).subtract(1, "day").format("YYYY-MM-DD")
|
||||||
|
const nextKey = `${nextFrom}:${nextTo}`
|
||||||
|
|
||||||
|
if (nextKey === lastRangeKey.value) return
|
||||||
|
|
||||||
|
lastRangeKey.value = nextKey
|
||||||
|
visibleRange.value = { from: nextFrom, to: nextTo }
|
||||||
|
loadPlanningBoard()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
function resolveEventColor(eventType) {
|
||||||
|
return profileStore.ownTenant?.calendarConfig?.eventTypes?.find((item) => item.label === eventType)?.color || "#111827"
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveEventTitle(event, projectsById) {
|
||||||
|
if (event.name) return event.name
|
||||||
|
if (event.project && projectsById.has(event.project)) return projectsById.get(event.project).name
|
||||||
|
return "Planung"
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProfileLabel(profile) {
|
||||||
|
return profile?.full_name || profile?.fullName || [profile?.first_name, profile?.last_name].filter(Boolean).join(" ") || profile?.email || `Profil ${profile?.id || ""}`.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAbsenceTitle(entry) {
|
||||||
|
const baseLabel = entry.type === "sick" ? "Krank" : "Urlaub"
|
||||||
|
return entry.description ? `${baseLabel}: ${entry.description}` : baseLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAbsenceColor(type) {
|
||||||
|
return type === "sick" ? "#dc2626" : "#d97706"
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildResources({ profiles, inventoryitems }) {
|
||||||
|
return [
|
||||||
|
...profiles
|
||||||
|
.filter((profile) => !profile.archived)
|
||||||
|
.map((profile) => ({
|
||||||
|
id: `P-${profile.id}`,
|
||||||
|
type: "Profile",
|
||||||
|
title: getProfileLabel(profile)
|
||||||
|
})),
|
||||||
|
...inventoryitems
|
||||||
|
.filter((item) => !item.archived && item.usePlanning)
|
||||||
|
.map((item) => ({
|
||||||
|
id: `I-${item.id}`,
|
||||||
|
type: "Inventarartikel",
|
||||||
|
title: item.name
|
||||||
|
}))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildEvents({ rawEvents, projectsById }) {
|
||||||
|
const mappedEvents = rawEvents
|
||||||
|
.filter((event) => !event.archived)
|
||||||
|
.map((event) => {
|
||||||
|
const resourceIds = [
|
||||||
|
...(event.profiles || []).map((profileId) => `P-${profileId}`),
|
||||||
|
...(event.inventoryitems || []).map((itemId) => `I-${itemId}`)
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: resolveEventTitle(event, projectsById),
|
||||||
|
start: event.startDate,
|
||||||
|
end: event.endDate,
|
||||||
|
resourceIds,
|
||||||
|
backgroundColor: resolveEventColor(event.eventtype),
|
||||||
|
borderColor: resolveEventColor(event.eventtype),
|
||||||
|
textColor: "#ffffff",
|
||||||
|
entrytype: "event",
|
||||||
|
eventId: event.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((event) => event.resourceIds.length > 0)
|
||||||
|
|
||||||
|
return mappedEvents
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAbsenceEvents(spansByUserId) {
|
||||||
|
const profileByUserId = new Map(
|
||||||
|
profiles.value
|
||||||
|
.filter((profile) => profile.user_id)
|
||||||
|
.map((profile) => [profile.user_id, profile])
|
||||||
|
)
|
||||||
|
|
||||||
|
return spansByUserId.flatMap(({ userId, spans }) => {
|
||||||
|
const profile = profileByUserId.get(userId)
|
||||||
|
if (!profile) return []
|
||||||
|
|
||||||
|
return (spans || [])
|
||||||
|
.filter((entry) => ["vacation", "sick"].includes(entry.type) && entry.state !== "rejected")
|
||||||
|
.map((entry) => ({
|
||||||
|
title: getAbsenceTitle(entry),
|
||||||
|
start: $dayjs(entry.started_at).startOf("day").toISOString(),
|
||||||
|
end: $dayjs(entry.stopped_at || entry.started_at).add(1, "day").startOf("day").toISOString(),
|
||||||
|
resourceIds: [`P-${profile.id}`],
|
||||||
|
allDay: true,
|
||||||
|
backgroundColor: getAbsenceColor(entry.type),
|
||||||
|
borderColor: getAbsenceColor(entry.type),
|
||||||
|
textColor: "#ffffff",
|
||||||
|
entrytype: "staff-absence",
|
||||||
|
absenceType: entry.type,
|
||||||
|
absenceEntry: entry
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetAbsenceForm() {
|
||||||
|
absenceForm.mode = "create"
|
||||||
|
absenceForm.entry = null
|
||||||
|
absenceForm.userId = profileOptions.value[0]?.value || ""
|
||||||
|
absenceForm.type = "vacation"
|
||||||
|
absenceForm.startDate = $dayjs().format("YYYY-MM-DD")
|
||||||
|
absenceForm.endDate = $dayjs().format("YYYY-MM-DD")
|
||||||
|
absenceForm.description = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAbsenceModal(type = "vacation", preset = {}) {
|
||||||
|
absenceForm.mode = preset.entry ? "edit" : "create"
|
||||||
|
absenceForm.entry = preset.entry || null
|
||||||
|
absenceForm.type = type
|
||||||
|
absenceForm.userId = preset.userId || profileOptions.value[0]?.value || ""
|
||||||
|
absenceForm.startDate = preset.startDate || $dayjs().format("YYYY-MM-DD")
|
||||||
|
absenceForm.endDate = preset.endDate || preset.startDate || $dayjs().format("YYYY-MM-DD")
|
||||||
|
absenceForm.description = preset.description || ""
|
||||||
|
isAbsenceModalOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveAbsence() {
|
||||||
|
if (!absenceForm.userId) {
|
||||||
|
toast.add({ title: "Profil fehlt", description: "Bitte ein Profil auswählen.", color: "orange" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!absenceForm.startDate || !absenceForm.endDate) {
|
||||||
|
toast.add({ title: "Zeitraum fehlt", description: "Bitte Start- und Enddatum angeben.", color: "orange" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dayjs(absenceForm.endDate).isBefore($dayjs(absenceForm.startDate))) {
|
||||||
|
toast.add({ title: "Zeitraum ungültig", description: "Das Enddatum muss am oder nach dem Startdatum liegen.", color: "orange" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
savingAbsence.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
start: $dayjs(absenceForm.startDate).startOf("day").toISOString(),
|
||||||
|
end: $dayjs(absenceForm.endDate).endOf("day").toISOString(),
|
||||||
|
type: absenceForm.type,
|
||||||
|
description: absenceForm.description || "",
|
||||||
|
user_id: absenceForm.userId
|
||||||
|
}
|
||||||
|
|
||||||
|
if (absenceForm.mode === "edit" && absenceForm.entry) {
|
||||||
|
await updateStaffTimeEntry(absenceForm.entry, payload)
|
||||||
|
toast.add({ title: "Abwesenheit aktualisiert", color: "green" })
|
||||||
|
} else {
|
||||||
|
await createEntry(payload)
|
||||||
|
toast.add({ title: absenceForm.type === "sick" ? "Krankmeldung eingetragen" : "Urlaub eingetragen", color: "green" })
|
||||||
|
}
|
||||||
|
|
||||||
|
isAbsenceModalOpen.value = false
|
||||||
|
await loadPlanningBoard()
|
||||||
|
} catch (error) {
|
||||||
|
console.error("saveAbsence failed", error)
|
||||||
|
toast.add({
|
||||||
|
title: "Abwesenheit konnte nicht gespeichert werden",
|
||||||
|
description: error?.message || "Bitte Eingaben prüfen und erneut versuchen.",
|
||||||
|
color: "red"
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
savingAbsence.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPlanningBoard() {
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [rawEvents, projects, profileRows, inventoryItemRows] = await Promise.all([
|
||||||
|
useEntities("events").select(),
|
||||||
|
useEntities("projects").select(),
|
||||||
|
useEntities("profiles").select(),
|
||||||
|
useEntities("inventoryitems").select()
|
||||||
|
])
|
||||||
|
|
||||||
|
const absenceSpansByUserId = await Promise.all(
|
||||||
|
(profileRows || [])
|
||||||
|
.filter((profile) => profile.user_id)
|
||||||
|
.map(async (profile) => ({
|
||||||
|
userId: profile.user_id,
|
||||||
|
spans: await listStaffTimeSpans({
|
||||||
|
user_id: profile.user_id,
|
||||||
|
from: visibleRange.value.from,
|
||||||
|
to: visibleRange.value.to
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const projectsById = new Map((projects || []).map((project) => [project.id, project]))
|
||||||
|
profiles.value = profileRows || []
|
||||||
|
inventoryitems.value = inventoryItemRows || []
|
||||||
|
|
||||||
|
if (!absenceForm.userId) {
|
||||||
|
absenceForm.userId = profileOptions.value[0]?.value || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
resources.value = buildResources({
|
||||||
|
profiles: profiles.value,
|
||||||
|
inventoryitems: inventoryitems.value
|
||||||
|
})
|
||||||
|
|
||||||
|
events.value = buildEvents({
|
||||||
|
rawEvents: rawEvents || [],
|
||||||
|
projectsById
|
||||||
|
}).concat(buildAbsenceEvents(absenceSpansByUserId))
|
||||||
|
} catch (error) {
|
||||||
|
console.error("loadPlanningBoard failed", error)
|
||||||
|
toast.add({
|
||||||
|
title: "Plantafel konnte nicht geladen werden",
|
||||||
|
description: "Die Ressourcen oder Planungen konnten nicht abgerufen werden.",
|
||||||
|
color: "red"
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
resetAbsenceForm()
|
||||||
|
loadPlanningBoard()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<UDashboardNavbar title="Plantafel">
|
||||||
|
<template #right>
|
||||||
|
<UButton
|
||||||
|
color="gray"
|
||||||
|
variant="ghost"
|
||||||
|
icon="i-heroicons-arrow-path"
|
||||||
|
:loading="loading"
|
||||||
|
@click="loadPlanningBoard"
|
||||||
|
>
|
||||||
|
Aktualisieren
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UDashboardNavbar>
|
||||||
|
|
||||||
|
<UDashboardToolbar>
|
||||||
|
<template #left>
|
||||||
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<USelectMenu
|
||||||
|
v-model="selectedType"
|
||||||
|
:options="resourceTypeOptions"
|
||||||
|
value-attribute="value"
|
||||||
|
option-attribute="label"
|
||||||
|
:clearable="false"
|
||||||
|
class="min-w-[220px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #right>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<UButton
|
||||||
|
color="amber"
|
||||||
|
variant="soft"
|
||||||
|
icon="i-heroicons-sun"
|
||||||
|
@click="openAbsenceModal('vacation')"
|
||||||
|
>
|
||||||
|
Urlaub
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
color="rose"
|
||||||
|
variant="soft"
|
||||||
|
icon="i-heroicons-heart"
|
||||||
|
@click="openAbsenceModal('sick')"
|
||||||
|
>
|
||||||
|
Krank
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UDashboardToolbar>
|
||||||
|
|
||||||
|
<UDashboardPanelContent class="p-5">
|
||||||
|
<div v-if="loading" class="space-y-3">
|
||||||
|
<USkeleton class="h-12 w-full" />
|
||||||
|
<USkeleton class="h-[70vh] w-full" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UAlert
|
||||||
|
v-else-if="visibleResources.length === 0"
|
||||||
|
icon="i-heroicons-calendar-days"
|
||||||
|
title="Keine planbaren Ressourcen vorhanden"
|
||||||
|
description="Lege Profile an oder aktiviere die Plantafel-Nutzung bei Inventarartikeln, damit hier Ressourcen erscheinen."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FullCalendar
|
||||||
|
v-else
|
||||||
|
:options="calendarOptions"
|
||||||
|
/>
|
||||||
|
</UDashboardPanelContent>
|
||||||
|
|
||||||
|
<UModal v-model="isAbsenceModalOpen">
|
||||||
|
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||||
|
{{ absenceModalTitle }}
|
||||||
|
</h3>
|
||||||
|
<UButton
|
||||||
|
color="gray"
|
||||||
|
variant="ghost"
|
||||||
|
icon="i-heroicons-x-mark-20-solid"
|
||||||
|
class="-my-1"
|
||||||
|
@click="isAbsenceModalOpen = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<UFormGroup label="Profil">
|
||||||
|
<USelectMenu
|
||||||
|
v-model="absenceForm.userId"
|
||||||
|
:options="profileOptions"
|
||||||
|
value-attribute="value"
|
||||||
|
option-attribute="label"
|
||||||
|
searchable
|
||||||
|
/>
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="Typ">
|
||||||
|
<USelectMenu
|
||||||
|
v-model="absenceForm.type"
|
||||||
|
:options="absenceTypeOptions"
|
||||||
|
value-attribute="value"
|
||||||
|
option-attribute="label"
|
||||||
|
/>
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<UFormGroup label="Start">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UInput v-model="absenceForm.startDate" type="date" class="flex-1" />
|
||||||
|
<UButton color="gray" variant="soft" label="Heute" @click="setAbsenceDateToToday('startDate')" />
|
||||||
|
</div>
|
||||||
|
</UFormGroup>
|
||||||
|
<UFormGroup label="Ende">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UInput v-model="absenceForm.endDate" type="date" class="flex-1" />
|
||||||
|
<UButton color="gray" variant="soft" label="Heute" @click="setAbsenceDateToToday('endDate')" />
|
||||||
|
</div>
|
||||||
|
</UFormGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UFormGroup label="Notiz">
|
||||||
|
<UTextarea
|
||||||
|
v-model="absenceForm.description"
|
||||||
|
:placeholder="absenceForm.type === 'sick' ? 'z. B. Krankmeldung eingegangen' : 'z. B. Sommerurlaub'"
|
||||||
|
/>
|
||||||
|
</UFormGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<UButton color="gray" variant="soft" @click="isAbsenceModalOpen = false">
|
||||||
|
Abbrechen
|
||||||
|
</UButton>
|
||||||
|
<UButton color="primary" :loading="savingAbsence" @click="saveAbsence">
|
||||||
|
Speichern
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
</UModal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user