Files
NautilusDeskTimeFrontend/NautilusDeskTimeFrontend/app/pages/verfuegbarkeit.vue
Tom Trappmann 68c9ba90c5 init
2026-02-02 22:44:52 +01:00

305 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { addDays, format } from 'date-fns'
const buildMockRules = (employeeId: string): AvailabilityRule[] => [
{ id: `rule_${employeeId}_mon`, weekday: 0, start_time: '08:00', end_time: '16:00', is_available: true },
{ id: `rule_${employeeId}_tue`, weekday: 1, start_time: '08:00', end_time: '16:00', is_available: true },
{ id: `rule_${employeeId}_fri`, weekday: 4, start_time: '10:00', end_time: '18:00', is_available: true }
]
const buildMockOverrides = (): AvailabilityOverride[] => [
{ id: 'override_1', date: format(addDays(today, 2), 'yyyy-MM-dd'), start_time: '12:00', end_time: '16:00', is_available: false },
{ id: 'override_2', date: format(addDays(today, 5), 'yyyy-MM-dd'), start_time: '09:00', end_time: '14:00', is_available: true }
]
type AvailabilityRule = {
id: string
weekday: number
start_time: string
end_time: string
is_available: boolean
}
type AvailabilityOverride = {
id: string
date: string
start_time: string
end_time: string
is_available: boolean
}
type Branch = {
id: string
name: string
}
type Employee = {
id: string
name: string
}
const orgId = 'org_demo_001'
const branches: Branch[] = [
{ id: 'branch_berlin', name: 'Filiale Berlin Mitte' },
{ id: 'branch_hamburg', name: 'Filiale Hamburg Hafen' },
{ id: 'branch_munich', name: 'Filiale München Süd' }
]
const employees: Employee[] = [
{ id: 'emp_anna', name: 'Anna Keller' },
{ id: 'emp_mehmet', name: 'Mehmet Aydin' },
{ id: 'emp_lisa', name: 'Lisa Schmidt' }
]
const role = 'MANAGER'
const isManager = computed(() => role === 'MANAGER' || role === 'OWNER')
const selectedBranchId = ref(branches[0].id)
const selectedEmployeeId = ref(employees[0].id)
const activeTab = ref<'rules' | 'overrides'>('rules')
const rules = ref<AvailabilityRule[]>([])
const overrides = ref<AvailabilityOverride[]>([])
const today = new Date()
const overrideRange = reactive({
from: format(today, 'yyyy-MM-dd'),
to: format(addDays(today, 30), 'yyyy-MM-dd')
})
const weekdays = [
{ value: 0, label: 'Montag', short: 'Mo' },
{ value: 1, label: 'Dienstag', short: 'Di' },
{ value: 2, label: 'Mittwoch', short: 'Mi' },
{ value: 3, label: 'Donnerstag', short: 'Do' },
{ value: 4, label: 'Freitag', short: 'Fr' },
{ value: 5, label: 'Samstag', short: 'Sa' },
{ value: 6, label: 'Sonntag', short: 'So' }
]
const newRuleByWeekday = reactive<Record<number, { start_time: string; end_time: string }>>({
0: { start_time: '08:00', end_time: '16:00' },
1: { start_time: '08:00', end_time: '16:00' },
2: { start_time: '08:00', end_time: '16:00' },
3: { start_time: '08:00', end_time: '16:00' },
4: { start_time: '08:00', end_time: '16:00' },
5: { start_time: '08:00', end_time: '16:00' },
6: { start_time: '08:00', end_time: '16:00' }
})
const newOverride = reactive({
date: format(today, 'yyyy-MM-dd'),
start_time: '08:00',
end_time: '16:00',
is_available: true
})
const loadAvailability = async () => {
if (!selectedEmployeeId.value) {
return
}
rules.value = buildMockRules(selectedEmployeeId.value)
overrides.value = buildMockOverrides()
}
const addRule = async (weekday: number) => {
const entry = newRuleByWeekday[weekday]
rules.value.push({
id: `rule_${Math.random().toString(36).slice(2, 9)}`,
weekday,
start_time: entry.start_time,
end_time: entry.end_time,
is_available: true
})
}
const removeRule = async (ruleId: string) => {
rules.value = rules.value.filter((rule) => rule.id !== ruleId)
}
const addOverride = async () => {
overrides.value.push({
id: `override_${Math.random().toString(36).slice(2, 9)}`,
date: newOverride.date,
start_time: newOverride.start_time,
end_time: newOverride.end_time,
is_available: newOverride.is_available
})
}
const removeOverride = async (overrideId: string) => {
overrides.value = overrides.value.filter((entry) => entry.id !== overrideId)
}
watch([selectedEmployeeId, () => overrideRange.from, () => overrideRange.to], () => {
loadAvailability()
})
onMounted(() => {
loadAvailability()
})
</script>
<template>
<UDashboardPanel id="verfuegbarkeit">
<template #header>
<UDashboardNavbar title="Verfügbarkeit">
<template #leading>
<UDashboardSidebarCollapse />
</template>
</UDashboardNavbar>
<div class="border-b border-default px-4 py-5 sm:px-6">
<div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div>
<div class="text-base text-muted">Standardzeiten und Tagesausnahmen pflegen.</div>
</div>
</div>
<div class="mt-5 rounded-2xl border border-default bg-elevated p-4 shadow-sm">
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<label class="flex flex-col gap-2 text-base font-semibold text-default">
Filiale auswählen
<select v-model="selectedBranchId"
class="h-12 rounded-lg border border-default bg-elevated px-3 text-lg text-default focus:border-primary-400 focus:outline-none focus:ring-4 focus:ring-primary-900/30">
<option v-for="branch in branches" :key="branch.id" :value="branch.id">
{{ branch.name }}
</option>
</select>
</label>
<label class="flex flex-col gap-2 text-base font-semibold text-default">
Mitarbeiter auswählen
<select v-model="selectedEmployeeId" :disabled="!isManager"
class="h-12 rounded-lg border border-default bg-elevated px-3 text-lg text-default focus:border-primary-400 focus:outline-none focus:ring-4 focus:ring-primary-900/30 disabled:opacity-60">
<option v-for="employee in employees" :key="employee.id" :value="employee.id">
{{ employee.name }}
</option>
</select>
</label>
<div class="flex flex-wrap gap-3">
<UButton size="lg" color="neutral" variant="outline" label="Aktualisieren"
@click="loadAvailability" />
</div>
</div>
</div>
</div>
</template>
<template #body>
<div class="max-h-[calc(100vh-260px)] space-y-6 overflow-y-auto px-4 py-6 sm:px-6">
<div class="flex flex-wrap gap-3">
<UButton size="lg" :color="activeTab === 'rules' ? 'primary' : 'neutral'"
:variant="activeTab === 'rules' ? 'solid' : 'outline'" label="Standardzeiten"
@click="activeTab = 'rules'" />
<UButton size="lg" :color="activeTab === 'overrides' ? 'primary' : 'neutral'"
:variant="activeTab === 'overrides' ? 'solid' : 'outline'" label="Tagesausnahmen"
@click="activeTab = 'overrides'" />
</div>
<div v-if="activeTab === 'rules'" class="space-y-6">
<div v-for="day in weekdays" :key="day.value"
class="rounded-2xl border border-default bg-elevated p-4 shadow-sm">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<div class="text-sm font-semibold text-muted">{{ day.short }}</div>
<div class="text-2xl font-semibold text-default">{{ day.label }}</div>
</div>
</div>
<div class="mt-4 space-y-3">
<div v-for="rule in rules.filter((entry) => entry.weekday === day.value)" :key="rule.id"
class="flex flex-col gap-3 rounded-xl border border-default bg-muted/30 p-3 lg:flex-row lg:items-center lg:justify-between">
<div class="text-lg font-semibold text-default">
{{ rule.start_time }} {{ rule.end_time }}
</div>
<UButton size="lg" color="neutral" variant="outline" label="Löschen"
@click="removeRule(rule.id)" />
</div>
<div
class="flex flex-col gap-3 rounded-xl border border-dashed border-default p-3 lg:flex-row lg:items-center lg:justify-between">
<div class="flex flex-wrap gap-3">
<label class="flex flex-col gap-2 text-base font-semibold text-default">
Start
<UInput v-model="newRuleByWeekday[day.value].start_time" type="time"
size="lg" />
</label>
<label class="flex flex-col gap-2 text-base font-semibold text-default">
Ende
<UInput v-model="newRuleByWeekday[day.value].end_time" type="time" size="lg" />
</label>
</div>
<UButton size="lg" color="primary" label="Zeit hinzufügen"
@click="addRule(day.value)" />
</div>
</div>
</div>
</div>
<div v-else class="space-y-6">
<div class="rounded-2xl border border-default bg-elevated p-4 shadow-sm">
<div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div class="flex flex-wrap gap-4">
<label class="flex flex-col gap-2 text-base font-semibold text-default">
Von
<UInput v-model="overrideRange.from" type="date" size="lg" />
</label>
<label class="flex flex-col gap-2 text-base font-semibold text-default">
Bis
<UInput v-model="overrideRange.to" type="date" size="lg" />
</label>
</div>
<UButton size="lg" color="neutral" variant="outline" label="Bereich laden"
@click="loadAvailability" />
</div>
</div>
<div class="rounded-2xl border border-default bg-elevated p-4 shadow-sm">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<div class="text-sm font-semibold text-muted">Neue Ausnahme</div>
<div class="text-2xl font-semibold text-default">Tagesausnahme hinzufügen</div>
</div>
<UButton size="lg" color="primary" label="+ Ausnahme hinzufügen" @click="addOverride" />
</div>
<div class="mt-4 flex flex-wrap gap-4">
<label class="flex flex-col gap-2 text-base font-semibold text-default">
Datum
<UInput v-model="newOverride.date" type="date" size="lg" />
</label>
<label class="flex flex-col gap-2 text-base font-semibold text-default">
Start
<UInput v-model="newOverride.start_time" type="time" size="lg" />
</label>
<label class="flex flex-col gap-2 text-base font-semibold text-default">
Ende
<UInput v-model="newOverride.end_time" type="time" size="lg" />
</label>
<label class="flex flex-col gap-2 text-base font-semibold text-default">
Typ
<select v-model="newOverride.is_available"
class="h-12 rounded-lg border border-default bg-elevated px-3 text-lg text-default focus:border-primary-400 focus:outline-none focus:ring-4 focus:ring-primary-900/30">
<option :value="true">Verfügbar</option>
<option :value="false">Nicht verfügbar</option>
</select>
</label>
</div>
</div>
<div class="space-y-3">
<div v-for="entry in overrides" :key="entry.id"
class="flex flex-col gap-3 rounded-xl border border-default bg-elevated p-4 shadow-sm lg:flex-row lg:items-center lg:justify-between">
<div class="text-lg font-semibold text-default">
{{ entry.date }} · {{ entry.start_time }} {{ entry.end_time }}
</div>
<div class="text-base font-semibold text-muted">
{{ entry.is_available ? 'Verfügbar' : 'Nicht verfügbar' }}
</div>
<UButton size="lg" color="neutral" variant="outline" label="Löschen"
@click="removeOverride(entry.id)" />
</div>
</div>
</div>
</div>
</template>
</UDashboardPanel>
</template>