Plantafel um Mitarbeiterdetails mit Resturlaub erweitern

This commit is contained in:
2026-04-29 16:12:13 +02:00
parent 898a5459fa
commit cfc5efb556

View File

@@ -30,10 +30,14 @@ const events = ref([])
const profiles = ref([]) const profiles = ref([])
const inventoryitems = ref([]) const inventoryitems = ref([])
const savingQuickConfig = ref(false) const savingQuickConfig = ref(false)
const isQuickConfigModalOpen = ref(false)
const showQuickConfigEditor = ref(true)
const showQuickPresetManagement = ref(false)
const quickEntryConfig = reactive({ const quickEntryConfig = reactive({
name: "Quick-Eintrag", name: "Quick-Eintrag",
color: "#2563eb" color: "#2563eb"
}) })
const newQuickPresetName = ref("")
const quickEntryColorOptions = [ const quickEntryColorOptions = [
{ label: "Blau", value: "#2563eb" }, { label: "Blau", value: "#2563eb" },
{ label: "Grün", value: "#16a34a" }, { label: "Grün", value: "#16a34a" },
@@ -46,6 +50,11 @@ const quickEntryColorOptions = [
] ]
const isAbsenceModalOpen = ref(false) const isAbsenceModalOpen = ref(false)
const isProfileDetailsModalOpen = ref(false)
const loadingProfileVacation = ref(false)
const selectedProfile = ref(null)
const selectedProfileVacationSummary = ref(null)
const profileVacationRequestId = ref(0)
const absenceForm = reactive({ const absenceForm = reactive({
mode: "create", mode: "create",
entry: null, entry: null,
@@ -142,8 +151,14 @@ const absenceModalTitle = computed(() =>
: (absenceForm.type === "sick" ? "Krank eintragen" : "Urlaub eintragen") : (absenceForm.type === "sick" ? "Krank eintragen" : "Urlaub eintragen")
) )
const tenantCalendarConfig = computed(() =>
auth.activeTenantData?.calendarConfig
|| profileStore.ownTenant?.calendarConfig
|| {}
)
const resolvedQuickEntryConfig = computed(() => { const resolvedQuickEntryConfig = computed(() => {
const config = profileStore.ownTenant?.calendarConfig?.quickEntry || {} const config = tenantCalendarConfig.value?.quickEntry || {}
return { return {
name: config.name || "Quick-Eintrag", name: config.name || "Quick-Eintrag",
@@ -151,9 +166,73 @@ const resolvedQuickEntryConfig = computed(() => {
} }
}) })
const activeQuickEntryConfig = computed(() => ({
name: quickEntryConfig.name?.trim() || resolvedQuickEntryConfig.value.name,
color: quickEntryConfig.color || resolvedQuickEntryConfig.value.color
}))
const quickEntryPresets = computed(() => {
const rawPresets = tenantCalendarConfig.value?.quickEntryPresets
if (!Array.isArray(rawPresets)) return []
return rawPresets
.filter((preset) => preset?.name && preset?.color)
.map((preset) => ({
name: String(preset.name),
color: String(preset.color)
}))
})
const resourcesById = computed(() => new Map(resources.value.map((resource) => [resource.id, resource])))
const currentVacationYear = computed(() => $dayjs().year())
const selectedProfileTeamsLabel = computed(() =>
(selectedProfile.value?.teams || [])
.map((team) => team?.name)
.filter(Boolean)
.join(", ") || "Keine Teams zugeordnet"
)
const selectedProfileBranchesLabel = computed(() => {
const profile = selectedProfile.value
if (!profile) return "Keine Niederlassung"
const labels = [
profile.branch?.name,
...(profile.branches || []).map((branch) => branch?.name)
].filter(Boolean)
return [...new Set(labels)].join(", ") || "Keine Niederlassung"
})
const selectedProfileVacationDaysTaken = computed(() =>
Number(selectedProfileVacationSummary.value?.sumVacationDays || 0)
)
const selectedProfileAnnualLeaveDays = computed(() => {
const value = selectedProfile.value?.annual_paid_leave_days
return value === null || value === undefined || value === "" ? null : Number(value)
})
const selectedProfileRemainingVacationDays = computed(() => {
if (selectedProfileAnnualLeaveDays.value === null) return null
return Number((selectedProfileAnnualLeaveDays.value - selectedProfileVacationDaysTaken.value).toFixed(2))
})
const visibleResources = computed(() => { const visibleResources = computed(() => {
if (selectedType.value === "all") return resources.value if (selectedType.value === "all") return resources.value
return resources.value.filter((resource) => resource.type === selectedType.value)
const visibleMap = new Map()
resources.value
.filter((resource) => resource.type === selectedType.value)
.forEach((resource) => {
let current = resource
while (current) {
visibleMap.set(current.id, current)
current = current.parentId ? resourcesById.value.get(current.parentId) : null
}
})
return resources.value.filter((resource) => visibleMap.has(resource.id))
}) })
const visibleResourceIds = computed(() => new Set(visibleResources.value.map((resource) => resource.id))) const visibleResourceIds = computed(() => new Set(visibleResources.value.map((resource) => resource.id)))
@@ -188,6 +267,28 @@ const calendarOptions = computed(() => ({
headerContent: "Ressource" headerContent: "Ressource"
} }
], ],
resourceLabelDidMount(info) {
const { resource } = info
const isProfileResource = resource.extendedProps?.resourceKind === "profile" && resource.extendedProps?.profileId
if (!isProfileResource) return
info.el.classList.add("cursor-pointer")
info.el.setAttribute("role", "button")
info.el.setAttribute("tabindex", "0")
info.el.setAttribute("title", "Mitarbeiterdetails öffnen")
const openProfile = () => openProfileDetails(resource.extendedProps.profileId)
const onKeydown = (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault()
openProfile()
}
}
info.el.addEventListener("click", openProfile)
info.el.addEventListener("keydown", onKeydown)
},
views: { views: {
resourceTimelineDay: { resourceTimelineDay: {
type: "resourceTimeline", type: "resourceTimeline",
@@ -245,17 +346,17 @@ const calendarOptions = computed(() => ({
})) }))
function resolveEventColor(eventType) { function resolveEventColor(eventType) {
return profileStore.ownTenant?.calendarConfig?.eventTypes?.find((item) => item.label === eventType)?.color || "#111827" return tenantCalendarConfig.value?.eventTypes?.find((item) => item.label === eventType)?.color || "#111827"
} }
function resolveEventTitle(event, projectsById) { function resolveEventTitle(event, projectsById) {
if (event.name) return event.name if (event.name) return event.name
if (event.project && projectsById.has(event.project)) return projectsById.get(event.project).name if (event.project && projectsById.has(event.project)) return projectsById.get(event.project).name
return event.quick ? resolvedQuickEntryConfig.value.name : "Planung" return event.quick ? activeQuickEntryConfig.value.name : "Planung"
} }
function resolveRenderedEventColor(event) { function resolveRenderedEventColor(event) {
if (event?.quick) return resolvedQuickEntryConfig.value.color if (event?.quick) return activeQuickEntryConfig.value.color
return resolveEventColor(event.eventtype) return resolveEventColor(event.eventtype)
} }
@@ -274,7 +375,11 @@ function getAbsenceColor(type) {
function getTeamLabel(team) { function getTeamLabel(team) {
if (!team) return "Team" if (!team) return "Team"
return team.branch?.name ? `${team.name} (${team.branch.name})` : team.name return team.name
}
function getBranchResourceId(branchId) {
return `B-${branchId}`
} }
function getProfileResourceIds(profile) { function getProfileResourceIds(profile) {
@@ -305,8 +410,10 @@ function normalizeSelectedResourceIds(resourceId) {
} }
function buildResources({ profiles, inventoryitems }) { function buildResources({ profiles, inventoryitems }) {
const branchResources = []
const teamResources = [] const teamResources = []
const profileResources = [] const profileResources = []
const topLevelTeamGroupId = "TEAM-UNBOUND"
profiles profiles
.filter((profile) => !profile.archived) .filter((profile) => !profile.archived)
@@ -317,17 +424,40 @@ function buildResources({ profiles, inventoryitems }) {
profileResources.push({ profileResources.push({
id: `P-${profile.id}`, id: `P-${profile.id}`,
type: "Profile", type: "Profile",
title: getProfileLabel(profile) title: getProfileLabel(profile),
resourceKind: "profile",
profileId: profile.id
}) })
return return
} }
assignedTeams.forEach((team) => { assignedTeams.forEach((team) => {
if (team?.branch?.id) {
const branchResourceId = getBranchResourceId(team.branch.id)
if (!branchResources.find((resource) => resource.id === branchResourceId)) {
branchResources.push({
id: branchResourceId,
type: "Niederlassung",
title: team.branch.name
})
}
}
if (!team?.branch?.id && !branchResources.find((resource) => resource.id === topLevelTeamGroupId)) {
branchResources.push({
id: topLevelTeamGroupId,
type: "Niederlassung",
title: "Übergreifende Teams"
})
}
if (!teamResources.find((resource) => resource.id === `T-${team.id}`)) { if (!teamResources.find((resource) => resource.id === `T-${team.id}`)) {
teamResources.push({ teamResources.push({
id: `T-${team.id}`, id: `T-${team.id}`,
type: "Team", type: "Team",
title: getTeamLabel(team) title: getTeamLabel(team),
parentId: team?.branch?.id ? getBranchResourceId(team.branch.id) : topLevelTeamGroupId
}) })
} }
@@ -335,7 +465,9 @@ function buildResources({ profiles, inventoryitems }) {
id: `T-${team.id}:P-${profile.id}`, id: `T-${team.id}:P-${profile.id}`,
parentId: `T-${team.id}`, parentId: `T-${team.id}`,
type: "Profile", type: "Profile",
title: getProfileLabel(profile) title: getProfileLabel(profile),
resourceKind: "profile",
profileId: profile.id
}) })
}) })
}) })
@@ -348,7 +480,7 @@ function buildResources({ profiles, inventoryitems }) {
title: item.name title: item.name
})) }))
return [...teamResources, ...profileResources, ...inventoryResources] return [...branchResources, ...teamResources, ...profileResources, ...inventoryResources]
} }
function buildEvents({ rawEvents, projectsById }) { function buildEvents({ rawEvents, projectsById }) {
@@ -443,19 +575,83 @@ function moveCalendarToday() {
api.today() api.today()
} }
function openQuickConfig() { function formatProfileDate(value) {
quickEntryConfig.name = resolvedQuickEntryConfig.value.name if (!value) return "Nicht hinterlegt"
quickEntryConfig.color = resolvedQuickEntryConfig.value.color return $dayjs(value).isValid() ? $dayjs(value).format("DD.MM.YYYY") : "Nicht hinterlegt"
} }
async function saveQuickConfig() { function formatProfileNumber(value) {
if (savingQuickConfig.value) return if (value === null || value === undefined || value === "") return "Nicht hinterlegt"
const name = quickEntryConfig.name?.trim() const numericValue = Number(value)
if (!name) { return Number.isFinite(numericValue)
toast.add({ title: "Name fehlt", description: "Bitte einen Namen für Quick-Einträge angeben.", color: "orange" }) ? new Intl.NumberFormat("de-DE", { minimumFractionDigits: 0, maximumFractionDigits: 2 }).format(numericValue)
return : String(value)
}
async function openProfileDetails(profileId) {
const profile = profiles.value.find((item) => item.id === profileId)
if (!profile) return
selectedProfile.value = profile
selectedProfileVacationSummary.value = null
isProfileDetailsModalOpen.value = true
loadingProfileVacation.value = true
const requestId = profileVacationRequestId.value + 1
profileVacationRequestId.value = requestId
try {
const from = `${currentVacationYear.value}-01-01`
const to = `${currentVacationYear.value}-12-31`
const response = await $api(`/api/staff/time/evaluation?${new URLSearchParams({
from,
to,
targetUserId: profile.user_id
}).toString()}`)
if (profileVacationRequestId.value !== requestId) return
selectedProfileVacationSummary.value = response?.summary || null
} catch (error) {
console.error("openProfileDetails failed", error)
if (profileVacationRequestId.value !== requestId) return
selectedProfileVacationSummary.value = null
toast.add({
title: "Resturlaub konnte nicht geladen werden",
description: "Die Mitarbeiterdetails sind sichtbar, aber die Urlaubsauswertung konnte nicht abgerufen werden.",
color: "orange"
})
} finally {
if (profileVacationRequestId.value === requestId) {
loadingProfileVacation.value = false
} }
}
}
function openQuickConfig() {
newQuickPresetName.value = quickEntryConfig.name
showQuickConfigEditor.value = true
showQuickPresetManagement.value = false
isQuickConfigModalOpen.value = true
}
function applyQuickPreset(preset) {
if (!preset) return
quickEntryConfig.name = preset.name
quickEntryConfig.color = preset.color
newQuickPresetName.value = preset.name
}
watch(resolvedQuickEntryConfig, (config) => {
if (!quickEntryConfig.name?.trim()) quickEntryConfig.name = config.name
if (!quickEntryConfig.color) quickEntryConfig.color = config.color
}, { immediate: true })
async function persistQuickPresets(nextPresets) {
if (savingQuickConfig.value) return
savingQuickConfig.value = true savingQuickConfig.value = true
@@ -463,10 +659,7 @@ async function saveQuickConfig() {
const currentTenantData = auth.activeTenantData || {} const currentTenantData = auth.activeTenantData || {}
const nextCalendarConfig = { const nextCalendarConfig = {
...(currentTenantData.calendarConfig || {}), ...(currentTenantData.calendarConfig || {}),
quickEntry: { quickEntryPresets: nextPresets
name,
color: quickEntryConfig.color || "#2563eb"
}
} }
const updatedTenant = await $api(`/api/tenant/other/${auth.activeTenant}`, { const updatedTenant = await $api(`/api/tenant/other/${auth.activeTenant}`, {
@@ -480,25 +673,49 @@ async function saveQuickConfig() {
auth.activeTenantData = updatedTenant auth.activeTenantData = updatedTenant
profileStore.ownTenant = updatedTenant profileStore.ownTenant = updatedTenant
toast.add({ title: "Quick-Einträge gespeichert", color: "green" })
await loadPlanningBoard()
} catch (error) { } catch (error) {
console.error("saveQuickConfig failed", error) console.error("persistQuickPresets failed", error)
toast.add({ toast.add({
title: "Quick-Konfiguration konnte nicht gespeichert werden", title: "Vorlagen konnten nicht gespeichert werden",
description: error?.message || "Bitte erneut versuchen.", description: error?.message || "Bitte erneut versuchen.",
color: "red" color: "red"
}) })
throw error
} finally { } finally {
savingQuickConfig.value = false savingQuickConfig.value = false
} }
} }
async function saveQuickPreset() {
const presetName = newQuickPresetName.value?.trim() || quickEntryConfig.name?.trim()
if (!presetName) {
toast.add({ title: "Name fehlt", description: "Bitte einen Namen für die Vorlage angeben.", color: "orange" })
return
}
const nextPresets = [
...quickEntryPresets.value.filter((preset) => preset.name !== presetName),
{
name: presetName,
color: quickEntryConfig.color || "#2563eb"
}
]
await persistQuickPresets(nextPresets)
toast.add({ title: "Vorlage gespeichert", color: "green" })
}
async function deleteQuickPreset(presetName) {
const nextPresets = quickEntryPresets.value.filter((preset) => preset.name !== presetName)
await persistQuickPresets(nextPresets)
toast.add({ title: "Vorlage gelöscht", color: "green" })
}
async function createQuickEvent(info) { async function createQuickEvent(info) {
const resourceIds = normalizeSelectedResourceIds(info.resource?.id) const resourceIds = normalizeSelectedResourceIds(info.resource?.id)
const payload = { const payload = {
name: resolvedQuickEntryConfig.value.name, name: activeQuickEntryConfig.value.name,
quick: true, quick: true,
startDate: info.startStr, startDate: info.startStr,
endDate: info.endStr, endDate: info.endStr,
@@ -720,7 +937,6 @@ onMounted(() => {
</template> </template>
<template #right> <template #right>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<UPopover>
<UButton <UButton
color="neutral" color="neutral"
variant="outline" variant="outline"
@@ -729,69 +945,6 @@ onMounted(() => {
> >
Quick-Einträge Quick-Einträge
</UButton> </UButton>
<template #content>
<div class="w-[320px] space-y-4 p-4">
<div>
<h3 class="text-sm font-semibold text-highlighted">Quick-Einträge</h3>
<p class="text-xs text-muted">
Name und Farbe für neu gezogene Quick-Einträge in der Plantafel.
</p>
</div>
<UFormField label="Name">
<UInput v-model="quickEntryConfig.name" class="w-full" />
</UFormField>
<UFormField label="Farbe">
<div class="space-y-3">
<div class="flex flex-wrap gap-2">
<button
v-for="option in quickEntryColorOptions"
:key="option.value"
type="button"
class="flex items-center gap-2 rounded border px-2 py-1 text-xs transition"
:class="quickEntryConfig.color === option.value ? 'border-primary ring-1 ring-primary' : 'border-default'"
@click="quickEntryConfig.color = option.value"
>
<span
class="h-4 w-4 rounded-full border border-white/40"
:style="{ backgroundColor: option.value }"
/>
{{ option.label }}
</button>
</div>
<div class="flex items-center gap-3">
<input
v-model="quickEntryConfig.color"
type="color"
class="h-10 w-14 cursor-pointer rounded border border-default bg-transparent p-1"
>
<UInput v-model="quickEntryConfig.color" class="flex-1" />
</div>
</div>
</UFormField>
<div class="rounded border border-default p-3">
<p class="text-xs text-muted">Vorschau</p>
<div class="mt-2 inline-flex rounded px-3 py-1 text-sm font-medium text-white" :style="{ backgroundColor: quickEntryConfig.color }">
{{ quickEntryConfig.name || "Quick-Eintrag" }}
</div>
</div>
<div class="flex justify-end">
<UButton
color="primary"
:loading="savingQuickConfig"
@click="saveQuickConfig"
>
Speichern
</UButton>
</div>
</div>
</template>
</UPopover>
<UButton <UButton
color="amber" color="amber"
variant="soft" variant="soft"
@@ -832,6 +985,174 @@ onMounted(() => {
/> />
</UDashboardPanelContent> </UDashboardPanelContent>
<UModal v-model:open="isQuickConfigModalOpen">
<template #content>
<UCard class="mx-auto w-full max-w-2xl" :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header>
<div class="flex items-center justify-between gap-4">
<div>
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
Quick-Einträge
</h3>
<p class="text-sm text-muted">
Name und Farbe für neu gezogene Quick-Einträge in der Plantafel.
</p>
</div>
<UButton
color="gray"
variant="ghost"
icon="i-heroicons-x-mark-20-solid"
class="-my-1"
@click="isQuickConfigModalOpen = false"
/>
</div>
</template>
<div class="space-y-4 p-1">
<div v-if="quickEntryPresets.length" class="space-y-2">
<div class="text-xs font-medium text-muted">Vorlagen</div>
<div class="flex flex-wrap gap-2">
<button
v-for="preset in quickEntryPresets"
:key="preset.name"
type="button"
class="flex items-center gap-2 rounded border border-default px-2 py-1 text-xs transition hover:border-primary"
@click="applyQuickPreset(preset)"
>
<span
class="h-4 w-4 rounded-full border border-white/40"
:style="{ backgroundColor: preset.color }"
/>
{{ preset.name }}
</button>
</div>
</div>
<div class="rounded border border-default p-3">
<button
type="button"
class="flex w-full items-center justify-between text-left"
@click="showQuickConfigEditor = !showQuickConfigEditor"
>
<div>
<p class="text-sm font-medium text-highlighted">Konfiguration</p>
<p class="text-xs text-muted">Aktuellen Titel und Farbe für Quick-Einträge anpassen.</p>
</div>
<UIcon
:name="showQuickConfigEditor ? 'i-heroicons-chevron-up' : 'i-heroicons-chevron-down'"
class="text-muted"
/>
</button>
<div v-if="showQuickConfigEditor" class="mt-4 space-y-3 border-t border-default pt-4">
<UFormField label="Titel">
<UInput v-model="quickEntryConfig.name" class="w-full" />
</UFormField>
<UFormField label="Vorlagenname">
<UInput v-model="newQuickPresetName" class="w-full" />
</UFormField>
<UFormField label="Farbe">
<div class="space-y-3">
<div class="flex flex-wrap gap-2">
<button
v-for="option in quickEntryColorOptions"
:key="option.value"
type="button"
class="flex items-center gap-2 rounded border px-2 py-1 text-xs transition"
:class="quickEntryConfig.color === option.value ? 'border-primary ring-1 ring-primary' : 'border-default'"
@click="quickEntryConfig.color = option.value"
>
<span
class="h-4 w-4 rounded-full border border-white/40"
:style="{ backgroundColor: option.value }"
/>
{{ option.label }}
</button>
</div>
<div class="flex items-center gap-3">
<input
v-model="quickEntryConfig.color"
type="color"
class="h-10 w-14 cursor-pointer rounded border border-default bg-transparent p-1"
>
<UInput v-model="quickEntryConfig.color" class="flex-1" />
</div>
</div>
</UFormField>
<div class="rounded border border-default p-3">
<div class="flex items-start justify-between gap-3">
<div>
<p class="text-xs text-muted">Vorschau</p>
<div class="mt-2 inline-flex rounded px-3 py-1 text-sm font-medium text-white" :style="{ backgroundColor: quickEntryConfig.color }">
{{ quickEntryConfig.name || "Quick-Eintrag" }}
</div>
</div>
<UButton
color="primary"
:loading="savingQuickConfig"
@click="saveQuickPreset"
>
Vorlage speichern
</UButton>
</div>
</div>
</div>
</div>
<div class="rounded border border-default p-3">
<button
type="button"
class="flex w-full items-center justify-between text-left"
@click="showQuickPresetManagement = !showQuickPresetManagement"
>
<div>
<p class="text-sm font-medium text-highlighted">Vorlagen verwalten</p>
<p class="text-xs text-muted">Tenantweite Vorlagen löschen oder aufräumen.</p>
</div>
<UIcon
:name="showQuickPresetManagement ? 'i-heroicons-chevron-up' : 'i-heroicons-chevron-down'"
class="text-muted"
/>
</button>
<div v-if="showQuickPresetManagement" class="mt-4 space-y-4 border-t border-default pt-4">
<div v-if="quickEntryPresets.length" class="space-y-2">
<p class="text-xs font-medium text-muted">Gespeicherte Vorlagen</p>
<div
v-for="preset in quickEntryPresets"
:key="`${preset.name}-${preset.color}`"
class="flex items-center justify-between gap-3 rounded border border-default px-3 py-2"
>
<button
type="button"
class="flex min-w-0 items-center gap-2 text-left"
@click="applyQuickPreset(preset)"
>
<span
class="h-4 w-4 shrink-0 rounded-full border border-white/40"
:style="{ backgroundColor: preset.color }"
/>
<span class="truncate text-sm">{{ preset.name }}</span>
</button>
<UButton
color="error"
variant="ghost"
icon="i-heroicons-trash"
@click="deleteQuickPreset(preset.name)"
/>
</div>
</div>
</div>
</div>
</div>
</UCard>
</template>
</UModal>
<UModal v-model:open="isAbsenceModalOpen"> <UModal v-model:open="isAbsenceModalOpen">
<template #content> <template #content>
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }"> <UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
@@ -955,5 +1276,146 @@ onMounted(() => {
</UCard> </UCard>
</template> </template>
</UModal> </UModal>
<UModal v-model:open="isProfileDetailsModalOpen">
<template #content>
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header>
<div class="flex items-center justify-between gap-3">
<div>
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
{{ getProfileLabel(selectedProfile) }}
</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">
Mitarbeiterdetails
</p>
</div>
<UButton
color="gray"
variant="ghost"
icon="i-heroicons-x-mark-20-solid"
class="-my-1"
@click="isProfileDetailsModalOpen = false"
/>
</div>
</template>
<div v-if="selectedProfile" class="space-y-6">
<div class="grid gap-4 md:grid-cols-2">
<div class="rounded-lg border border-default p-4">
<p class="text-xs font-medium uppercase tracking-wide text-muted">Kontakt</p>
<dl class="mt-3 space-y-3 text-sm">
<div class="flex items-start justify-between gap-4">
<dt class="text-muted">E-Mail</dt>
<dd class="text-right font-medium text-highlighted">{{ selectedProfile.email || "Nicht hinterlegt" }}</dd>
</div>
<div class="flex items-start justify-between gap-4">
<dt class="text-muted">Mobil</dt>
<dd class="text-right font-medium text-highlighted">{{ selectedProfile.mobile_tel || "Nicht hinterlegt" }}</dd>
</div>
<div class="flex items-start justify-between gap-4">
<dt class="text-muted">Festnetz</dt>
<dd class="text-right font-medium text-highlighted">{{ selectedProfile.fixed_tel || "Nicht hinterlegt" }}</dd>
</div>
<div class="flex items-start justify-between gap-4">
<dt class="text-muted">Geburtstag</dt>
<dd class="text-right font-medium text-highlighted">{{ formatProfileDate(selectedProfile.birthday) }}</dd>
</div>
</dl>
</div>
<div class="rounded-lg border border-default p-4">
<p class="text-xs font-medium uppercase tracking-wide text-muted">Beschäftigung</p>
<dl class="mt-3 space-y-3 text-sm">
<div class="flex items-start justify-between gap-4">
<dt class="text-muted">MA-Nummer</dt>
<dd class="text-right font-medium text-highlighted">{{ selectedProfile.employee_number || "Nicht hinterlegt" }}</dd>
</div>
<div class="flex items-start justify-between gap-4">
<dt class="text-muted">Position</dt>
<dd class="text-right font-medium text-highlighted">{{ selectedProfile.position || "Nicht hinterlegt" }}</dd>
</div>
<div class="flex items-start justify-between gap-4">
<dt class="text-muted">Vertragsart</dt>
<dd class="text-right font-medium text-highlighted">{{ selectedProfile.contract_type || "Nicht hinterlegt" }}</dd>
</div>
<div class="flex items-start justify-between gap-4">
<dt class="text-muted">Eintrittsdatum</dt>
<dd class="text-right font-medium text-highlighted">{{ formatProfileDate(selectedProfile.entry_date) }}</dd>
</div>
<div class="flex items-start justify-between gap-4">
<dt class="text-muted">Wochenstunden</dt>
<dd class="text-right font-medium text-highlighted">{{ formatProfileNumber(selectedProfile.weekly_working_hours) }}</dd>
</div>
</dl>
</div>
</div>
<div class="grid gap-4 md:grid-cols-2">
<div class="rounded-lg border border-default p-4">
<p class="text-xs font-medium uppercase tracking-wide text-muted">Zuordnung</p>
<dl class="mt-3 space-y-3 text-sm">
<div class="flex items-start justify-between gap-4">
<dt class="text-muted">Niederlassung</dt>
<dd class="text-right font-medium text-highlighted">{{ selectedProfileBranchesLabel }}</dd>
</div>
<div class="flex items-start justify-between gap-4">
<dt class="text-muted">Teams</dt>
<dd class="text-right font-medium text-highlighted">{{ selectedProfileTeamsLabel }}</dd>
</div>
<div class="flex items-start justify-between gap-4">
<dt class="text-muted">Status</dt>
<dd class="text-right font-medium text-highlighted">{{ selectedProfile.active ? "Aktiv" : "Inaktiv" }}</dd>
</div>
</dl>
</div>
<div class="rounded-lg border border-default p-4">
<div class="flex items-start justify-between gap-4">
<div>
<p class="text-xs font-medium uppercase tracking-wide text-muted">Urlaub {{ currentVacationYear }}</p>
<p class="mt-1 text-sm text-muted">Anzeige auf Basis der aktuellen Zeitauswertung.</p>
</div>
<UBadge color="amber" variant="subtle">
Resturlaub
</UBadge>
</div>
<div v-if="loadingProfileVacation" class="mt-4 space-y-2">
<USkeleton class="h-6 w-full" />
<USkeleton class="h-6 w-4/5" />
<USkeleton class="h-6 w-3/5" />
</div>
<dl v-else class="mt-4 space-y-3 text-sm">
<div class="flex items-start justify-between gap-4">
<dt class="text-muted">Urlaubsanspruch</dt>
<dd class="text-right font-medium text-highlighted">{{ formatProfileNumber(selectedProfileAnnualLeaveDays) }}</dd>
</div>
<div class="flex items-start justify-between gap-4">
<dt class="text-muted">Bereits verplant/genehmigt</dt>
<dd class="text-right font-medium text-highlighted">{{ formatProfileNumber(selectedProfileVacationDaysTaken) }}</dd>
</div>
<div class="flex items-start justify-between gap-4">
<dt class="text-muted">Resturlaub</dt>
<dd class="text-right text-base font-semibold text-highlighted">
{{ selectedProfileRemainingVacationDays === null ? "Nicht hinterlegt" : formatProfileNumber(selectedProfileRemainingVacationDays) }}
</dd>
</div>
</dl>
</div>
</div>
</div>
<template #footer>
<div class="flex justify-end">
<UButton color="gray" variant="soft" @click="isProfileDetailsModalOpen = false">
Schließen
</UButton>
</div>
</template>
</UCard>
</template>
</UModal>
</div> </div>
</template> </template>