Implemented admin assign unit UI #201
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -14,8 +14,8 @@ export async function getUnits(): Promise<Unit[]> {
|
||||
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'
|
||||
});
|
||||
|
||||
@@ -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<Unit[]>([])
|
||||
const ranks = ref<Rank[]>([])
|
||||
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() {
|
||||
</Select>
|
||||
</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>
|
||||
<FieldLabel>Reason</FieldLabel>
|
||||
<Select v-model="selectedReason" :disabled="submitting">
|
||||
@@ -185,7 +241,7 @@ async function onSubmit() {
|
||||
<Button variant="ghost" @click="emit('update:open', false)">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" form="transferForm" :disabled="!canSubmit || loadingUnits || submitting">
|
||||
<Button type="submit" form="transferForm" :disabled="!canSubmit || loadingUnits || loadingRanks || submitting">
|
||||
{{ submitting ? 'Transferring...' : 'Transfer Member' }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -174,7 +174,7 @@
|
||||
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();
|
||||
memberCache.invalidateMember(value.memberId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user