From 82eb6b7bbfca301bfb6ca08c1edb62283c416f4c Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Sat, 13 Dec 2025 01:21:07 -0500 Subject: [PATCH] Added displayname and member card system --- api/src/routes/applications.ts | 3 +- api/src/routes/{members.js => members.ts} | 60 +++++++-- api/src/services/memberService.ts | 57 ++++++-- shared/types/member.ts | 31 +++++ ui/src/api/member.ts | 74 +++++++++-- ui/src/components/members/MemberCard.vue | 153 ++++++++++++++++++++++ ui/src/components/ui/spinner/Spinner.vue | 16 +++ ui/src/components/ui/spinner/index.js | 1 + ui/src/pages/MyProfile.vue | 98 +++++++++++++- ui/src/pages/TrainingReport.vue | 36 +++-- ui/src/stores/memberDirectory.ts | 140 ++++++++++++++++++++ 11 files changed, 624 insertions(+), 45 deletions(-) rename api/src/routes/{members.js => members.ts} (62%) create mode 100644 shared/types/member.ts create mode 100644 ui/src/components/members/MemberCard.vue create mode 100644 ui/src/components/ui/spinner/Spinner.vue create mode 100644 ui/src/components/ui/spinner/index.js create mode 100644 ui/src/stores/memberDirectory.ts diff --git a/api/src/routes/applications.ts b/api/src/routes/applications.ts index e1c9394..239c3dd 100644 --- a/api/src/routes/applications.ts +++ b/api/src/routes/applications.ts @@ -3,7 +3,8 @@ const router = express.Router(); import pool from '../db'; import { approveApplication, createApplication, denyApplication, getAllMemberApplications, getApplicationByID, getApplicationComments, getApplicationList, getMemberApplication } from '../services/applicationService'; -import { MemberState, setUserState } from '../services/memberService'; +import { setUserState } from '../services/memberService'; +import { MemberState } from '@app/shared/types/member'; import { getRankByName, insertMemberRank } from '../services/rankService'; import { ApplicationFull, CommentRow } from "@app/shared/types/application" import { assignUserToStatus } from '../services/statusService'; diff --git a/api/src/routes/members.js b/api/src/routes/members.ts similarity index 62% rename from api/src/routes/members.js rename to api/src/routes/members.ts index 08c169e..513351c 100644 --- a/api/src/routes/members.js +++ b/api/src/routes/members.ts @@ -1,10 +1,12 @@ const express = require('express'); const router = express.Router(); +import { Request, Response } from 'express'; import pool from '../db'; import { getUserActiveLOA } from '../services/loaService'; -import { getUserData } from '../services/memberService'; +import { getMemberSettings, getMembersFull, getMembersLite, getUserData, setUserSettings } from '../services/memberService'; import { getUserRoles } from '../services/rolesService'; +import { memberSettings } from '@app/shared/types/member'; router.use((req, res, next) => { console.log(req.user); @@ -60,6 +62,53 @@ router.get('/me', async (req, res) => { } }) +router.get('/settings', async (req: Request, res: Response) => { + try { + let user = req.user.id; + console.log(user); + let output = await getMemberSettings(user); + res.status(200).json(output); + } catch (error) { + console.error(error); + res.status(500).json(error); + } +}) + +router.put('/settings', async (req: Request, res: Response) => { + try { + let user = req.user.id; + let settings: memberSettings = req.body; + console.log(settings) + await setUserSettings(user, settings); + res.sendStatus(200); + } catch (error) { + console.error(error); + res.status(500).json(error); + } +}) + +router.post('/lite/bulk', async (req: Request, res: Response) => { + try { + let ids = req.body.ids; + let out = await getMembersLite(ids); + res.status(200).json(out); + } catch (error) { + console.error(error); + res.status(500).json(error); + } +}) + +router.post('/full/bulk', async (req: Request, res: Response) => { + try { + let ids = req.body.ids; + let out = await getMembersFull(ids); + res.status(200).json(out); + } catch (error) { + console.error(error); + res.status(500).json(error); + } +}) + router.get('/:id', async (req, res) => { try { const userId = req.params.id; @@ -74,13 +123,4 @@ router.get('/:id', async (req, res) => { } }); -//update a user's display name (stub) -router.put('/:id/displayname', async (req, res) => { - // Stub: not implemented yet - return res.status(501).json({ error: 'Update display name not implemented' }); -}); - - - - module.exports = router; diff --git a/api/src/services/memberService.ts b/api/src/services/memberService.ts index 4563a6a..cc57ebc 100644 --- a/api/src/services/memberService.ts +++ b/api/src/services/memberService.ts @@ -1,25 +1,17 @@ import pool from "../db"; - -export enum MemberState { - Guest = "guest", - Applicant = "applicant", - Member = "member", - Retired = "retired", - Banned = "banned", - Denied = "denied" -} +import { Member, MemberLight, memberSettings, MemberState } from '@app/shared/types/member' export async function getUserData(userID: number) { - const sql = `SELECT * FROM members WHERE id = ?`; - const res = await pool.query(sql, [userID]); - return res[0] ?? null; + const sql = `SELECT * FROM members WHERE id = ?`; + const res = await pool.query(sql, [userID]); + return res[0] ?? null; } export async function setUserState(userID: number, state: MemberState) { - const sql = `UPDATE members + const sql = `UPDATE members SET state = ? WHERE id = ?;`; - return await pool.query(sql, [state, userID]); + return await pool.query(sql, [state, userID]); } declare global { @@ -32,3 +24,40 @@ declare global { } } } + + +export async function getMemberSettings(id: number): Promise { + const sql = `SELECT * FROM view_member_settings WHERE id = ?`; + let out: memberSettings[] = await pool.query(sql, [id]); + + if (out.length != 1) + throw new Error("Could not get user settings"); + + return out[0]; +} + +export async function setUserSettings(id: number, settings: memberSettings) { + const sql = `UPDATE view_member_settings SET + displayName = ? + WHERE id = ?;`; + let result = await pool.query(sql, [settings.displayName, id]) + console.log(result); +} + +export async function getMembersLite(ids: number[]): Promise { + const sql = `SELECT m.member_id AS id, + m.member_name AS username, + m.displayName, + u.color + FROM view_member_rank_unit_status_latest m + LEFT JOIN units u ON u.name = m.unit + WHERE member_id IN (?);`; + const res: MemberLight[] = await pool.query(sql, [ids]); + return res; +} + +export async function getMembersFull(ids: number[]): Promise { + const sql = `SELECT * FROM view_member_rank_unit_status_latest WHERE member_id IN (?);`; + const res: Member[] = await pool.query(sql, [ids]); + return res; +} \ No newline at end of file diff --git a/shared/types/member.ts b/shared/types/member.ts new file mode 100644 index 0000000..20e0469 --- /dev/null +++ b/shared/types/member.ts @@ -0,0 +1,31 @@ +export interface memberSettings { + displayName: string; +} + +export enum MemberState { + Guest = "guest", + Applicant = "applicant", + Member = "member", + Retired = "retired", + Banned = "banned", + Denied = "denied" +} + +export type Member = { + member_id: number; + member_name: string; + rank: string | null; + rank_date: string | null; + unit: string | null; + unit_date: string | null; + status: string | null; + status_date: string | null; + loa_until?: Date; +}; + +export interface MemberLight { + id: number + displayName: string + username: string + color: string +} \ No newline at end of file diff --git a/ui/src/api/member.ts b/ui/src/api/member.ts index 4b55d8d..9fe2935 100644 --- a/ui/src/api/member.ts +++ b/ui/src/api/member.ts @@ -1,14 +1,4 @@ -export type Member = { - member_id: number; - member_name: string; - rank: string | null; - rank_date: string | null; - unit: string | null; - unit_date: string | null; - status: string | null; - status_date: string | null; - on_loa: boolean | null; -}; +import { memberSettings, Member, MemberLight } from "@shared/types/member"; // @ts-ignore const addr = import.meta.env.VITE_APIHOST; @@ -21,4 +11,66 @@ export async function getMembers(): Promise { throw new Error("Failed to fetch members"); } return response.json(); +} + +export async function getMemberSettings(): Promise { + const response = await fetch(`${addr}/members/settings`, { + credentials: 'include' + }); + if (!response.ok) { + throw new Error("Failed to fetch settings"); + } + return response.json(); +} + +export async function setMemberSettings(settings: memberSettings) { + const response = await fetch(`${addr}/members/settings`, { + credentials: 'include', + method: 'PUT', + headers: { + 'Content-Type': 'Application/json', + }, + body: JSON.stringify(settings) + }); + if (!response.ok) { + throw new Error("Failed to fetch settings"); + } + return; +} + +export async function getLightMembers(ids: number[]): Promise { + + if (ids.length === 0) return []; + + const response = await fetch(`${addr}/members/lite/bulk`, { + credentials: 'include', + method: 'POST', + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ ids }) + }); + + if (!response.ok) { + throw new Error("Failed to fetch light members"); + } + return response.json(); +} + +export async function getFullMembers(ids: number[]): Promise { + + if (ids.length === 0) return []; + + const response = await fetch(`${addr}/members/full/bulk`, { + credentials: 'include', + method: 'POST', + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ ids }) + }); + if (!response.ok) { + throw new Error("Failed to fetch settings"); + } + return response.json(); } \ No newline at end of file diff --git a/ui/src/components/members/MemberCard.vue b/ui/src/components/members/MemberCard.vue new file mode 100644 index 0000000..0ba3352 --- /dev/null +++ b/ui/src/components/members/MemberCard.vue @@ -0,0 +1,153 @@ + + + diff --git a/ui/src/components/ui/spinner/Spinner.vue b/ui/src/components/ui/spinner/Spinner.vue new file mode 100644 index 0000000..077c490 --- /dev/null +++ b/ui/src/components/ui/spinner/Spinner.vue @@ -0,0 +1,16 @@ + + + diff --git a/ui/src/components/ui/spinner/index.js b/ui/src/components/ui/spinner/index.js new file mode 100644 index 0000000..8a95e7a --- /dev/null +++ b/ui/src/components/ui/spinner/index.js @@ -0,0 +1 @@ +export { default as Spinner } from "./Spinner.vue"; diff --git a/ui/src/pages/MyProfile.vue b/ui/src/pages/MyProfile.vue index e2ea846..48dfd61 100644 --- a/ui/src/pages/MyProfile.vue +++ b/ui/src/pages/MyProfile.vue @@ -1 +1,97 @@ - \ No newline at end of file + + + + + + \ No newline at end of file diff --git a/ui/src/pages/TrainingReport.vue b/ui/src/pages/TrainingReport.vue index 46ad7ea..1985cf8 100644 --- a/ui/src/pages/TrainingReport.vue +++ b/ui/src/pages/TrainingReport.vue @@ -21,6 +21,7 @@ import SelectValue from '@/components/ui/select/SelectValue.vue'; import SelectContent from '@/components/ui/select/SelectContent.vue'; import SelectItem from '@/components/ui/select/SelectItem.vue'; import Input from '@/components/ui/input/Input.vue'; +import MemberCard from '@/components/members/MemberCard.vue'; enum sidePanelState { view, create, closed }; @@ -152,9 +153,13 @@ onMounted(async () => { {{ report.course_name.length > 30 ? report.course_shortname : report.course_name }} {{ report.date.split('T')[0] }} - {{ report.created_by_name === null ? "Unknown User" : + + + Unknown User + + @@ -172,11 +177,14 @@ onMounted(async () => {

{{ focusedTrainingReport.course_name }}

-
+

{{ focusedTrainingReport.event_date.split('T')[0] }}

-

Created by {{ focusedTrainingReport.created_by_name === null ? "Unknown User" : +

Created by: + +

{{ focusedTrainingReport.created_by_name === null ? "Unknown User" : focusedTrainingReport.created_by_name - }} + }}

@@ -191,7 +199,11 @@ onMounted(async () => {
-

{{ person.attendee_name }}

+
+ +

{{ person.attendee_name }}

+

{{ person.role.name }}

@@ -213,7 +225,11 @@ onMounted(async () => {

-

{{ person.attendee_name }}

+
+ +

{{ person.attendee_name }}

+
@@ -242,7 +258,11 @@ onMounted(async () => {
-

{{ person.attendee_name }}

+
+ +

{{ person.attendee_name }}

+