thats-me/frontend/_src/pages/PersonSelector.vue
2026-04-22 12:57:10 +02:00

423 lines
No EOL
10 KiB
Vue
Raw Permalink 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.

<template>
<q-page padding class="person-selector-page">
<div class="row justify-center">
<div class="col-12 col-md-8 col-lg-6">
<q-card>
<q-card-section>
<div class="text-h5">Select Related Persons</div>
<div class="text-subtitle2 text-grey">Choose people related to this life event</div>
</q-card-section>
<q-card-section>
<!-- Search bar -->
<q-input
v-model="searchQuery"
label="Search persons..."
filled
clearable
class="q-mb-md"
>
<template v-slot:prepend>
<q-icon name="search" />
</template>
</q-input>
<!-- Person list -->
<div class="person-list q-gutter-md">
<div
v-for="person in filteredPersons"
:key="person.id"
class="person-item"
:class="{ 'selected': isPersonSelected(person.id) }"
@click="togglePersonSelection(person.id)"
>
<div class="person-avatar">
<q-avatar
v-if="person.avatar"
size="60px"
class="person-avatar-img"
>
<img :src="person.avatar" :alt="person.name" />
</q-avatar>
<q-avatar
v-else
size="60px"
color="primary"
text-color="white"
class="person-avatar-initials"
>
{{ getInitials(person.name) }}
</q-avatar>
<!-- Selection indicator -->
<q-icon
v-if="isPersonSelected(person.id)"
name="check_circle"
color="positive"
size="24px"
class="selection-indicator"
/>
</div>
<div class="person-info">
<div class="person-name">{{ person.name }}</div>
<div v-if="person.role" class="person-role text-grey">{{ person.role }}</div>
</div>
</div>
</div>
</q-card-section>
<!-- Selected persons section -->
<q-card-section v-if="selectedPersons.length > 0" class="selected-section">
<q-separator class="q-mb-md" />
<div class="text-subtitle2 q-mb-md">Selected Persons ({{ selectedPersons.length }})</div>
<div class="selected-persons-container">
<div class="selected-persons-list">
<div
v-for="person in selectedPersonsData"
:key="person.id"
class="selected-person"
@mouseenter="hoveredPersonId = person.id"
@mouseleave="hoveredPersonId = null"
>
<q-avatar
v-if="person.avatar"
size="50px"
class="selected-avatar"
>
<img :src="person.avatar" :alt="person.name" />
</q-avatar>
<q-avatar
v-else
size="50px"
color="primary"
text-color="white"
class="selected-avatar"
>
{{ getInitials(person.name) }}
</q-avatar>
<!-- Remove button on hover -->
<div
v-if="hoveredPersonId === person.id"
class="remove-person"
@click.stop="removePersonSelection(person.id)"
>
×
</div>
<div class="selected-person-name">{{ person.name }}</div>
</div>
</div>
</div>
</q-card-section>
</q-card>
</div>
</div>
<!-- Fixed Footer with Action Buttons -->
<q-footer elevated class="bg-white text-dark selector-footer">
<q-toolbar class="justify-between">
<q-btn
icon="arrow_back"
color="grey"
flat
round
@click="onCancel"
class="q-ml-sm"
>
<q-tooltip class="bg-grey">Cancel</q-tooltip>
</q-btn>
<q-btn
label="Add Persons"
color="primary"
unelevated
:disable="selectedPersons.length === 0"
@click="onAddPersons"
class="q-mr-sm"
/>
</q-toolbar>
</q-footer>
</q-page>
</template>
<script>
import { ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { personOptions } from '../utils/editFormOptions.js'
export default {
name: 'PersonSelector',
setup() {
const router = useRouter()
const route = useRoute()
// State
const searchQuery = ref('')
const selectedPersons = ref([])
const hoveredPersonId = ref(null)
// Convert simple person names to objects with IDs
const persons = ref(
personOptions.map((name, index) => ({
id: index + 1,
name: name,
avatar: null, // Could be loaded from API later
role: null // Could be added later
}))
)
// Computed
const filteredPersons = computed(() => {
if (!searchQuery.value) {
return persons.value
}
return persons.value.filter(person =>
person.name.toLowerCase().includes(searchQuery.value.toLowerCase())
)
})
const selectedPersonsData = computed(() => {
return persons.value.filter(person => selectedPersons.value.includes(person.id))
})
// Methods
const getInitials = (name) => {
return name
.split(' ')
.map(word => word.charAt(0).toUpperCase())
.slice(0, 2)
.join('')
}
const isPersonSelected = (personId) => {
return selectedPersons.value.includes(personId)
}
const togglePersonSelection = (personId) => {
const index = selectedPersons.value.indexOf(personId)
if (index > -1) {
selectedPersons.value.splice(index, 1)
} else {
selectedPersons.value.push(personId)
}
}
const removePersonSelection = (personId) => {
const index = selectedPersons.value.indexOf(personId)
if (index > -1) {
selectedPersons.value.splice(index, 1)
}
}
const onCancel = () => {
router.go(-1)
}
const onAddPersons = () => {
// Pass selected persons back to the edit page
const selectedPersonsNames = selectedPersonsData.value.map(person => person.name)
// Navigate back with the selected persons data
// We'll use query parameters to pass the data
router.push({
name: 'edit',
query: {
...route.query,
selectedPersons: JSON.stringify(selectedPersonsNames)
}
})
}
// Load previously selected persons from route query
onMounted(() => {
if (route.query.currentSelection) {
try {
const currentSelection = JSON.parse(route.query.currentSelection)
// Convert names back to IDs
selectedPersons.value = persons.value
.filter(person => currentSelection.includes(person.name))
.map(person => person.id)
} catch (error) {
console.error('Error parsing current selection:', error)
}
}
})
return {
searchQuery,
selectedPersons,
hoveredPersonId,
persons,
filteredPersons,
selectedPersonsData,
getInitials,
isPersonSelected,
togglePersonSelection,
removePersonSelection,
onCancel,
onAddPersons
}
}
}
</script>
<style scoped>
.person-selector-page {
min-height: 100vh;
padding-bottom: 80px; /* Space for footer */
}
.person-list {
max-height: 400px;
overflow-y: auto;
}
.person-item {
display: flex;
align-items: center;
padding: 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
border: 2px solid transparent;
}
.person-item:hover {
background-color: rgba(25, 118, 210, 0.1);
}
.person-item.selected {
background-color: rgba(25, 118, 210, 0.15);
border-color: var(--q-primary);
}
.person-avatar {
position: relative;
margin-right: 16px;
}
.selection-indicator {
position: absolute;
top: -4px;
right: -4px;
background: white;
border-radius: 50%;
}
.person-info {
flex: 1;
}
.person-name {
font-weight: 500;
font-size: 16px;
line-height: 1.2;
}
.person-role {
font-size: 14px;
margin-top: 2px;
}
.selected-section {
background-color: rgba(0, 0, 0, 0.02);
}
.selected-persons-container {
max-height: 200px;
overflow-y: auto;
}
.selected-persons-list {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.selected-person {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding: 8px;
border-radius: 8px;
transition: all 0.2s ease;
cursor: pointer;
min-width: 80px;
}
.selected-person:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.selected-avatar {
margin-bottom: 8px;
}
.selected-person-name {
font-size: 12px;
text-align: center;
line-height: 1.2;
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.remove-person {
position: absolute;
top: 4px;
right: 4px;
width: 20px;
height: 20px;
background: rgba(244, 67, 54, 0.9);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: bold;
cursor: pointer;
transition: all 0.2s ease;
}
.remove-person:hover {
background: #f44336;
transform: scale(1.1);
}
/* Responsive design */
@media (max-width: 600px) {
.selected-persons-list {
justify-content: center;
}
.person-item {
padding: 8px;
}
.person-avatar {
margin-right: 12px;
}
}
/* Footer styling */
.selector-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 1000;
border-top: 1px solid #e0e0e0;
}
.selector-footer .q-toolbar {
min-height: 60px;
padding: 8px 16px;
}
</style>