Initial commit
This commit is contained in:
265
src/components/PersonList.vue
Normal file
265
src/components/PersonList.vue
Normal file
@@ -0,0 +1,265 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useEntitiesStore } from '@PeopleManager/stores/entitiesStore'
|
||||
import type { CollectionObject } from '@PeopleManager/models/collection'
|
||||
import type { EntityObject } from '@PeopleManager/models/entity'
|
||||
|
||||
// Stores
|
||||
const entitiesStore = useEntitiesStore()
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
selectedCollection?: CollectionObject | null
|
||||
selectedEntity?: EntityObject | null
|
||||
}>()
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
'select': [entity: EntityObject]
|
||||
'fresh': [type: string]
|
||||
}>()
|
||||
|
||||
// State
|
||||
const loading = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const collectionEntities = ref<EntityObject[]>([])
|
||||
|
||||
// Computed
|
||||
const filteredEntities = computed(() => {
|
||||
if (!searchQuery.value) {
|
||||
return collectionEntities.value
|
||||
}
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
return collectionEntities.value.filter(entity => {
|
||||
const name = `${entity.data?.names.family || ''} ${entity.data?.names.given || ''}`.toLowerCase()
|
||||
const email = (entity.data?.emails?.[0]?.address || '').toLowerCase()
|
||||
return name.includes(query) || email.includes(query)
|
||||
})
|
||||
})
|
||||
|
||||
// Watchers
|
||||
watch(() => props.selectedCollection, async (newCollection) => {
|
||||
if (newCollection) {
|
||||
loading.value = true
|
||||
try {
|
||||
collectionEntities.value = await entitiesStore.list(
|
||||
newCollection.provider,
|
||||
newCollection.service,
|
||||
newCollection.id
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('[People] - Failed to load contacts:', error);
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
} else {
|
||||
collectionEntities.value = []
|
||||
loading.value = false
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// Functions
|
||||
function entityType(entity: EntityObject): string {
|
||||
return entity.data?.type || 'individual'
|
||||
}
|
||||
|
||||
function entityIcon(entity: EntityObject): string {
|
||||
const type = entityType(entity)
|
||||
switch (type) {
|
||||
case 'organization':
|
||||
return 'mdi-domain'
|
||||
case 'group':
|
||||
return 'mdi-account-group'
|
||||
default:
|
||||
return 'mdi-account'
|
||||
}
|
||||
}
|
||||
|
||||
function entityPhoto(entity: EntityObject): string | null {
|
||||
// TODO: Implement photo retrieval from entity data
|
||||
return null
|
||||
}
|
||||
|
||||
function entityInitials(entity: EntityObject): string {
|
||||
const type = entityType(entity)
|
||||
|
||||
if (type === 'organization') {
|
||||
const name = (entity.data as any)?.names?.full || entity.data?.label || ''
|
||||
return name.substring(0, 2).toUpperCase() || 'OR'
|
||||
}
|
||||
|
||||
if (type === 'group') {
|
||||
const name = (entity.data as any)?.names?.full || entity.data?.label || ''
|
||||
return name.substring(0, 2).toUpperCase() || 'GR'
|
||||
}
|
||||
|
||||
// Individual
|
||||
const given = (entity.data as any)?.names?.given
|
||||
const family = (entity.data as any)?.names?.family
|
||||
const initials = `${given?.[0] || ''}${family?.[0] || ''}`.toUpperCase()
|
||||
return initials || '?'
|
||||
}
|
||||
|
||||
function entityLabel(entity: EntityObject): string {
|
||||
const type = entityType(entity)
|
||||
|
||||
if (type === 'organization' || type === 'group') {
|
||||
return entity.data?.label || (entity.data as any)?.names?.full || 'Unknown'
|
||||
}
|
||||
|
||||
// Individual
|
||||
return entity.data?.label || `${(entity.data as any)?.names?.given || ''} ${(entity.data as any)?.names?.family || ''}`.trim() || 'Unknown'
|
||||
}
|
||||
|
||||
function entityEmail(entity: EntityObject): string | null {
|
||||
const emails = (entity.data as any)?.emails
|
||||
if (!emails) return null
|
||||
const emailEntries = Object.values(emails)
|
||||
if (emailEntries.length === 0) return null
|
||||
// Sort by priority (assuming lower number = higher priority)
|
||||
emailEntries.sort((a: any, b: any) => (a.priority || 999) - (b.priority || 999))
|
||||
return (emailEntries[0] as any).address || null
|
||||
}
|
||||
|
||||
function entityOrganization(entity: EntityObject): string | null {
|
||||
const organizations = (entity.data as any)?.organizations
|
||||
if (!organizations || Object.keys(organizations).length === 0) return null
|
||||
const orgEntries = Object.values(organizations)
|
||||
return (orgEntries[0] as any).Label || null
|
||||
}
|
||||
|
||||
function entityMemberCount(entity: EntityObject): number | null {
|
||||
const type = entityType(entity)
|
||||
if (type !== 'group') return null
|
||||
const members = (entity.data as any)?.members
|
||||
if (!members) return 0
|
||||
return Object.keys(members).length
|
||||
}
|
||||
|
||||
function entityAvatarColor(entity: EntityObject): string {
|
||||
const type = entityType(entity)
|
||||
switch (type) {
|
||||
case 'organization':
|
||||
return 'blue-darken-1'
|
||||
case 'group':
|
||||
return 'green-darken-1'
|
||||
default:
|
||||
return 'primary'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="person-list-container">
|
||||
<div class="person-list-header pa-4">
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
label="Search contacts"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
clearable
|
||||
hide-details
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pa-0 person-list-content">
|
||||
<v-progress-linear v-if="loading" indeterminate color="primary" />
|
||||
|
||||
<div v-else-if="!selectedCollection" class="text-center pa-8">
|
||||
<v-icon icon="mdi-book-alert" size="64" color="grey" class="mb-4" />
|
||||
<p class="text-body-1 text-grey">Select an address book to view contacts</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="filteredEntities.length === 0" class="text-center pa-8">
|
||||
<v-icon icon="mdi-account-search" size="64" color="grey" class="mb-4" />
|
||||
<p class="text-body-1 text-grey">No Individuals, Organizations or Groups found</p>
|
||||
</div>
|
||||
|
||||
<v-list v-else density="compact" nav>
|
||||
<v-list-item
|
||||
v-for="entity in filteredEntities"
|
||||
:key="entity.id"
|
||||
:value="entity.id"
|
||||
@click="$emit('select', entity)">
|
||||
|
||||
<template #prepend>
|
||||
<v-avatar :color="entityAvatarColor(entity)" size="40">
|
||||
<v-img v-if="entityPhoto(entity)" :src="entityPhoto(entity)!" />
|
||||
<v-icon v-else-if="entityType(entity) !== 'individual'" :icon="entityIcon(entity)" size="small" />
|
||||
<span v-else class="text-subtitle-2">{{ entityInitials(entity) }}</span>
|
||||
</v-avatar>
|
||||
</template>
|
||||
|
||||
<v-list-item-title>
|
||||
<v-icon :icon="entityIcon(entity)" size="x-small" class="mr-1" />
|
||||
{{ entityLabel(entity) }}
|
||||
</v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle>
|
||||
<!-- For individuals: show email and organization -->
|
||||
<template v-if="entityType(entity) === 'individual'">
|
||||
<div v-if="entityEmail(entity)">
|
||||
<v-icon icon="mdi-email" size="x-small" class="mr-1" />
|
||||
{{ entityEmail(entity) }}
|
||||
</div>
|
||||
<div v-if="entityOrganization(entity)">
|
||||
<v-icon icon="mdi-domain" size="x-small" class="mr-1" />
|
||||
{{ entityOrganization(entity) }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- For organizations: show email -->
|
||||
<template v-else-if="entityType(entity) === 'organization'">
|
||||
<div v-if="entityEmail(entity)">
|
||||
<v-icon icon="mdi-email" size="x-small" class="mr-1" />
|
||||
{{ entityEmail(entity) }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- For groups: show member count -->
|
||||
<template v-else-if="entityType(entity) === 'group'">
|
||||
<div>
|
||||
<v-icon icon="mdi-account-multiple" size="x-small" class="mr-1" />
|
||||
{{ entityMemberCount(entity) }} member{{ entityMemberCount(entity) !== 1 ? 's' : '' }}
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.person-list-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.person-list-header {
|
||||
min-height: 72px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid rgb(var(--v-border-color));
|
||||
}
|
||||
|
||||
.person-list-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.person-list-container :deep(.v-list) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.person-list-container :deep(.v-field) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.v-list-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user