Plantafel um Mitarbeiterdetails mit Resturlaub erweitern
This commit is contained in:
@@ -30,10 +30,14 @@ const events = ref([])
|
||||
const profiles = ref([])
|
||||
const inventoryitems = ref([])
|
||||
const savingQuickConfig = ref(false)
|
||||
const isQuickConfigModalOpen = ref(false)
|
||||
const showQuickConfigEditor = ref(true)
|
||||
const showQuickPresetManagement = ref(false)
|
||||
const quickEntryConfig = reactive({
|
||||
name: "Quick-Eintrag",
|
||||
color: "#2563eb"
|
||||
})
|
||||
const newQuickPresetName = ref("")
|
||||
const quickEntryColorOptions = [
|
||||
{ label: "Blau", value: "#2563eb" },
|
||||
{ label: "Grün", value: "#16a34a" },
|
||||
@@ -46,6 +50,11 @@ const quickEntryColorOptions = [
|
||||
]
|
||||
|
||||
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({
|
||||
mode: "create",
|
||||
entry: null,
|
||||
@@ -142,8 +151,14 @@ const absenceModalTitle = computed(() =>
|
||||
: (absenceForm.type === "sick" ? "Krank eintragen" : "Urlaub eintragen")
|
||||
)
|
||||
|
||||
const tenantCalendarConfig = computed(() =>
|
||||
auth.activeTenantData?.calendarConfig
|
||||
|| profileStore.ownTenant?.calendarConfig
|
||||
|| {}
|
||||
)
|
||||
|
||||
const resolvedQuickEntryConfig = computed(() => {
|
||||
const config = profileStore.ownTenant?.calendarConfig?.quickEntry || {}
|
||||
const config = tenantCalendarConfig.value?.quickEntry || {}
|
||||
|
||||
return {
|
||||
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(() => {
|
||||
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)))
|
||||
@@ -188,6 +267,28 @@ const calendarOptions = computed(() => ({
|
||||
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: {
|
||||
resourceTimelineDay: {
|
||||
type: "resourceTimeline",
|
||||
@@ -245,17 +346,17 @@ const calendarOptions = computed(() => ({
|
||||
}))
|
||||
|
||||
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) {
|
||||
if (event.name) return event.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) {
|
||||
if (event?.quick) return resolvedQuickEntryConfig.value.color
|
||||
if (event?.quick) return activeQuickEntryConfig.value.color
|
||||
return resolveEventColor(event.eventtype)
|
||||
}
|
||||
|
||||
@@ -274,7 +375,11 @@ function getAbsenceColor(type) {
|
||||
|
||||
function getTeamLabel(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) {
|
||||
@@ -305,8 +410,10 @@ function normalizeSelectedResourceIds(resourceId) {
|
||||
}
|
||||
|
||||
function buildResources({ profiles, inventoryitems }) {
|
||||
const branchResources = []
|
||||
const teamResources = []
|
||||
const profileResources = []
|
||||
const topLevelTeamGroupId = "TEAM-UNBOUND"
|
||||
|
||||
profiles
|
||||
.filter((profile) => !profile.archived)
|
||||
@@ -317,17 +424,40 @@ function buildResources({ profiles, inventoryitems }) {
|
||||
profileResources.push({
|
||||
id: `P-${profile.id}`,
|
||||
type: "Profile",
|
||||
title: getProfileLabel(profile)
|
||||
title: getProfileLabel(profile),
|
||||
resourceKind: "profile",
|
||||
profileId: profile.id
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
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}`)) {
|
||||
teamResources.push({
|
||||
id: `T-${team.id}`,
|
||||
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}`,
|
||||
parentId: `T-${team.id}`,
|
||||
type: "Profile",
|
||||
title: getProfileLabel(profile)
|
||||
title: getProfileLabel(profile),
|
||||
resourceKind: "profile",
|
||||
profileId: profile.id
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -348,7 +480,7 @@ function buildResources({ profiles, inventoryitems }) {
|
||||
title: item.name
|
||||
}))
|
||||
|
||||
return [...teamResources, ...profileResources, ...inventoryResources]
|
||||
return [...branchResources, ...teamResources, ...profileResources, ...inventoryResources]
|
||||
}
|
||||
|
||||
function buildEvents({ rawEvents, projectsById }) {
|
||||
@@ -443,19 +575,83 @@ function moveCalendarToday() {
|
||||
api.today()
|
||||
}
|
||||
|
||||
function openQuickConfig() {
|
||||
quickEntryConfig.name = resolvedQuickEntryConfig.value.name
|
||||
quickEntryConfig.color = resolvedQuickEntryConfig.value.color
|
||||
function formatProfileDate(value) {
|
||||
if (!value) return "Nicht hinterlegt"
|
||||
return $dayjs(value).isValid() ? $dayjs(value).format("DD.MM.YYYY") : "Nicht hinterlegt"
|
||||
}
|
||||
|
||||
async function saveQuickConfig() {
|
||||
if (savingQuickConfig.value) return
|
||||
function formatProfileNumber(value) {
|
||||
if (value === null || value === undefined || value === "") return "Nicht hinterlegt"
|
||||
|
||||
const name = quickEntryConfig.name?.trim()
|
||||
if (!name) {
|
||||
toast.add({ title: "Name fehlt", description: "Bitte einen Namen für Quick-Einträge angeben.", color: "orange" })
|
||||
return
|
||||
const numericValue = Number(value)
|
||||
return Number.isFinite(numericValue)
|
||||
? new Intl.NumberFormat("de-DE", { minimumFractionDigits: 0, maximumFractionDigits: 2 }).format(numericValue)
|
||||
: 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
|
||||
|
||||
@@ -463,10 +659,7 @@ async function saveQuickConfig() {
|
||||
const currentTenantData = auth.activeTenantData || {}
|
||||
const nextCalendarConfig = {
|
||||
...(currentTenantData.calendarConfig || {}),
|
||||
quickEntry: {
|
||||
name,
|
||||
color: quickEntryConfig.color || "#2563eb"
|
||||
}
|
||||
quickEntryPresets: nextPresets
|
||||
}
|
||||
|
||||
const updatedTenant = await $api(`/api/tenant/other/${auth.activeTenant}`, {
|
||||
@@ -480,25 +673,49 @@ async function saveQuickConfig() {
|
||||
|
||||
auth.activeTenantData = updatedTenant
|
||||
profileStore.ownTenant = updatedTenant
|
||||
toast.add({ title: "Quick-Einträge gespeichert", color: "green" })
|
||||
await loadPlanningBoard()
|
||||
} catch (error) {
|
||||
console.error("saveQuickConfig failed", error)
|
||||
console.error("persistQuickPresets failed", error)
|
||||
toast.add({
|
||||
title: "Quick-Konfiguration konnte nicht gespeichert werden",
|
||||
title: "Vorlagen konnten nicht gespeichert werden",
|
||||
description: error?.message || "Bitte erneut versuchen.",
|
||||
color: "red"
|
||||
})
|
||||
throw error
|
||||
} finally {
|
||||
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) {
|
||||
const resourceIds = normalizeSelectedResourceIds(info.resource?.id)
|
||||
|
||||
const payload = {
|
||||
name: resolvedQuickEntryConfig.value.name,
|
||||
name: activeQuickEntryConfig.value.name,
|
||||
quick: true,
|
||||
startDate: info.startStr,
|
||||
endDate: info.endStr,
|
||||
@@ -720,7 +937,6 @@ onMounted(() => {
|
||||
</template>
|
||||
<template #right>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<UPopover>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
@@ -729,69 +945,6 @@ onMounted(() => {
|
||||
>
|
||||
Quick-Einträge
|
||||
</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
|
||||
color="amber"
|
||||
variant="soft"
|
||||
@@ -832,6 +985,174 @@ onMounted(() => {
|
||||
/>
|
||||
</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">
|
||||
<template #content>
|
||||
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
|
||||
@@ -955,5 +1276,146 @@ onMounted(() => {
|
||||
</UCard>
|
||||
</template>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user