Files
FEDEO/frontend/pages/staff/profiles/[id].vue

598 lines
18 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">
const route = useRoute()
const toast = useToast()
const auth = useAuthStore()
const admin = useAdmin()
const { $api } = useNuxtApp()
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 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")
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
}
}
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="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>
</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>