added support for integrated rank changes
All checks were successful
Pull Request CI / Merge Check (pull_request) Successful in 3m27s
All checks were successful
Pull Request CI / Merge Check (pull_request) Successful in 3m27s
This commit is contained in:
@@ -11,6 +11,7 @@ import { Unit } from '@app/shared/types/units';
|
|||||||
import { MemberState } from '@app/shared/types/member';
|
import { MemberState } from '@app/shared/types/member';
|
||||||
import { assignNewUnit } from '../services/db/unitService';
|
import { assignNewUnit } from '../services/db/unitService';
|
||||||
import { audit } from '../services/logging/auditLog';
|
import { audit } from '../services/logging/auditLog';
|
||||||
|
import { forceInsertMemberRank, insertMemberRank } from '../services/db/rankService';
|
||||||
|
|
||||||
unitsRouter.use(requireLogin);
|
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) => {
|
memberUnitsRouter.post('/admin', [requireMemberState(MemberState.Member), requireRole("17th Administrator")], async (req: Request, res: Response) => {
|
||||||
const memberId = Number(req.query.memberId);
|
const memberId = Number(req.query.memberId);
|
||||||
const unitId = Number(req.query.unitId);
|
const unitId = Number(req.query.unitId);
|
||||||
|
const rankId = Number(req.query.rankId);
|
||||||
const reason = req.query.reason as string;
|
const reason = req.query.reason as string;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -44,12 +46,14 @@ memberUnitsRouter.post('/admin', [requireMemberState(MemberState.Member), requir
|
|||||||
}
|
}
|
||||||
|
|
||||||
await assignNewUnit(memberId, unitId, req.user.id, req.user.id, reason);
|
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', {
|
logger.info('app', 'Member force assigned unit', {
|
||||||
member: memberId,
|
member: memberId,
|
||||||
unit: unitId,
|
unit: unitId,
|
||||||
|
rank: rankId,
|
||||||
caller: req.user.id,
|
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);
|
res.sendStatus(200);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -36,6 +36,16 @@ export async function insertMemberRank(member_id: number, rank_id: number, date?
|
|||||||
await pool.query(sql, params);
|
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) {
|
export async function batchInsertMemberRank(promos: BatchPromotionMember[], author: number, approver: number) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ export async function getUnits(): Promise<Unit[]> {
|
|||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function adminAssignUnit(member: number, unit: number, reason: string) {
|
export async function adminAssignUnit(member: number, unit: number, rank: number, reason: string) {
|
||||||
const response = await fetch(`${addr}/memberUnits/admin?memberId=${member}&unitId=${unit}&reason=${encodeURIComponent(reason)}`, {
|
const response = await fetch(`${addr}/memberUnits/admin?memberId=${member}&unitId=${unit}&rankId=${rank}&reason=${encodeURIComponent(reason)}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
import { adminAssignUnit, getUnits } from '@/api/units'
|
import { adminAssignUnit, getUnits } from '@/api/units'
|
||||||
|
import { getAllRanks } from '@/api/rank'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
import MemberCard from './MemberCard.vue'
|
import MemberCard from './MemberCard.vue'
|
||||||
import type { Member } from '@shared/types/member'
|
import type { Member } from '@shared/types/member'
|
||||||
|
import type { Rank } from '@shared/types/rank'
|
||||||
import type { Unit } from '@shared/types/units'
|
import type { Unit } from '@shared/types/units'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -31,21 +33,25 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:open': [value: boolean]
|
'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<Unit[]>([])
|
const units = ref<Unit[]>([])
|
||||||
|
const ranks = ref<Rank[]>([])
|
||||||
const loadingUnits = ref(false)
|
const loadingUnits = ref(false)
|
||||||
|
const loadingRanks = ref(false)
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const formError = ref('')
|
const formError = ref('')
|
||||||
|
|
||||||
const selectedUnitId = ref('')
|
const selectedUnitId = ref('')
|
||||||
|
const selectedRankId = ref('')
|
||||||
const selectedReason = ref('transfer_request')
|
const selectedReason = ref('transfer_request')
|
||||||
const customReason = ref('')
|
const customReason = ref('')
|
||||||
|
|
||||||
const reasonOptions = [
|
const reasonOptions = [
|
||||||
{ label: 'Transfer Request', value: 'transfer_request' },
|
{ label: 'Transfer Request', value: 'transfer_request' },
|
||||||
{ label: 'Leadership Vote', value: 'leadership_vote' },
|
{ label: 'Leadership Vote', value: 'leadership_vote' },
|
||||||
|
{ label: 'Appointment', value: 'appointment' },
|
||||||
{ label: 'Step Down', value: 'step_down' },
|
{ label: 'Step Down', value: 'step_down' },
|
||||||
{ label: 'Custom', value: 'custom' },
|
{ label: 'Custom', value: 'custom' },
|
||||||
]
|
]
|
||||||
@@ -58,11 +64,26 @@ const resolvedReason = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const canSubmit = 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() {
|
function resetForm() {
|
||||||
selectedUnitId.value = ''
|
selectedUnitId.value = ''
|
||||||
|
selectedRankId.value = ''
|
||||||
selectedReason.value = 'transfer_request'
|
selectedReason.value = 'transfer_request'
|
||||||
customReason.value = ''
|
customReason.value = ''
|
||||||
formError.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(
|
watch(
|
||||||
() => props.open,
|
() => props.open,
|
||||||
(isOpen) => {
|
(isOpen) => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
resetForm()
|
resetForm()
|
||||||
loadUnits()
|
loadUnits()
|
||||||
|
loadRanks()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -100,6 +135,11 @@ async function onSubmit() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!selectedRankId.value) {
|
||||||
|
formError.value = 'Please select a target rank.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!resolvedReason.value) {
|
if (!resolvedReason.value) {
|
||||||
formError.value = 'Please select a reason or enter a custom reason.'
|
formError.value = 'Please select a reason or enter a custom reason.'
|
||||||
return
|
return
|
||||||
@@ -109,11 +149,13 @@ async function onSubmit() {
|
|||||||
formError.value = ''
|
formError.value = ''
|
||||||
try {
|
try {
|
||||||
const unitId = Number(selectedUnitId.value)
|
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', {
|
emit('transferred', {
|
||||||
memberId: props.member.member_id,
|
memberId: props.member.member_id,
|
||||||
unitId,
|
unitId,
|
||||||
|
rankId,
|
||||||
reason: resolvedReason.value,
|
reason: resolvedReason.value,
|
||||||
})
|
})
|
||||||
emit('update:open', false)
|
emit('update:open', false)
|
||||||
@@ -151,6 +193,20 @@ async function onSubmit() {
|
|||||||
</Select>
|
</Select>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<FieldLabel>Target Rank</FieldLabel>
|
||||||
|
<Select v-model="selectedRankId" :disabled="loadingRanks || submitting">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select rank" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem v-for="rank in ranks" :key="rank.id" :value="String(rank.id)">
|
||||||
|
{{ rank.name }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</Field>
|
||||||
|
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel>Reason</FieldLabel>
|
<FieldLabel>Reason</FieldLabel>
|
||||||
<Select v-model="selectedReason" :disabled="submitting">
|
<Select v-model="selectedReason" :disabled="submitting">
|
||||||
@@ -185,7 +241,7 @@ async function onSubmit() {
|
|||||||
<Button variant="ghost" @click="emit('update:open', false)">
|
<Button variant="ghost" @click="emit('update:open', false)">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" form="transferForm" :disabled="!canSubmit || loadingUnits || submitting">
|
<Button type="submit" form="transferForm" :disabled="!canSubmit || loadingUnits || loadingRanks || submitting">
|
||||||
{{ submitting ? 'Transferring...' : 'Transfer Member' }}
|
{{ submitting ? 'Transferring...' : 'Transfer Member' }}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -174,7 +174,7 @@
|
|||||||
memberCache.invalidateMember(value.data.userID);
|
memberCache.invalidateMember(value.data.userID);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTransferSuccess(value: { memberId: number; unitId: number; reason: string }) {
|
function handleTransferSuccess(value: { memberId: number; unitId: number; rankId: number; reason: string }) {
|
||||||
fetchMembers();
|
fetchMembers();
|
||||||
memberCache.invalidateMember(value.memberId);
|
memberCache.invalidateMember(value.memberId);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user