733 lines
23 KiB
Vue
733 lines
23 KiB
Vue
<script setup lang="ts">
|
||
|
||
const route = useRoute()
|
||
const toast = useToast()
|
||
const auth = useAuthStore()
|
||
const admin = useAdmin()
|
||
const { $api } = useNuxtApp()
|
||
const runtimeConfig = useRuntimeConfig()
|
||
|
||
const id = route.params.id as string
|
||
const profile = ref<any>(null)
|
||
const branches = ref<any[]>([])
|
||
const teams = ref<any[]>([])
|
||
const pending = ref(true)
|
||
const saving = ref(false)
|
||
const creatingLinkedUser = ref(false)
|
||
const createLinkedUserModalOpen = ref(false)
|
||
const createdLinkedUserPassword = ref("")
|
||
const generatingCalendarSubscription = ref(false)
|
||
const createLinkedUserForm = reactive({
|
||
email: "",
|
||
})
|
||
const selectMenuUi = {
|
||
base: 'w-full',
|
||
content: 'min-w-[min(32rem,90vw)] w-max max-w-[90vw]'
|
||
}
|
||
|
||
const canCreateLinkedUser = computed(() => Boolean(auth.user?.is_admin && profile.value && !profile.value.user_id))
|
||
const linkedUserStatusLabel = computed(() => profile.value?.user_id ? "Benutzer verknüpft" : "Kein Benutzer verknüpft")
|
||
const linkedUserStatusColor = computed(() => profile.value?.user_id ? "green" : "orange")
|
||
const calendarSubscriptionHttpUrl = computed(() => {
|
||
const token = profile.value?.calendar_subscription_token
|
||
if (!token) return ""
|
||
|
||
const path = profile.value?.calendar_subscription_path || `/api/public/calendar/subscriptions/${token}.ics`
|
||
const apiBase = String(runtimeConfig.public.apiBase || "")
|
||
|
||
if (/^https?:\/\//i.test(apiBase)) {
|
||
const apiUrl = new URL(apiBase)
|
||
return new URL(path, `${apiUrl.protocol}//${apiUrl.host}`).toString()
|
||
}
|
||
|
||
if (process.client) {
|
||
return `${window.location.origin}${path}`
|
||
}
|
||
|
||
return path
|
||
})
|
||
|
||
const calendarSubscriptionWebcalUrl = computed(() =>
|
||
calendarSubscriptionHttpUrl.value
|
||
? calendarSubscriptionHttpUrl.value.replace(/^https?/i, "webcal")
|
||
: ""
|
||
)
|
||
|
||
async function fetchBranches() {
|
||
try {
|
||
branches.value = await useEntities("branches").select()
|
||
} catch (err) {
|
||
console.error('[fetchBranches]', err)
|
||
branches.value = []
|
||
}
|
||
}
|
||
|
||
async function fetchTeams() {
|
||
try {
|
||
teams.value = await useEntities("teams").select()
|
||
} catch (err) {
|
||
console.error('[fetchTeams]', err)
|
||
teams.value = []
|
||
}
|
||
}
|
||
|
||
/** Profil laden **/
|
||
async function fetchProfile() {
|
||
pending.value = true
|
||
try {
|
||
profile.value = await $api(`/api/profiles/${id}`)
|
||
ensureWorkingHoursStructure()
|
||
ensureBranchStructure()
|
||
ensureTeamStructure()
|
||
} catch (err: any) {
|
||
console.error('[fetchProfile]', err)
|
||
toast.add({
|
||
title: 'Fehler beim Laden',
|
||
description: err?.data?.error || err?.message || 'Unbekannter Fehler',
|
||
color: 'red'
|
||
})
|
||
} finally {
|
||
pending.value = false
|
||
}
|
||
}
|
||
|
||
function ensureBranchStructure() {
|
||
if (!profile.value) return
|
||
|
||
profile.value.branch_id = profile.value.branch_id ?? profile.value.branch?.id ?? null
|
||
|
||
if (!Array.isArray(profile.value.branch_ids)) {
|
||
if (Array.isArray(profile.value.branches)) {
|
||
profile.value.branch_ids = profile.value.branches
|
||
.map((entry: any) => entry?.id ?? entry)
|
||
.filter((entry: any) => entry != null)
|
||
} else {
|
||
profile.value.branch_ids = []
|
||
}
|
||
}
|
||
|
||
if (profile.value.branch_id && !profile.value.branch_ids.includes(profile.value.branch_id)) {
|
||
profile.value.branch_ids = [...profile.value.branch_ids, profile.value.branch_id]
|
||
}
|
||
}
|
||
|
||
function ensureTeamStructure() {
|
||
if (!profile.value) return
|
||
|
||
if (!Array.isArray(profile.value.team_ids)) {
|
||
if (Array.isArray(profile.value.teams)) {
|
||
profile.value.team_ids = profile.value.teams
|
||
.map((entry: any) => entry?.id ?? entry)
|
||
.filter((entry: any) => entry != null)
|
||
} else {
|
||
profile.value.team_ids = []
|
||
}
|
||
}
|
||
}
|
||
|
||
const updatePrimaryBranch = (value: number | null) => {
|
||
if (!profile.value) return
|
||
profile.value.branch_id = value
|
||
|
||
if (value && !profile.value.branch_ids.includes(value)) {
|
||
profile.value.branch_ids = [...profile.value.branch_ids, value]
|
||
}
|
||
}
|
||
|
||
const updateBranchMemberships = (values: number[]) => {
|
||
if (!profile.value) return
|
||
|
||
profile.value.branch_ids = values || []
|
||
|
||
if (profile.value.branch_id && !profile.value.branch_ids.includes(profile.value.branch_id)) {
|
||
profile.value.branch_id = null
|
||
}
|
||
}
|
||
|
||
const updateTeamMemberships = (values: number[]) => {
|
||
if (!profile.value) return
|
||
profile.value.team_ids = values || []
|
||
}
|
||
|
||
/** Profil speichern **/
|
||
async function saveProfile() {
|
||
if (saving.value) return
|
||
saving.value = true
|
||
|
||
try {
|
||
await $api(`/api/profiles/${id}`, {
|
||
method: 'PUT',
|
||
body: profile.value
|
||
})
|
||
toast.add({ title: 'Profil gespeichert', color: 'green' })
|
||
fetchProfile()
|
||
} catch (err: any) {
|
||
console.error('[saveProfile]', err)
|
||
toast.add({
|
||
title: 'Fehler beim Speichern',
|
||
description: err?.data?.error || err?.message || 'Unbekannter Fehler',
|
||
color: 'red'
|
||
})
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
function openCreateLinkedUserModal() {
|
||
if (!profile.value) return
|
||
|
||
createLinkedUserForm.email = profile.value.email || ""
|
||
createLinkedUserModalOpen.value = true
|
||
}
|
||
|
||
async function createLinkedUser() {
|
||
if (!profile.value || creatingLinkedUser.value) return
|
||
|
||
const email = createLinkedUserForm.email.trim().toLowerCase()
|
||
if (!email) {
|
||
toast.add({
|
||
title: 'E-Mail fehlt',
|
||
description: 'Bitte eine E-Mail-Adresse für den neuen Benutzer angeben.',
|
||
color: 'orange'
|
||
})
|
||
return
|
||
}
|
||
|
||
creatingLinkedUser.value = true
|
||
|
||
try {
|
||
const response = await admin.createUserForProfile(profile.value.id, { email })
|
||
|
||
createdLinkedUserPassword.value = response?.initialPassword || ""
|
||
createLinkedUserModalOpen.value = false
|
||
createLinkedUserForm.email = ""
|
||
|
||
toast.add({
|
||
title: 'Benutzer angelegt',
|
||
description: createdLinkedUserPassword.value ? `Initialpasswort: ${createdLinkedUserPassword.value}` : undefined,
|
||
color: 'green'
|
||
})
|
||
|
||
await fetchProfile()
|
||
} catch (err: any) {
|
||
console.error('[createLinkedUser]', err)
|
||
toast.add({
|
||
title: 'Benutzer konnte nicht angelegt werden',
|
||
description: err?.data?.error || err?.message || 'Unbekannter Fehler',
|
||
color: 'red'
|
||
})
|
||
} finally {
|
||
creatingLinkedUser.value = false
|
||
}
|
||
}
|
||
|
||
async function generateCalendarSubscription() {
|
||
if (!profile.value || generatingCalendarSubscription.value) return
|
||
|
||
generatingCalendarSubscription.value = true
|
||
|
||
try {
|
||
profile.value = await $api(`/api/profiles/${id}/calendar-subscription-token`, {
|
||
method: "POST"
|
||
})
|
||
|
||
ensureWorkingHoursStructure()
|
||
ensureBranchStructure()
|
||
ensureTeamStructure()
|
||
|
||
toast.add({
|
||
title: "Kalender-Abo erstellt",
|
||
description: "Der abonnierbare Kalender-Link wurde generiert.",
|
||
color: "green"
|
||
})
|
||
} catch (err: any) {
|
||
console.error("[generateCalendarSubscription]", err)
|
||
toast.add({
|
||
title: "Kalender-Abo konnte nicht erstellt werden",
|
||
description: err?.data?.error || err?.message || "Unbekannter Fehler",
|
||
color: "red"
|
||
})
|
||
} finally {
|
||
generatingCalendarSubscription.value = false
|
||
}
|
||
}
|
||
|
||
async function copyCalendarSubscriptionUrl(value: string, successTitle: string) {
|
||
if (!value) return
|
||
|
||
try {
|
||
await navigator.clipboard.writeText(value)
|
||
toast.add({
|
||
title: successTitle,
|
||
color: "green"
|
||
})
|
||
} catch (err) {
|
||
console.error("[copyCalendarSubscriptionUrl]", err)
|
||
toast.add({
|
||
title: "Link konnte nicht kopiert werden",
|
||
color: "red"
|
||
})
|
||
}
|
||
}
|
||
|
||
const weekdays = [
|
||
{ key: '1', label: 'Montag' },
|
||
{ key: '2', label: 'Dienstag' },
|
||
{ key: '3', label: 'Mittwoch' },
|
||
{ key: '4', label: 'Donnerstag' },
|
||
{ key: '5', label: 'Freitag' },
|
||
{ key: '6', label: 'Samstag' },
|
||
{ key: '7', label: 'Sonntag' }
|
||
]
|
||
|
||
const bundeslaender = [
|
||
{ code: 'DE-BW', name: 'Baden-Württemberg' },
|
||
{ code: 'DE-BY', name: 'Bayern' },
|
||
{ code: 'DE-BE', name: 'Berlin' },
|
||
{ code: 'DE-BB', name: 'Brandenburg' },
|
||
{ code: 'DE-HB', name: 'Bremen' },
|
||
{ code: 'DE-HH', name: 'Hamburg' },
|
||
{ code: 'DE-HE', name: 'Hessen' },
|
||
{ code: 'DE-MV', name: 'Mecklenburg-Vorpommern' },
|
||
{ code: 'DE-NI', name: 'Niedersachsen' },
|
||
{ code: 'DE-NW', name: 'Nordrhein-Westfalen' },
|
||
{ code: 'DE-RP', name: 'Rheinland-Pfalz' },
|
||
{ code: 'DE-SL', name: 'Saarland' },
|
||
{ code: 'DE-SN', name: 'Sachsen' },
|
||
{ code: 'DE-ST', name: 'Sachsen-Anhalt' },
|
||
{ code: 'DE-SH', name: 'Schleswig-Holstein' },
|
||
{ code: 'DE-TH', name: 'Thüringen' }
|
||
]
|
||
|
||
|
||
// Sicherstellen, dass das JSON-Feld existiert
|
||
function ensureWorkingHoursStructure() {
|
||
if (!profile.value.weekly_regular_working_hours) {
|
||
profile.value.weekly_regular_working_hours = {}
|
||
}
|
||
for (const { key } of weekdays) {
|
||
if (profile.value.weekly_regular_working_hours[key] == null) {
|
||
profile.value.weekly_regular_working_hours[key] = 0
|
||
}
|
||
}
|
||
}
|
||
|
||
function recalculateWeeklyHours() {
|
||
if (!profile.value?.weekly_regular_working_hours) return
|
||
|
||
const total = Object.values(profile.value.weekly_regular_working_hours).reduce(
|
||
(sum: number, val: any) => {
|
||
const num = parseFloat(val)
|
||
return sum + (isNaN(num) ? 0 : num)
|
||
},
|
||
0
|
||
)
|
||
|
||
profile.value.weekly_working_hours = Number(total.toFixed(2))
|
||
}
|
||
|
||
const getToday = () => {
|
||
const now = new Date()
|
||
const year = now.getFullYear()
|
||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||
const day = String(now.getDate()).padStart(2, '0')
|
||
return `${year}-${month}-${day}`
|
||
}
|
||
|
||
const setProfileDate = (field: 'birthday' | 'entry_date') => {
|
||
if (!profile.value) return
|
||
profile.value[field] = getToday()
|
||
}
|
||
|
||
const checkZip = async () => {
|
||
const zipData = await useFunctions().useZipCheck(profile.value.address_zip)
|
||
if (zipData) {
|
||
profile.value.address_zip = zipData.zip || profile.value.address_zip
|
||
profile.value.address_city = zipData.short
|
||
profile.value.state_code = zipData.state_code
|
||
}
|
||
}
|
||
|
||
onMounted(async () => {
|
||
await Promise.all([fetchBranches(), fetchTeams(), fetchProfile()])
|
||
})
|
||
|
||
</script>
|
||
|
||
<template>
|
||
<!-- Haupt-Navigation -->
|
||
<UDashboardNavbar title="Mitarbeiter">
|
||
<template #left>
|
||
<UButton
|
||
color="primary"
|
||
variant="outline"
|
||
@click="navigateTo(`/staff/profiles`)"
|
||
icon="i-heroicons-chevron-left"
|
||
|
||
>
|
||
Mitarbeiter
|
||
</UButton>
|
||
</template>
|
||
<template #center>
|
||
<h1 class="text-xl font-medium truncate">
|
||
Mitarbeiter bearbeiten: {{ profile?.full_name || '' }}
|
||
</h1>
|
||
</template>
|
||
</UDashboardNavbar>
|
||
|
||
<!-- Toolbar -->
|
||
<UDashboardToolbar>
|
||
<template #right>
|
||
<div class="flex items-center gap-2">
|
||
<UButton
|
||
v-if="canCreateLinkedUser"
|
||
icon="i-heroicons-user-plus"
|
||
color="neutral"
|
||
variant="outline"
|
||
@click="openCreateLinkedUserModal"
|
||
>
|
||
Benutzer anlegen
|
||
</UButton>
|
||
<UButton
|
||
icon="i-mdi-content-save"
|
||
color="primary"
|
||
:loading="saving"
|
||
@click="saveProfile"
|
||
>
|
||
Speichern
|
||
</UButton>
|
||
</div>
|
||
</template>
|
||
</UDashboardToolbar>
|
||
|
||
<!-- Inhalt -->
|
||
<UDashboardPanelContent>
|
||
<UCard v-if="!pending && profile">
|
||
<div class="flex items-center gap-4 mb-6">
|
||
<UAvatar size="xl" :alt="profile.full_name" />
|
||
<div>
|
||
<h2 class="text-xl font-semibold text-gray-900">{{ profile.full_name }}</h2>
|
||
<p class="text-sm text-gray-500">{{ profile.employee_number || '–' }}</p>
|
||
</div>
|
||
<UBadge :color="linkedUserStatusColor" variant="subtle">
|
||
{{ linkedUserStatusLabel }}
|
||
</UBadge>
|
||
</div>
|
||
|
||
<USeparator label="Persönliche Daten" />
|
||
|
||
<UForm :state="profile" @submit.prevent="saveProfile" class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
|
||
<UFormField label="Vorname" class="w-full">
|
||
<UInput v-model="profile.first_name" class="w-full" />
|
||
</UFormField>
|
||
|
||
<UFormField label="Nachname" class="w-full">
|
||
<UInput v-model="profile.last_name" class="w-full" />
|
||
</UFormField>
|
||
|
||
<UFormField label="E-Mail" class="w-full">
|
||
<UInput v-model="profile.email" class="w-full" />
|
||
</UFormField>
|
||
|
||
<UFormField label="Telefon (Mobil)" class="w-full">
|
||
<UInput v-model="profile.mobile_tel" class="w-full" />
|
||
</UFormField>
|
||
|
||
<UFormField label="Telefon (Festnetz)" class="w-full">
|
||
<UInput v-model="profile.fixed_tel" class="w-full" />
|
||
</UFormField>
|
||
|
||
<UFormField label="Geburtstag" class="w-full">
|
||
<div class="flex items-center gap-2">
|
||
<UInput type="date" v-model="profile.birthday" class="flex-1 w-full" />
|
||
<UButton color="gray" variant="soft" label="Heute" @click="setProfileDate('birthday')" />
|
||
</div>
|
||
</UFormField>
|
||
</UForm>
|
||
</UCard>
|
||
<UCard v-if="!pending && profile" class="mt-3">
|
||
<USeparator label="Vertragsinformationen" />
|
||
|
||
<UForm :state="profile" @submit.prevent="saveProfile" class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
|
||
<UFormField label="Vertragsart" class="w-full">
|
||
<UInput v-model="profile.contract_type" class="w-full"/>
|
||
</UFormField>
|
||
|
||
<UFormField label="Status" class="w-full">
|
||
<UInput v-model="profile.status" class="w-full"/>
|
||
</UFormField>
|
||
|
||
<UFormField label="Position" class="w-full">
|
||
<UInput v-model="profile.position" class="w-full"/>
|
||
</UFormField>
|
||
|
||
<UFormField label="Qualifikation" class="w-full">
|
||
<UInput v-model="profile.qualification" class="w-full"/>
|
||
</UFormField>
|
||
|
||
<UFormField label="Eintrittsdatum" class="w-full">
|
||
<div class="flex items-center gap-2">
|
||
<UInput type="date" v-model="profile.entry_date" class="flex-1 w-full" />
|
||
<UButton color="gray" variant="soft" label="Heute" @click="setProfileDate('entry_date')" />
|
||
</div>
|
||
</UFormField>
|
||
|
||
<UFormField label="Wöchentliche Arbeitszeit (Std)" class="w-full">
|
||
<UInput type="number" v-model="profile.weekly_working_hours" class="w-full" />
|
||
</UFormField>
|
||
|
||
<UFormField label="Bezahlte Urlaubstage (Jahr)" class="w-full">
|
||
<UInput type="number" v-model="profile.annual_paid_leave_days" class="w-full" />
|
||
</UFormField>
|
||
<UFormField label="Aktiv" class="w-full">
|
||
<div class="flex items-center gap-3">
|
||
<USwitch v-model="profile.active" color="primary" />
|
||
<span class="text-sm text-gray-600">
|
||
</span>
|
||
</div>
|
||
</UFormField>
|
||
</UForm>
|
||
|
||
</UCard>
|
||
|
||
<UCard v-if="!pending && profile" class="mt-3">
|
||
<USeparator label="Niederlassungen" />
|
||
|
||
<UForm :state="profile" @submit.prevent="saveProfile" class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
|
||
<UFormField label="Primäre Niederlassung" class="w-full">
|
||
<USelectMenu
|
||
:model-value="profile.branch_id"
|
||
:items="branches"
|
||
label-key="name"
|
||
value-key="id"
|
||
class="w-full"
|
||
:ui="selectMenuUi"
|
||
@update:model-value="updatePrimaryBranch"
|
||
/>
|
||
</UFormField>
|
||
|
||
<UFormField label="Weitere Niederlassungen" class="w-full">
|
||
<USelectMenu
|
||
:model-value="profile.branch_ids"
|
||
:items="branches"
|
||
label-key="name"
|
||
value-key="id"
|
||
multiple
|
||
class="w-full"
|
||
:ui="selectMenuUi"
|
||
@update:model-value="updateBranchMemberships"
|
||
/>
|
||
</UFormField>
|
||
</UForm>
|
||
</UCard>
|
||
|
||
<UCard v-if="!pending && profile" class="mt-3">
|
||
<USeparator label="Teams" />
|
||
|
||
<UForm :state="profile" @submit.prevent="saveProfile" class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
|
||
<UFormField label="Team-Zuordnung" class="w-full">
|
||
<USelectMenu
|
||
:model-value="profile.team_ids"
|
||
:items="teams"
|
||
label-key="name"
|
||
value-key="id"
|
||
multiple
|
||
class="w-full"
|
||
:ui="selectMenuUi"
|
||
@update:model-value="updateTeamMemberships"
|
||
/>
|
||
</UFormField>
|
||
|
||
<UFormField label="Hinweis" class="w-full">
|
||
<div class="text-sm text-gray-500 pt-2">
|
||
Teams können in den Stammdaten gepflegt und optional einer Niederlassung zugeordnet werden.
|
||
</div>
|
||
</UFormField>
|
||
</UForm>
|
||
</UCard>
|
||
|
||
<UCard v-if="!pending && profile" class="mt-3">
|
||
<USeparator label="Kalender-Abo" />
|
||
|
||
<div class="mt-4 space-y-4">
|
||
<p class="text-sm text-gray-500">
|
||
Hier kann ein abonnierbarer Kalender-Link für Handy-Kalender erzeugt werden. Das Abo nutzt einen persönlichen Backend-Link und kann in vielen Kalender-Apps direkt als `webcal` oder `ics` eingebunden werden.
|
||
</p>
|
||
|
||
<div class="flex flex-wrap gap-2">
|
||
<UButton
|
||
icon="i-heroicons-link"
|
||
color="primary"
|
||
:loading="generatingCalendarSubscription"
|
||
@click="generateCalendarSubscription"
|
||
>
|
||
{{ profile.calendar_subscription_token ? 'Link neu generieren' : 'Link generieren' }}
|
||
</UButton>
|
||
|
||
<UButton
|
||
v-if="calendarSubscriptionHttpUrl"
|
||
icon="i-heroicons-clipboard-document"
|
||
color="neutral"
|
||
variant="outline"
|
||
@click="copyCalendarSubscriptionUrl(calendarSubscriptionHttpUrl, 'ICS-Link kopiert')"
|
||
>
|
||
ICS kopieren
|
||
</UButton>
|
||
|
||
<UButton
|
||
v-if="calendarSubscriptionWebcalUrl"
|
||
icon="i-heroicons-device-phone-mobile"
|
||
color="neutral"
|
||
variant="outline"
|
||
@click="copyCalendarSubscriptionUrl(calendarSubscriptionWebcalUrl, 'Webcal-Link kopiert')"
|
||
>
|
||
Webcal kopieren
|
||
</UButton>
|
||
</div>
|
||
|
||
<UForm :state="profile" class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
<UFormField label="ICS-Link" class="w-full">
|
||
<UInput :model-value="calendarSubscriptionHttpUrl" readonly class="w-full" />
|
||
</UFormField>
|
||
|
||
<UFormField label="Webcal-Link" class="w-full">
|
||
<UInput :model-value="calendarSubscriptionWebcalUrl" readonly class="w-full" />
|
||
</UFormField>
|
||
</UForm>
|
||
</div>
|
||
</UCard>
|
||
|
||
<UCard v-if="!pending && profile" class="mt-3">
|
||
<USeparator label="Adresse & Standort" />
|
||
|
||
<UForm :state="profile" @submit.prevent="saveProfile" class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
|
||
<UFormField label="Straße und Hausnummer" class="w-full">
|
||
<UInput v-model="profile.address_street" class="w-full"/>
|
||
</UFormField>
|
||
|
||
<UFormField label="PLZ" class="w-full">
|
||
<UInput type="text" v-model="profile.address_zip" class="w-full" @focusout="checkZip"/>
|
||
</UFormField>
|
||
|
||
<UFormField label="Ort" class="w-full">
|
||
<UInput v-model="profile.address_city" class="w-full"/>
|
||
</UFormField>
|
||
|
||
<UFormField label="Bundesland" class="w-full">
|
||
<USelectMenu
|
||
v-model="profile.state_code"
|
||
:options="bundeslaender"
|
||
value-attribute="code"
|
||
option-attribute="name"
|
||
placeholder="Bundesland auswählen"
|
||
class="w-full"
|
||
:ui="selectMenuUi"
|
||
/>
|
||
</UFormField>
|
||
</UForm>
|
||
</UCard>
|
||
|
||
|
||
<UCard v-if="!pending && profile" class="mt-3">
|
||
<USeparator label="Wöchentliche Arbeitsstunden" />
|
||
|
||
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||
<div
|
||
v-for="day in weekdays"
|
||
:key="day.key"
|
||
:class="[...profile.weekly_regular_working_hours[day.key] === 0 ? ['bg-gray-100'] : ['bg-gray-100','border-green-400'], 'flex items-center justify-between border rounded-lg p-3 bg-gray-50']"
|
||
>
|
||
<span class="font-medium text-gray-700">{{ day.label }}</span>
|
||
<div class="flex items-center gap-2">
|
||
<UInput
|
||
type="number"
|
||
size="sm"
|
||
min="0"
|
||
max="24"
|
||
step="0.25"
|
||
v-model.number="profile.weekly_regular_working_hours[day.key]"
|
||
placeholder="0"
|
||
class="w-24"
|
||
@change="recalculateWeeklyHours"
|
||
/>
|
||
<span class="text-gray-400 text-sm">Std</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</UCard>
|
||
|
||
<UCard v-if="!pending && profile" class="mt-3">
|
||
<USeparator label="Sonstiges" />
|
||
<UForm :state="profile" @submit.prevent="saveProfile" class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
|
||
<UFormField label="Kleidergröße (Oberteil)" class="w-full">
|
||
<UInput v-model="profile.clothing_size_top" class="w-full" />
|
||
</UFormField>
|
||
|
||
<UFormField label="Kleidergröße (Hose)" class="w-full">
|
||
<UInput v-model="profile.clothing_size_bottom" class="w-full" />
|
||
</UFormField>
|
||
|
||
<UFormField label="Schuhgröße" class="w-full">
|
||
<UInput v-model="profile.clothing_size_shoe" class="w-full" />
|
||
</UFormField>
|
||
|
||
<UFormField label="Token-ID" class="w-full">
|
||
<UInput v-model="profile.token_id" class="w-full" />
|
||
</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>
|
||
</UCard>
|
||
|
||
<USkeleton v-if="pending" height="300px" />
|
||
|
||
<div v-if="createdLinkedUserPassword" class="mt-4">
|
||
<UAlert
|
||
title="Initialpasswort für den neuen Benutzer"
|
||
:description="createdLinkedUserPassword"
|
||
color="amber"
|
||
variant="soft"
|
||
close-button
|
||
@close="createdLinkedUserPassword = ''"
|
||
/>
|
||
</div>
|
||
</UDashboardPanelContent>
|
||
|
||
<UModal v-model:open="createLinkedUserModalOpen">
|
||
<template #content>
|
||
<UCard>
|
||
<template #header>
|
||
<div>
|
||
<div class="text-lg font-semibold">Benutzer zum Profil anlegen</div>
|
||
<p class="mt-1 text-sm text-gray-500">
|
||
Es wird automatisch ein zufälliges Initialpasswort erzeugt und der neue Benutzer direkt mit diesem Mitarbeiterprofil verknüpft.
|
||
</p>
|
||
</div>
|
||
</template>
|
||
|
||
<UForm :state="createLinkedUserForm" class="space-y-4" @submit.prevent="createLinkedUser">
|
||
<UFormField label="E-Mail">
|
||
<UInput v-model="createLinkedUserForm.email" type="email" autocomplete="email" />
|
||
</UFormField>
|
||
|
||
<div class="flex justify-end gap-3 pt-2">
|
||
<UButton color="gray" variant="soft" @click="createLinkedUserModalOpen = false">
|
||
Abbrechen
|
||
</UButton>
|
||
<UButton type="submit" color="primary" :loading="creatingLinkedUser">
|
||
Benutzer anlegen
|
||
</UButton>
|
||
</div>
|
||
</UForm>
|
||
</UCard>
|
||
</template>
|
||
</UModal>
|
||
</template>
|