feat: Enhance user entity with project-related serialization groups

This commit is contained in:
olli 2025-11-14 11:59:31 +01:00
parent 1e02439e8a
commit ef77c1e6f1
2 changed files with 86 additions and 19 deletions

View File

@ -307,15 +307,16 @@
<div class="flex flex-col gap-2">
<label for="teamMembers">Team-Mitglieder</label>
<Select
<MultiSelect
id="teamMembers"
v-model="editingProject.teamMembers"
:options="users"
option-label="email"
placeholder="Team-Mitglieder auswählen"
filter
multiple
display="chip"
:disabled="saving"
:max-selected-labels="3"
>
<template #option="slotProps">
<div class="flex flex-col">
@ -325,7 +326,7 @@
</span>
</div>
</template>
</Select>
</MultiSelect>
<small class="text-500">Team-Mitglieder können das Projekt ansehen und bearbeiten</small>
</div>
</div>
@ -703,6 +704,42 @@
</div>
</div>
<!-- Team & Access -->
<div class="p-4 border-round border-1 surface-border">
<div class="font-semibold text-lg mb-3">Team & Zugriff</div>
<div class="grid grid-cols-2 gap-4">
<div class="flex flex-col gap-2">
<label class="font-medium text-sm text-500">Eigentümer</label>
<div v-if="viewingProject.owner" class="flex align-items-center gap-2">
<i class="pi pi-user text-primary"></i>
<div>
<div class="font-medium">{{ viewingProject.owner.email }}</div>
<div v-if="viewingProject.owner.firstName || viewingProject.owner.lastName" class="text-sm text-500">
{{ viewingProject.owner.firstName }} {{ viewingProject.owner.lastName }}
</div>
</div>
</div>
<div v-else class="text-900">-</div>
</div>
<div class="flex flex-col gap-2">
<label class="font-medium text-sm text-500">Team-Mitglieder</label>
<div v-if="viewingProject.teamMembers && viewingProject.teamMembers.length > 0" class="flex flex-col gap-2">
<div v-for="member in viewingProject.teamMembers" :key="member.id" class="flex align-items-center gap-2">
<i class="pi pi-users text-500"></i>
<div>
<div class="font-medium">{{ member.email }}</div>
<div v-if="member.firstName || member.lastName" class="text-sm text-500">
{{ member.firstName }} {{ member.lastName }}
</div>
</div>
</div>
</div>
<div v-else class="text-500">Keine Team-Mitglieder</div>
</div>
</div>
</div>
<!-- Documents Section -->
<div class="p-4 border-round border-1 surface-border">
<div class="font-semibold text-lg mb-3">Dokumente</div>
@ -819,6 +856,7 @@ import Button from 'primevue/button'
import InputText from 'primevue/inputtext'
import Textarea from 'primevue/textarea'
import Select from 'primevue/select'
import MultiSelect from 'primevue/multiselect'
import DatePicker from 'primevue/datepicker'
import InputNumber from 'primevue/inputnumber'
import RadioButton from 'primevue/radiobutton'
@ -898,7 +936,11 @@ const projectColumns = ref([
])
onMounted(async () => {
await Promise.all([loadCustomers(), loadStatuses(), loadUsers(), loadCurrentUser()])
await Promise.all([loadCustomers(), loadStatuses(), loadUsers()])
// Set current user to first user as fallback (until /api/me endpoint is implemented)
if (users.value.length > 0) {
currentUser.value = users.value[0]
}
})
async function loadCustomers() {
@ -967,15 +1009,10 @@ async function loadUsers() {
}
async function loadCurrentUser() {
try {
const response = await fetch('/api/me')
if (!response.ok) throw new Error('Fehler beim Laden des aktuellen Benutzers')
currentUser.value = await response.json()
} catch (error) {
console.error('Error loading current user:', error)
currentUser.value = null
}
// TODO: Implement proper /api/me endpoint
// For now, use the first user from the users list as a fallback
// This will be set after loadUsers() completes
currentUser.value = null
}
function filterByType(type, loadData) {
@ -1056,6 +1093,33 @@ function openNewProjectDialog() {
}
function editProject(project) {
// Find owner object from users array
let ownerObject = null
if (project.owner) {
if (typeof project.owner === 'object' && project.owner.id) {
ownerObject = users.value.find(u => u.id === project.owner.id) || project.owner
} else if (typeof project.owner === 'string') {
// Extract ID from IRI like "/api/users/1"
const ownerId = parseInt(project.owner.split('/').pop())
ownerObject = users.value.find(u => u.id === ownerId)
}
}
// Find team member objects from users array
let teamMembersArray = []
if (Array.isArray(project.teamMembers)) {
teamMembersArray = project.teamMembers.map(member => {
if (typeof member === 'object' && member.id) {
return users.value.find(u => u.id === member.id) || member
} else if (typeof member === 'string') {
// Extract ID from IRI like "/api/users/1"
const memberId = parseInt(member.split('/').pop())
return users.value.find(u => u.id === memberId)
}
return null
}).filter(m => m !== null)
}
// Convert date strings to Date objects for Calendar component
editingProject.value = {
...project,
@ -1064,7 +1128,8 @@ function editProject(project) {
endDate: project.endDate ? new Date(project.endDate) : null,
budget: project.budget ? parseFloat(project.budget) : null,
hourContingent: project.hourContingent ? parseFloat(project.hourContingent) : null,
teamMembers: project.teamMembers || []
owner: ownerObject,
teamMembers: teamMembersArray
}
submitted.value = false
projectDialog.value = true
@ -1330,7 +1395,9 @@ async function saveProject() {
hourContingent: editingProject.value.hourContingent ? editingProject.value.hourContingent.toString() : null,
isPrivate: editingProject.value.isPrivate,
owner: `/api/users/${editingProject.value.owner.id}`,
teamMembers: editingProject.value.teamMembers?.map(member => `/api/users/${member.id}`) || []
teamMembers: Array.isArray(editingProject.value.teamMembers)
? editingProject.value.teamMembers.map(member => `/api/users/${member.id}`)
: []
}
const isNew = !editingProject.value.id

View File

@ -37,21 +37,21 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['user:read', 'document:read'])]
#[Groups(['user:read', 'document:read', 'project:read'])]
private ?int $id = null;
#[ORM\Column(length: 180)]
#[Groups(['user:read', 'user:write'])]
#[Groups(['user:read', 'user:write', 'project:read'])]
#[Assert\NotBlank(message: 'Die E-Mail-Adresse darf nicht leer sein')]
#[Assert\Email(message: 'Bitte geben Sie eine gültige E-Mail-Adresse ein')]
private ?string $email = null;
#[ORM\Column(length: 100)]
#[Groups(['user:read', 'user:write', 'document:read'])]
#[Groups(['user:read', 'user:write', 'document:read', 'project:read'])]
private ?string $firstName = null;
#[ORM\Column(length: 100)]
#[Groups(['user:read', 'user:write', 'document:read'])]
#[Groups(['user:read', 'user:write', 'document:read', 'project:read'])]
private ?string $lastName = null;
#[ORM\Column]