Files
FEDEO/frontend/pages/staff/profiles/[id].vue
florianfederspiel 849e24092e
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 26s
Build and Push Docker Images / build-frontend (push) Successful in 1m7s
Added Teams
Minor Rework of Plantafel
2026-04-14 21:17:05 +02:00

483 lines
15 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 { $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 selectMenuUi = {
base: 'w-full',
content: 'min-w-[min(32rem,90vw)] w-max max-w-[90vw]'
}
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
}
}
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>
<UButton
icon="i-mdi-content-save"
color="primary"
:loading="saving"
@click="saveProfile"
>
Speichern
</UButton>
</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>
</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" />
</UDashboardPanelContent>
</template>