added roles to member card

This commit is contained in:
2025-12-18 17:39:44 -05:00
parent a699c20f9b
commit f124e41630
5 changed files with 81 additions and 27 deletions

View File

@@ -1,5 +1,6 @@
import { Role } from "@app/shared/types/roles";
import pool from "../db"; import pool from "../db";
import { Member, MemberLight, memberSettings, MemberState } from '@app/shared/types/member' import { Member, MemberCardDetails, MemberLight, memberSettings, MemberState } from '@app/shared/types/member'
export async function getUserData(userID: number): Promise<Member> { export async function getUserData(userID: number): Promise<Member> {
const sql = `SELECT * FROM view_member_rank_unit_status_latest WHERE member_id = ?`; const sql = `SELECT * FROM view_member_rank_unit_status_latest WHERE member_id = ?`;
@@ -60,10 +61,50 @@ export async function getAllMembersLite(): Promise<MemberLight[]> {
return res; return res;
} }
export async function getMembersFull(ids: number[]): Promise<Member[]> { export async function getMembersFull(ids: number[]): Promise<MemberCardDetails[]> {
const sql = `SELECT * FROM view_member_rank_unit_status_latest WHERE member_id IN (?);`; const sql = `
const res: Member[] = await pool.query(sql, [ids]); SELECT m.*,
return res; COALESCE(
JSON_ARRAYAGG(
CASE
WHEN r.id IS NOT NULL THEN JSON_OBJECT(
'id', r.id,
'name', r.name,
'color', r.color,
'description', r.description
)
END
),
JSON_ARRAY()
) AS roles
FROM view_member_rank_unit_status_latest m
LEFT JOIN members_roles mr ON m.member_id = mr.member_id
LEFT JOIN roles r ON mr.role_id = r.id
WHERE m.member_id IN (?)
GROUP BY m.member_id;
`;
const rows: any[] = await pool.query(sql, [ids]);
return rows.map(row => {
const member: Member = {
member_id: row.member_id,
member_name: row.member_name,
displayName: row.displayName,
rank: row.rank,
rank_date: row.rank_date,
unit: row.unit,
unit_date: row.unit_date,
status: row.status,
status_date: row.status_date,
loa_until: row.loa_until ? new Date(row.loa_until) : undefined,
};
// roles comes as array of strings; parse each one
const roles: Role[] = JSON.parse(row.roles).map((r: string) => JSON.parse(r));
return { member, roles };
});
} }
export async function mapDiscordtoID(id: number): Promise<number | null> { export async function mapDiscordtoID(id: number): Promise<number | null> {

View File

@@ -34,6 +34,11 @@ export interface MemberLight {
color: string color: string
} }
export interface MemberCardDetails {
member: Member;
roles: Role[];
}
export interface myData { export interface myData {
member: Member; member: Member;
LOAs: LOARequest[]; LOAs: LOARequest[];

View File

@@ -1,4 +1,4 @@
import { memberSettings, Member, MemberLight } from "@shared/types/member"; import { memberSettings, Member, MemberLight, MemberCardDetails } from "@shared/types/member";
// @ts-ignore // @ts-ignore
const addr = import.meta.env.VITE_APIHOST; const addr = import.meta.env.VITE_APIHOST;
@@ -71,7 +71,7 @@ export async function getLightMembers(ids: number[]): Promise<MemberLight[]> {
return response.json(); return response.json();
} }
export async function getFullMembers(ids: number[]): Promise<Member[]> { export async function getFullMembers(ids: number[]): Promise<MemberCardDetails[]> {
if (ids.length === 0) return []; if (ids.length === 0) return [];

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useMemberDirectory } from '@/stores/memberDirectory'; import { useMemberDirectory } from '@/stores/memberDirectory';
import { ref, onMounted, computed } from 'vue'; import { ref, onMounted, computed } from 'vue';
import { Member, type MemberLight } from '@shared/types/member' import { Member, MemberCardDetails, type MemberLight } from '@shared/types/member'
import Popover from '../ui/popover/Popover.vue'; import Popover from '../ui/popover/Popover.vue';
import PopoverTrigger from '../ui/popover/PopoverTrigger.vue'; import PopoverTrigger from '../ui/popover/PopoverTrigger.vue';
import PopoverContent from '../ui/popover/PopoverContent.vue'; import PopoverContent from '../ui/popover/PopoverContent.vue';
@@ -21,7 +21,7 @@ const props = defineProps({
// Local state // Local state
const memberLight = ref<MemberLight | null>(null); const memberLight = ref<MemberLight | null>(null);
const memberFull = ref<Member | null>(null) const memberFull = ref<MemberCardDetails | null>(null)
const loadingFull = ref(false) const loadingFull = ref(false)
const membersStore = useMemberDirectory(); const membersStore = useMemberDirectory();
@@ -63,7 +63,7 @@ const hasFullInfo = computed(() => {
if (!memberFull.value) return false if (!memberFull.value) return false
// check if any field has a value // check if any field has a value
const { rank, unit, status } = memberFull.value const { rank, unit, status } = memberFull.value.member
return !!(rank || unit || status) return !!(rank || unit || status)
}) })
@@ -90,7 +90,7 @@ function formatDate(date: Date): string {
{{ displayName }} {{ displayName }}
</p> </p>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent class="w-72 p-0 overflow-hidden"> <PopoverContent class="w-80 p-0 overflow-hidden">
<!-- Loading --> <!-- Loading -->
<div v-if="loadingFull" class="p-4 text-sm text-muted-foreground mx-auto flex justify-center my-5"> <div v-if="loadingFull" class="p-4 text-sm text-muted-foreground mx-auto flex justify-center my-5">
<Spinner></Spinner> <Spinner></Spinner>
@@ -114,26 +114,33 @@ function formatDate(date: Date): string {
<div class="p-4 space-y-3 text-sm"> <div class="p-4 space-y-3 text-sm">
<!-- Full info --> <!-- Full info -->
<template v-if="hasFullInfo"> <template v-if="hasFullInfo">
<div v-if="memberFull.loa_until" <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"> 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.loa_until) }} On Leave of Absence until {{ formatDate(memberFull.member.loa_until) }}
</div> </div>
<div v-if="memberFull.rank" class="flex justify-between">
<div v-if="memberFull.member.rank" class="flex justify-between">
<span class="text-muted-foreground">Rank</span> <span class="text-muted-foreground">Rank</span>
<span class="font-medium">{{ memberFull.rank }}</span> <span class="font-medium">{{ memberFull.member.rank }}</span>
</div> </div>
<div v-if="memberFull.unit" class="flex justify-between"> <div v-if="memberFull.member.unit" class="flex justify-between">
<span class="text-muted-foreground">Unit</span> <span class="text-muted-foreground">Unit</span>
<span class="font-medium">{{ memberFull.unit }}</span> <span class="font-medium">{{ memberFull.member.unit }}</span>
</div> </div>
<div v-if="memberFull.status" class="flex justify-between"> <div v-if="memberFull.member.status" class="flex justify-between">
<span class="text-muted-foreground">Status</span> <span class="text-muted-foreground">Status</span>
<span class="font-medium">{{ memberFull.status }}</span> <span class="font-medium">{{ memberFull.member.status }}</span>
</div> </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> </template>
<!-- No info fallback --> <!-- No info fallback -->

View File

@@ -1,5 +1,5 @@
import { defineStore } from "pinia" import { defineStore } from "pinia"
import type { MemberLight, Member } from "@shared/types/member" import type { MemberLight, Member, MemberCardDetails } from "@shared/types/member"
import { getLightMembers, getFullMembers } from "@/api/member" import { getLightMembers, getFullMembers } from "@/api/member"
import { reactive, ref } from "vue" import { reactive, ref } from "vue"
import { resolve } from "path" import { resolve } from "path"
@@ -7,7 +7,7 @@ import { rejects } from "assert"
export const useMemberDirectory = defineStore('memberDirectory', () => { export const useMemberDirectory = defineStore('memberDirectory', () => {
const light = reactive<Record<number, MemberLight>>({}); const light = reactive<Record<number, MemberLight>>({});
const full = reactive<Record<number, Member>>({}) const full = reactive<Record<number, MemberCardDetails>>({})
function getLight(id: number): Promise<MemberLight> { function getLight(id: number): Promise<MemberLight> {
if (light[id]) return Promise.resolve(light[id]); if (light[id]) return Promise.resolve(light[id]);
@@ -24,7 +24,7 @@ export const useMemberDirectory = defineStore('memberDirectory', () => {
}) })
} }
function getFull(id: number): Promise<Member> { function getFull(id: number): Promise<MemberCardDetails> {
if (full[id]) return Promise.resolve(full[id]) if (full[id]) return Promise.resolve(full[id])
if (!fullWaiters.has(id)) { if (!fullWaiters.has(id)) {
@@ -34,7 +34,7 @@ export const useMemberDirectory = defineStore('memberDirectory', () => {
scheduleBatch() scheduleBatch()
return new Promise<Member>((resolve, reject) => { return new Promise<MemberCardDetails>((resolve, reject) => {
fullWaiters.get(id)!.push({ resolve, reject }) fullWaiters.get(id)!.push({ resolve, reject })
}) })
} }
@@ -50,7 +50,7 @@ export const useMemberDirectory = defineStore('memberDirectory', () => {
// promises // promises
const lightWaiters = new Map<number, Array<{ resolve: (m: MemberLight) => void; reject: (e: any) => void }>>() const lightWaiters = new Map<number, Array<{ resolve: (m: MemberLight) => void; reject: (e: any) => void }>>()
const fullWaiters = new Map<number, Array<{ resolve: (m: Member) => void; reject: (e: any) => void }>>() const fullWaiters = new Map<number, Array<{ resolve: (m: MemberCardDetails) => void; reject: (e: any) => void }>>()
let batchTimer: ReturnType<typeof setTimeout> | null = null; let batchTimer: ReturnType<typeof setTimeout> | null = null;
@@ -105,12 +105,13 @@ export const useMemberDirectory = defineStore('memberDirectory', () => {
try { try {
const res = await getFullMembers(ids); const res = await getFullMembers(ids);
for (const m of res) { for (const m of res) {
full[m.member_id] = m; console.log(m)
full[m.member.member_id] = m;
const waiters = fullWaiters.get(m.member_id); const waiters = fullWaiters.get(m.member.member_id);
if (waiters) { if (waiters) {
for (const w of waiters) w.resolve(m) for (const w of waiters) w.resolve(m)
fullWaiters.delete(m.member_id); fullWaiters.delete(m.member.member_id);
} }
} }