166 lines
6.0 KiB
Vue
166 lines
6.0 KiB
Vue
<script setup lang="ts">
|
|
import { useMemberDirectory } from '@/stores/memberDirectory';
|
|
import { ref, onMounted, computed } from 'vue';
|
|
import { Member, MemberCardDetails, type MemberLight } from '@shared/types/member'
|
|
import Popover from '../ui/popover/Popover.vue';
|
|
import PopoverTrigger from '../ui/popover/PopoverTrigger.vue';
|
|
import PopoverContent from '../ui/popover/PopoverContent.vue';
|
|
import { cn } from '@/lib/utils.js'
|
|
import { watch } from 'vue';
|
|
import { format } from 'path';
|
|
import Spinner from '../ui/spinner/Spinner.vue';
|
|
import { Dot } from 'lucide-vue-next';
|
|
|
|
|
|
// Props
|
|
const props = defineProps({
|
|
memberId: {
|
|
type: Number,
|
|
required: true
|
|
}
|
|
});
|
|
|
|
// Local state
|
|
const memberLight = ref<MemberLight | null>(null);
|
|
const memberFull = ref<MemberCardDetails | null>(null)
|
|
const loadingFull = ref(false)
|
|
const membersStore = useMemberDirectory();
|
|
|
|
// Fetch the light member data on mount
|
|
onMounted(async () => {
|
|
memberLight.value = await membersStore.getLight(props.memberId);
|
|
});
|
|
|
|
async function loadFull() {
|
|
if (memberFull.value || loadingFull.value) return
|
|
|
|
loadingFull.value = true
|
|
try {
|
|
memberFull.value = await membersStore.getFull(props.memberId)
|
|
} finally {
|
|
loadingFull.value = false
|
|
}
|
|
}
|
|
|
|
watch(() => props.memberId, async (newId) => {
|
|
memberLight.value = await membersStore.getLight(newId);
|
|
memberFull.value = null;
|
|
loadingFull.value = false;
|
|
});
|
|
|
|
// Compute display name (displayName fallback to username)
|
|
const displayName = computed(() => {
|
|
if (!memberLight.value) return props.memberId;
|
|
return memberLight.value.displayName || memberLight.value.username;
|
|
});
|
|
|
|
const DEFAULT_TEXT_COLOR = '#9ca3af' // muted gray for text
|
|
const DEFAULT_BG_COLOR = '#d1d5db22' // muted gray ~20% opacity
|
|
|
|
const textColor = computed(() => memberLight.value?.color || DEFAULT_TEXT_COLOR)
|
|
const bgColor = computed(() => (memberLight.value?.color ? `${memberLight.value.color}22` : DEFAULT_BG_COLOR))
|
|
|
|
const hasFullInfo = computed(() => {
|
|
if (!memberFull.value) return false
|
|
|
|
// check if any field has a value
|
|
const { rank, unit, status } = memberFull.value.member
|
|
return !!(rank || unit || status)
|
|
})
|
|
|
|
function formatDate(date: Date): string {
|
|
if (!date) return "";
|
|
date = typeof date === 'string' ? new Date(date) : date;
|
|
return date.toLocaleDateString("en-US", {
|
|
year: "numeric",
|
|
month: "short",
|
|
day: "numeric",
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<Popover @update:open="open => open && loadFull()">
|
|
<PopoverTrigger @click.stop>
|
|
<p :class="cn(
|
|
'px-2 py-1 rounded font-medium inline-flex items-center cursor-pointer'
|
|
)" :style="{
|
|
color: textColor,
|
|
backgroundColor: bgColor
|
|
}">
|
|
{{ displayName }}
|
|
</p>
|
|
</PopoverTrigger>
|
|
<PopoverContent class="w-80 p-0 overflow-hidden">
|
|
<!-- Loading -->
|
|
<div v-if="loadingFull" class="p-4 text-sm text-muted-foreground mx-auto flex justify-center my-5">
|
|
<Spinner></Spinner>
|
|
</div>
|
|
|
|
<!-- Profile -->
|
|
<div v-else-if="memberFull">
|
|
<!-- Header -->
|
|
<div class="px-4 py-3 relative" :style="{ backgroundColor: `${memberLight?.color}22` }">
|
|
<!-- Display name / username -->
|
|
<div class="text-lg font-semibold leading-tight" :style="{ color: memberLight?.color }">
|
|
{{ displayName }}
|
|
</div>
|
|
|
|
<div v-if="memberLight.displayName" class="text-xs text-muted-foreground">
|
|
{{ memberLight?.username }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Body -->
|
|
<div class="p-4 space-y-3 text-sm">
|
|
<!-- Full info -->
|
|
<template v-if="hasFullInfo">
|
|
<div v-if="memberFull.member.loa_until"
|
|
class=" rounded-md text-center bg-yellow-500/10 px-2 py-1 text-xs text-yellow-600">
|
|
On Leave of Absence until {{ formatDate(memberFull.member.loa_until) }}
|
|
</div>
|
|
|
|
|
|
<div v-if="memberFull.member.rank" class="flex justify-between">
|
|
<span class="text-muted-foreground">Rank</span>
|
|
<span class="font-medium">{{ memberFull.member.rank }}</span>
|
|
</div>
|
|
|
|
<div v-if="memberFull.member.unit" class="flex justify-between items-center">
|
|
<span class="text-muted-foreground">Unit</span>
|
|
<span class="font-medium flex items-center gap-2">
|
|
<span class="w-2 h-2 rounded-full" :style="{ backgroundColor: textColor }"></span>
|
|
{{ memberFull.member.unit }}
|
|
</span>
|
|
</div>
|
|
|
|
<div v-if="memberFull.member.status" class="flex justify-between">
|
|
<span class="text-muted-foreground">Status</span>
|
|
<span class="font-medium">{{ memberFull.member.status }}</span>
|
|
</div>
|
|
|
|
|
|
<div class="flex gap-2 flex-wrap mt-6">
|
|
<div v-for="role in memberFull.roles" class="border rounded-full px-3 text-nowrap">
|
|
{{ role.name }}
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- No info fallback -->
|
|
<div v-else class="text-sm text-muted-foreground italic">
|
|
No user info
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Not found -->
|
|
<div v-else class="p-4 text-sm text-muted-foreground">
|
|
Member not found
|
|
</div>
|
|
</PopoverContent>
|
|
|
|
</Popover>
|
|
</template>
|