305 lines
14 KiB
Vue
305 lines
14 KiB
Vue
<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>
|