From adc9da6a40cfeb66203145b0868a111c0ef035fb Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Mon, 2 Mar 2026 20:30:01 -0500 Subject: [PATCH] added support for integrated rank changes --- api/src/routes/units.ts | 6 +- api/src/services/db/rankService.ts | 10 +++ ui/src/api/units.ts | 4 +- ui/src/components/members/TransferMember.vue | 64 ++++++++++++++++++-- ui/src/pages/memberList.vue | 2 +- 5 files changed, 78 insertions(+), 8 deletions(-) diff --git a/api/src/routes/units.ts b/api/src/routes/units.ts index 081e32b..1478100 100644 --- a/api/src/routes/units.ts +++ b/api/src/routes/units.ts @@ -11,6 +11,7 @@ import { Unit } from '@app/shared/types/units'; import { MemberState } from '@app/shared/types/member'; import { assignNewUnit } from '../services/db/unitService'; import { audit } from '../services/logging/auditLog'; +import { forceInsertMemberRank, insertMemberRank } from '../services/db/rankService'; unitsRouter.use(requireLogin); @@ -35,6 +36,7 @@ unitsRouter.get('/', async (req, res) => { memberUnitsRouter.post('/admin', [requireMemberState(MemberState.Member), requireRole("17th Administrator")], async (req: Request, res: Response) => { const memberId = Number(req.query.memberId); const unitId = Number(req.query.unitId); + const rankId = Number(req.query.rankId); const reason = req.query.reason as string; try { @@ -44,12 +46,14 @@ memberUnitsRouter.post('/admin', [requireMemberState(MemberState.Member), requir } await assignNewUnit(memberId, unitId, req.user.id, req.user.id, reason); + await forceInsertMemberRank(memberId, rankId, req.user.id, req.user.id, reason); logger.info('app', 'Member force assigned unit', { member: memberId, unit: unitId, + rank: rankId, caller: req.user.id, }); - audit.member('update_unit', { actorId: req.user.id, targetId: memberId }, { unit: unitId, reason: reason }); + audit.member('update_unit', { actorId: req.user.id, targetId: memberId }, { unit: unitId, rank: rankId, reason: reason }); res.sendStatus(200); } catch (error) { diff --git a/api/src/services/db/rankService.ts b/api/src/services/db/rankService.ts index 427a088..b4195c9 100644 --- a/api/src/services/db/rankService.ts +++ b/api/src/services/db/rankService.ts @@ -36,6 +36,16 @@ export async function insertMemberRank(member_id: number, rank_id: number, date? await pool.query(sql, params); } +export async function forceInsertMemberRank(member_id: number, rank_id: number, authorized: number, creator: number, reason: string) { + const sql = `CALL sp_update_member_rank(?, ?, ?, ?, ?, NOW())`; + + const result = await pool.query(sql, [member_id, rank_id, authorized, creator, reason]); + + if (!result || result.affectedRows === 0) { + throw new Error("Failed to update member rank"); + } +} + export async function batchInsertMemberRank(promos: BatchPromotionMember[], author: number, approver: number) { try { diff --git a/ui/src/api/units.ts b/ui/src/api/units.ts index 251ee4f..c2de225 100644 --- a/ui/src/api/units.ts +++ b/ui/src/api/units.ts @@ -14,8 +14,8 @@ export async function getUnits(): Promise { return response.json(); } -export async function adminAssignUnit(member: number, unit: number, reason: string) { - const response = await fetch(`${addr}/memberUnits/admin?memberId=${member}&unitId=${unit}&reason=${encodeURIComponent(reason)}`, { +export async function adminAssignUnit(member: number, unit: number, rank: number, reason: string) { + const response = await fetch(`${addr}/memberUnits/admin?memberId=${member}&unitId=${unit}&rankId=${rank}&reason=${encodeURIComponent(reason)}`, { method: 'POST', credentials: 'include' }); diff --git a/ui/src/components/members/TransferMember.vue b/ui/src/components/members/TransferMember.vue index f67cabe..8da6f6c 100644 --- a/ui/src/components/members/TransferMember.vue +++ b/ui/src/components/members/TransferMember.vue @@ -2,6 +2,7 @@ import { computed, ref, watch } from 'vue' import { adminAssignUnit, getUnits } from '@/api/units' +import { getAllRanks } from '@/api/rank' import { Button } from '@/components/ui/button' import { Dialog, @@ -22,6 +23,7 @@ import { } from '@/components/ui/select' import MemberCard from './MemberCard.vue' import type { Member } from '@shared/types/member' +import type { Rank } from '@shared/types/rank' import type { Unit } from '@shared/types/units' const props = defineProps<{ @@ -31,21 +33,25 @@ const props = defineProps<{ const emit = defineEmits<{ 'update:open': [value: boolean] - transferred: [value: { memberId: number; unitId: number; reason: string }] + transferred: [value: { memberId: number; unitId: number; rankId: number; reason: string }] }>() const units = ref([]) +const ranks = ref([]) const loadingUnits = ref(false) +const loadingRanks = ref(false) const submitting = ref(false) const formError = ref('') const selectedUnitId = ref('') +const selectedRankId = ref('') const selectedReason = ref('transfer_request') const customReason = ref('') const reasonOptions = [ { label: 'Transfer Request', value: 'transfer_request' }, { label: 'Leadership Vote', value: 'leadership_vote' }, + { label: 'Appointment', value: 'appointment' }, { label: 'Step Down', value: 'step_down' }, { label: 'Custom', value: 'custom' }, ] @@ -58,11 +64,26 @@ const resolvedReason = computed(() => { }) const canSubmit = computed(() => { - return !!props.member && !!selectedUnitId.value && !!resolvedReason.value + return !!props.member && !!selectedUnitId.value && !!selectedRankId.value && !!resolvedReason.value }) +function resolveDefaultRankId(member: Member | null): string { + if (!member || !member.rank) { + return '' + } + + const normalizedMemberRank = member.rank.trim().toLowerCase() + const matchedRank = ranks.value.find((rank) => { + return rank.name.trim().toLowerCase() === normalizedMemberRank + || rank.short_name.trim().toLowerCase() === normalizedMemberRank + }) + + return matchedRank ? String(matchedRank.id) : '' +} + function resetForm() { selectedUnitId.value = '' + selectedRankId.value = '' selectedReason.value = 'transfer_request' customReason.value = '' formError.value = '' @@ -80,12 +101,26 @@ async function loadUnits() { } } +async function loadRanks() { + loadingRanks.value = true + formError.value = '' + try { + ranks.value = await getAllRanks() + selectedRankId.value = resolveDefaultRankId(props.member) + } catch { + formError.value = 'Failed to load ranks. Please try again.' + } finally { + loadingRanks.value = false + } +} + watch( () => props.open, (isOpen) => { if (isOpen) { resetForm() loadUnits() + loadRanks() } }, ) @@ -100,6 +135,11 @@ async function onSubmit() { return } + if (!selectedRankId.value) { + formError.value = 'Please select a target rank.' + return + } + if (!resolvedReason.value) { formError.value = 'Please select a reason or enter a custom reason.' return @@ -109,11 +149,13 @@ async function onSubmit() { formError.value = '' try { const unitId = Number(selectedUnitId.value) - await adminAssignUnit(props.member.member_id, unitId, resolvedReason.value) + const rankId = Number(selectedRankId.value) + await adminAssignUnit(props.member.member_id, unitId, rankId, resolvedReason.value) emit('transferred', { memberId: props.member.member_id, unitId, + rankId, reason: resolvedReason.value, }) emit('update:open', false) @@ -151,6 +193,20 @@ async function onSubmit() { + + Target Rank + + + Reason