diff --git a/api/src/routes/members.ts b/api/src/routes/members.ts index 3e574aa..026424d 100644 --- a/api/src/routes/members.ts +++ b/api/src/routes/members.ts @@ -5,7 +5,7 @@ import { Request, Response } from 'express'; import pool from '../db'; import { requireLogin, requireMemberState, requireRole } from '../middleware/auth'; import { getUserActiveLOA } from '../services/loaService'; -import { getMemberSettings, getMembersFull, getMembersLite, getUserData, setUserSettings } from '../services/memberService'; +import { getAllMembersLite, getMemberSettings, getMembersFull, getMembersLite, getUserData, setUserSettings } from '../services/memberService'; import { getUserRoles } from '../services/rolesService'; import { memberSettings, MemberState } from '@app/shared/types/member'; @@ -75,6 +75,16 @@ router.put('/settings', [requireLogin], async (req: Request, res: Response) => { } }) +router.get('/lite', [requireLogin], async (req: Request, res: Response) => { + try { + let out = await getAllMembersLite(); + res.status(200).json(out); + } 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; @@ -86,6 +96,7 @@ router.post('/lite/bulk', async (req: Request, res: Response) => { } }) + router.post('/full/bulk', async (req: Request, res: Response) => { try { let ids = req.body.ids; diff --git a/api/src/services/memberService.ts b/api/src/services/memberService.ts index 6381ad1..b639b21 100644 --- a/api/src/services/memberService.ts +++ b/api/src/services/memberService.ts @@ -49,6 +49,18 @@ export async function getMembersLite(ids: number[]): Promise { return res; } +export async function getAllMembersLite(): 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;`; + + const res: MemberLight[] = await pool.query(sql); + 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]); diff --git a/ui/src/api/member.ts b/ui/src/api/member.ts index 9fe2935..327a5ce 100644 --- a/ui/src/api/member.ts +++ b/ui/src/api/member.ts @@ -38,6 +38,20 @@ export async function setMemberSettings(settings: memberSettings) { return; } +export async function getAllLightMembers(): Promise { + const response = await fetch(`${addr}/members/lite`, { + credentials: 'include', + headers: { + "Content-Type": "application/json", + } + }); + + if (!response.ok) { + throw new Error("Failed to fetch light members"); + } + return response.json(); +} + export async function getLightMembers(ids: number[]): Promise { if (ids.length === 0) return []; diff --git a/ui/src/components/trainingReport/trainingReportForm.vue b/ui/src/components/trainingReport/trainingReportForm.vue index 26ef3c1..f68f49c 100644 --- a/ui/src/components/trainingReport/trainingReportForm.vue +++ b/ui/src/components/trainingReport/trainingReportForm.vue @@ -3,9 +3,10 @@ import { trainingReportSchema, courseEventAttendeeSchema } from '@shared/schemas import { Course, CourseAttendee, CourseAttendeeRole, CourseEventDetails } from '@shared/types/course' import { useForm, useFieldArray, FieldArray as VeeFieldArray, ErrorMessage, Field as VeeField } from 'vee-validate' import { toTypedSchema } from '@vee-validate/zod' -import { computed, onMounted, ref, watch } from 'vue' +import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue' import { getAllAttendeeRoles, getAllTrainings, postTrainingReport } from '@/api/trainingReport' -import { getMembers, Member } from '@/api/member' +import { getAllLightMembers, getLightMembers, getMembers } from '@/api/member' +import { Member, MemberLight } from '@shared/types/member' import FieldGroup from '../ui/field/FieldGroup.vue' import Field from '../ui/field/Field.vue' @@ -13,12 +14,17 @@ import FieldLabel from '../ui/field/FieldLabel.vue' import FieldError from '../ui/field/FieldError.vue' import Button from '../ui/button/Button.vue' import Textarea from '../ui/textarea/Textarea.vue' -import { Plus, X } from 'lucide-vue-next'; +import { Check, Plus, X } from 'lucide-vue-next'; import FieldSet from '../ui/field/FieldSet.vue' import FieldLegend from '../ui/field/FieldLegend.vue' import FieldDescription from '../ui/field/FieldDescription.vue' import Checkbox from '../ui/checkbox/Checkbox.vue' import { configure } from 'vee-validate' +import { ComboboxAnchor, ComboboxEmpty, ComboboxGroup, ComboboxInput, ComboboxItem, ComboboxItemIndicator, ComboboxList } from "@/components/ui/combobox" +import Popover from "@/components/ui/popover/Popover.vue"; +import PopoverTrigger from "@/components/ui/popover/PopoverTrigger.vue"; +import PopoverContent from "@/components/ui/popover/PopoverContent.vue"; +import Combobox from '../ui/combobox/Combobox.vue' const { handleSubmit, resetForm, errors, values, setFieldValue } = useForm({ @@ -79,16 +85,44 @@ const { remove, push, fields } = useFieldArray('attendees'); const selectedCourse = computed(() => { return trainings.value?.find(c => c.id == values.course_id) }) const trainings = ref(null); -const members = ref(null); +const members = ref(null); const eventRoles = ref(null); const emit = defineEmits(['submit']) onMounted(async () => { trainings.value = await getAllTrainings(); - members.value = await getMembers(); + members.value = await getAllLightMembers(); eventRoles.value = await getAllAttendeeRoles(); }) + +const selectCourse = ref(false); +const openMap = reactive>({}) + +const memberMap = computed(() => + Object.fromEntries( + members.value?.map(m => [m.id, m.displayName || m.username]) ?? [] + ) +) + +const memberSearch = ref('') + +const MAX_RESULTS = 50 + +const filteredMembers = computed(() => { + const q = memberSearch?.value?.toLowerCase() ?? "" + const results: MemberLight[] = [] + + for (const m of members.value ?? []) { + if (!q || (m.displayName || m.username).toLowerCase().includes(q)) { + results.push(m) + if (results.length >= MAX_RESULTS) break + } + } + + return results +}) +