Verfügbarkeitshinweise für Mitarbeiter und Plantafel-Details ergänzen

This commit is contained in:
2026-04-29 16:33:39 +02:00
parent 2d26cedaa3
commit 0f14f7ac3d
4 changed files with 332 additions and 247 deletions

View File

@@ -0,0 +1,2 @@
ALTER TABLE "auth_profiles"
ADD COLUMN "availability_note" text;

View File

@@ -74,6 +74,7 @@ export const authProfiles = pgTable("auth_profiles", {
contract_type: text("contract_type"), contract_type: text("contract_type"),
position: text("position"), position: text("position"),
qualification: text("qualification"), qualification: text("qualification"),
availability_note: text("availability_note"),
address_street: text("address_street"), address_street: text("address_street"),
address_zip: text("address_zip"), address_zip: text("address_zip"),

View File

@@ -4,6 +4,7 @@ import FullCalendar from "@fullcalendar/vue3"
import interactionPlugin from "@fullcalendar/interaction" import interactionPlugin from "@fullcalendar/interaction"
import resourceTimelinePlugin from "@fullcalendar/resource-timeline" import resourceTimelinePlugin from "@fullcalendar/resource-timeline"
import { parseDate } from "@internationalized/date" import { parseDate } from "@internationalized/date"
import { useDraggable } from "@vueuse/core"
const router = useRouter() const router = useRouter()
const auth = useAuthStore() const auth = useAuthStore()
@@ -31,6 +32,8 @@ const profiles = ref([])
const inventoryitems = ref([]) const inventoryitems = ref([])
const savingQuickConfig = ref(false) const savingQuickConfig = ref(false)
const isQuickConfigModalOpen = ref(false) const isQuickConfigModalOpen = ref(false)
const quickConfigWindowEl = ref(null)
const profileDetailsWindowEl = ref(null)
const showQuickConfigEditor = ref(true) const showQuickConfigEditor = ref(true)
const showQuickPresetManagement = ref(false) const showQuickPresetManagement = ref(false)
const quickEntryConfig = reactive({ const quickEntryConfig = reactive({
@@ -49,6 +52,14 @@ const quickEntryColorOptions = [
{ label: "Schwarz", value: "#111827" } { label: "Schwarz", value: "#111827" }
] ]
const { style: quickConfigWindowStyle } = useDraggable(quickConfigWindowEl, {
initialValue: { x: 120, y: 100 }
})
const { style: profileDetailsWindowStyle } = useDraggable(profileDetailsWindowEl, {
initialValue: { x: 220, y: 120 }
})
const isAbsenceModalOpen = ref(false) const isAbsenceModalOpen = ref(false)
const isProfileDetailsModalOpen = ref(false) const isProfileDetailsModalOpen = ref(false)
const loadingProfileVacation = ref(false) const loadingProfileVacation = ref(false)
@@ -157,6 +168,20 @@ const tenantCalendarConfig = computed(() =>
|| {} || {}
) )
const resolvedPlanningBoardConfig = computed(() => {
const config = tenantCalendarConfig.value?.planningBoard || {}
const normalizedStartTime = /^\d{2}:\d{2}$/.test(String(config.startTime || "")) ? String(config.startTime) : "06:00"
const normalizedEndTime = /^\d{2}:\d{2}$/.test(String(config.endTime || "")) ? String(config.endTime) : "21:00"
const normalizedSlotMinutes = Number(config.slotMinutes)
const allowedSlotMinutes = [15, 30, 60, 120, 180]
return {
startTime: normalizedStartTime,
endTime: normalizedEndTime,
slotMinutes: allowedSlotMinutes.includes(normalizedSlotMinutes) ? normalizedSlotMinutes : 180
}
})
const resolvedQuickEntryConfig = computed(() => { const resolvedQuickEntryConfig = computed(() => {
const config = tenantCalendarConfig.value?.quickEntry || {} const config = tenantCalendarConfig.value?.quickEntry || {}
@@ -215,6 +240,9 @@ const selectedProfileRemainingVacationDays = computed(() => {
return Number((selectedProfileAnnualLeaveDays.value - selectedProfileVacationDaysTaken.value).toFixed(2)) return Number((selectedProfileAnnualLeaveDays.value - selectedProfileVacationDaysTaken.value).toFixed(2))
}) })
const selectedProfileAvailabilityNote = computed(() =>
selectedProfile.value?.availability_note?.trim() || "Keine Verfügbarkeitshinweise hinterlegt"
)
const visibleResources = computed(() => { const visibleResources = computed(() => {
if (selectedType.value === "all") return resources.value if (selectedType.value === "all") return resources.value
@@ -294,16 +322,20 @@ const calendarOptions = computed(() => ({
type: "resourceTimeline", type: "resourceTimeline",
duration: { days: 1 }, duration: { days: 1 },
buttonText: "Tag", buttonText: "Tag",
slotDuration: { hours: 1 } slotDuration: { minutes: resolvedPlanningBoardConfig.value.slotMinutes },
snapDuration: { minutes: resolvedPlanningBoardConfig.value.slotMinutes },
slotMinTime: `${resolvedPlanningBoardConfig.value.startTime}:00`,
slotMaxTime: `${resolvedPlanningBoardConfig.value.endTime}:00`
}, },
resourceTimelineWeek: { resourceTimelineWeek: {
type: "resourceTimeline", type: "resourceTimeline",
duration: { days: 7 }, duration: { days: 7 },
buttonText: "Woche", buttonText: "Woche",
slotDuration: { hours: 3 }, slotDuration: { minutes: resolvedPlanningBoardConfig.value.slotMinutes },
snapDuration: { minutes: resolvedPlanningBoardConfig.value.slotMinutes },
weekends: false, weekends: false,
slotMinTime: "06:00:00", slotMinTime: `${resolvedPlanningBoardConfig.value.startTime}:00`,
slotMaxTime: "21:00:00" slotMaxTime: `${resolvedPlanningBoardConfig.value.endTime}:00`
}, },
resourceTimelineMonth: { resourceTimelineMonth: {
type: "resourceTimeline", type: "resourceTimeline",
@@ -991,33 +1023,44 @@ onMounted(() => {
/> />
</UDashboardPanelContent> </UDashboardPanelContent>
<UModal v-model:open="isQuickConfigModalOpen"> <div
<template #content> v-if="isQuickConfigModalOpen"
<UCard class="mx-auto w-full max-w-2xl" :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }"> ref="quickConfigWindowEl"
<template #header> :style="quickConfigWindowStyle"
<div class="flex items-center justify-between gap-4"> class="fixed z-[999] flex h-[720px] w-[980px] flex-col overflow-hidden rounded-xl border border-gray-200 bg-white shadow-2xl resize dark:border-gray-800 dark:bg-gray-900"
>
<div class="flex cursor-move items-center justify-between border-b border-gray-200 bg-gray-50 p-3 select-none dark:border-gray-800 dark:bg-gray-800/50">
<div class="flex items-center gap-3">
<UIcon name="i-heroicons-cog-6-tooth" class="text-gray-500" />
<div> <div>
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white"> <h3 class="text-sm font-semibold text-gray-900 dark:text-white">
Quick-Einträge Quick-Einträge
</h3> </h3>
<p class="text-sm text-muted"> <p class="text-xs text-muted">
Name und Farbe für neu gezogene Quick-Einträge in der Plantafel. Name und Farbe für neu gezogene Quick-Einträge in der Plantafel.
</p> </p>
</div> </div>
</div>
<UButton <UButton
color="gray" color="gray"
variant="ghost" variant="ghost"
icon="i-heroicons-x-mark-20-solid" icon="i-heroicons-x-mark-20-solid"
class="-my-1" size="sm"
@click="isQuickConfigModalOpen = false" @click="isQuickConfigModalOpen = false"
/> />
</div> </div>
</template>
<div class="space-y-4 p-1"> <div class="flex-1 overflow-auto p-4">
<div v-if="quickEntryPresets.length" class="space-y-2"> <div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(0,1.2fr)]">
<div class="text-xs font-medium text-muted">Vorlagen</div> <div class="space-y-4">
<div class="flex flex-wrap gap-2"> <div class="rounded border border-default p-3">
<div class="space-y-3">
<div>
<p class="text-sm font-medium text-highlighted">Vorlagen</p>
<p class="text-xs text-muted">Gespeicherte Kombinationen direkt für neue Quick-Einträge anwenden.</p>
</div>
<div v-if="quickEntryPresets.length" class="flex flex-wrap gap-2">
<button <button
v-for="preset in quickEntryPresets" v-for="preset in quickEntryPresets"
:key="preset.name" :key="preset.name"
@@ -1032,6 +1075,58 @@ onMounted(() => {
{{ preset.name }} {{ preset.name }}
</button> </button>
</div> </div>
<p v-else class="text-sm text-muted">
Es sind noch keine Vorlagen gespeichert.
</p>
</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> </div>
<div class="rounded border border-default p-3"> <div class="rounded border border-default p-3">
@@ -1108,56 +1203,13 @@ onMounted(() => {
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
<div class="rounded border border-default p-3"> <div class="absolute bottom-0 right-0 h-4 w-4 cursor-se-resize opacity-50">
<button <UIcon name="i-heroicons-arrows-pointing-out" class="h-3 w-3 rotate-90 text-gray-400" />
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>
</div>
</div>
</UCard>
</template>
</UModal>
<UModal v-model:open="isAbsenceModalOpen"> <UModal v-model:open="isAbsenceModalOpen">
<template #content> <template #content>
@@ -1283,30 +1335,35 @@ onMounted(() => {
</template> </template>
</UModal> </UModal>
<UModal v-model:open="isProfileDetailsModalOpen"> <div
<template #content> v-if="isProfileDetailsModalOpen"
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }"> ref="profileDetailsWindowEl"
<template #header> :style="profileDetailsWindowStyle"
<div class="flex items-center justify-between gap-3"> class="fixed z-[999] flex h-[760px] w-[980px] flex-col overflow-hidden rounded-xl border border-gray-200 bg-white shadow-2xl resize dark:border-gray-800 dark:bg-gray-900"
>
<div class="flex cursor-move items-center justify-between border-b border-gray-200 bg-gray-50 p-3 select-none dark:border-gray-800 dark:bg-gray-800/50">
<div class="flex items-center gap-3">
<UIcon name="i-heroicons-user-circle" class="text-gray-500" />
<div> <div>
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white"> <h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{{ getProfileLabel(selectedProfile) }} {{ getProfileLabel(selectedProfile) }}
</h3> </h3>
<p class="text-sm text-gray-500 dark:text-gray-400"> <p class="text-xs text-muted">
Mitarbeiterdetails Mitarbeiterdetails
</p> </p>
</div> </div>
</div>
<UButton <UButton
color="gray" color="gray"
variant="ghost" variant="ghost"
icon="i-heroicons-x-mark-20-solid" icon="i-heroicons-x-mark-20-solid"
class="-my-1" size="sm"
@click="isProfileDetailsModalOpen = false" @click="isProfileDetailsModalOpen = false"
/> />
</div> </div>
</template>
<div v-if="selectedProfile" class="space-y-6"> <div v-if="selectedProfile" class="flex-1 overflow-auto p-4">
<div class="space-y-6">
<div class="grid gap-4 md:grid-cols-2"> <div class="grid gap-4 md:grid-cols-2">
<div class="rounded-lg border border-default p-4"> <div class="rounded-lg border border-default p-4">
<p class="text-xs font-medium uppercase tracking-wide text-muted">Kontakt</p> <p class="text-xs font-medium uppercase tracking-wide text-muted">Kontakt</p>
@@ -1411,17 +1468,33 @@ onMounted(() => {
</dl> </dl>
</div> </div>
</div> </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">Verfügbarkeitshinweis</p>
<p class="mt-1 text-sm text-muted">Z. B. bevorzugte Einsatzzeiten oder bekannte Einschränkungen.</p>
</div>
<UBadge color="sky" variant="subtle">
Verfügbarkeit
</UBadge>
</div>
<p class="mt-4 whitespace-pre-wrap text-sm font-medium text-highlighted">
{{ selectedProfileAvailabilityNote }}
</p>
</div>
</div>
</div> </div>
<template #footer> <div class="flex justify-end border-t border-gray-200 bg-gray-50 p-3 dark:border-gray-800 dark:bg-gray-800/30">
<div class="flex justify-end">
<UButton color="gray" variant="soft" @click="isProfileDetailsModalOpen = false"> <UButton color="gray" variant="soft" @click="isProfileDetailsModalOpen = false">
Schließen Schließen
</UButton> </UButton>
</div> </div>
</template>
</UCard> <div class="absolute bottom-0 right-0 h-4 w-4 cursor-se-resize opacity-50">
</template> <UIcon name="i-heroicons-arrows-pointing-out" class="h-3 w-3 rotate-90 text-gray-400" />
</UModal> </div>
</div>
</div> </div>
</template> </template>

View File

@@ -548,6 +548,15 @@ onMounted(async () => {
<UFormField label="Token-ID" class="w-full"> <UFormField label="Token-ID" class="w-full">
<UInput v-model="profile.token_id" class="w-full" /> <UInput v-model="profile.token_id" class="w-full" />
</UFormField> </UFormField>
<UFormField label="Verfügbarkeitshinweis" class="w-full md:col-span-2">
<UTextarea
v-model="profile.availability_note"
class="w-full"
:rows="4"
placeholder="z. B. kann nur vormittags eingeplant werden, bevorzugt Außendienst, nicht dienstags verfügbar"
/>
</UFormField>
</UForm> </UForm>
</UCard> </UCard>