Merge pull request '100-recruitment-ui-fixes' (#101) from 100-recruitment-ui-fixes into main
All checks were successful
Continuous Integration / Update Development (push) Successful in 2m31s

Reviewed-on: #101
This commit was merged in pull request #101.
This commit is contained in:
2025-12-15 12:34:30 -06:00
6 changed files with 50 additions and 58 deletions

View File

@@ -155,21 +155,13 @@ router.post('/approve/:id', [requireLogin, requireRole("Recruiter")], async (req
try { try {
const app = await getApplicationByID(appID); const app = await getApplicationByID(appID);
const result = await approveApplication(appID); await approveApplication(appID, approved_by);
//guard against failures
if (result.affectedRows != 1) {
throw new Error("Something went wrong approving the application");
}
//update user profile //update user profile
await setUserState(app.member_id, MemberState.Member); await setUserState(app.member_id, MemberState.Member);
await pool.query('CALL sp_accept_new_recruit_validation(?, ?, ?, ?)', [Number(process.env.CONFIG_ID), app.member_id, approved_by, approved_by]) await pool.query('CALL sp_accept_new_recruit_validation(?, ?, ?, ?)', [Number(process.env.CONFIG_ID), app.member_id, approved_by, approved_by])
// let nextRank = await getRankByName('Recruit')
// await insertMemberRank(app.member_id, nextRank.id);
// //assign user to "pending basic"
// await assignUserToStatus(app.member_id, 1);
res.sendStatus(200); res.sendStatus(200);
} catch (err) { } catch (err) {
console.error('Approve failed:', err); console.error('Approve failed:', err);
@@ -178,12 +170,13 @@ router.post('/approve/:id', [requireLogin, requireRole("Recruiter")], async (req
}); });
// POST /application/deny/:id // POST /application/deny/:id
router.post('/deny/:id', [requireLogin, requireRole("Recruiter")], async (req, res) => { router.post('/deny/:id', [requireLogin, requireRole("Recruiter")], async (req: Request, res: Response) => {
const appID = req.params.id; const appID = Number(req.params.id);
const approver = Number(req.user.id);
try { try {
const app = await getApplicationByID(appID); const app = await getApplicationByID(appID);
await denyApplication(appID); await denyApplication(appID, approver);
await setUserState(app.member_id, MemberState.Denied); await setUserState(app.member_id, MemberState.Denied);
res.sendStatus(200); res.sendStatus(200);
} catch (err) { } catch (err) {

View File

@@ -31,7 +31,7 @@ export async function getApplicationByID(appID: number): Promise<ApplicationRow>
return app[0]; return app[0];
} }
export async function getApplicationList(): Promise<ApplicationListRow[]> { export async function getApplicationList(page: number = 1, pageSize: number = 25): Promise<ApplicationListRow[]> {
const sql = `SELECT const sql = `SELECT
member.name AS member_name, member.name AS member_name,
app.id, app.id,
@@ -40,9 +40,11 @@ export async function getApplicationList(): Promise<ApplicationListRow[]> {
app.app_status app.app_status
FROM applications AS app FROM applications AS app
LEFT JOIN members AS member LEFT JOIN members AS member
ON member.id = app.member_id;` ON member.id = app.member_id
ORDER BY app.submitted_at DESC
LIMIT ? OFFSET ?;`
const rows: ApplicationListRow[] = await pool.query(sql); const rows: ApplicationListRow[] = await pool.query(sql, [pageSize, page]);
return rows; return rows;
} }
@@ -59,30 +61,35 @@ export async function getAllMemberApplications(memberID: number): Promise<Applic
} }
export async function approveApplication(id: number) { export async function approveApplication(id: number, approver: number) {
const sql = ` const sql = `
UPDATE applications UPDATE applications
SET approved_at = NOW() SET approved_at = NOW(), approved_by = ?
WHERE id = ? WHERE id = ?
AND approved_at IS NULL AND approved_at IS NULL
AND denied_at IS NULL AND denied_at IS NULL
`; `;
const result = await pool.execute(sql, id); const result = await pool.execute(sql, [approver, id]);
return result; console.log(result);
if (result.affectedRows == 1) {
return
} else {
throw new Error(`"Something went wrong approving application with ID ${id}`);
}
} }
export async function denyApplication(id: number) { export async function denyApplication(id: number, approver: number) {
const sql = ` const sql = `
UPDATE applications UPDATE applications
SET denied_at = NOW() SET denied_at = NOW(), approved_by = ?
WHERE id = ? WHERE id = ?
AND approved_at IS NULL AND approved_at IS NULL
AND denied_at IS NULL AND denied_at IS NULL
`; `;
const result = await pool.execute(sql, id); const result = await pool.execute(sql, [approver, id]);
console.log(result);
if (result.affectedRows == 1) { if (result.affectedRows == 1) {
return return
} else { } else {

View File

@@ -94,16 +94,18 @@ export async function approveApplication(id: Number) {
const res = await fetch(`${addr}/application/approve/${id}`, { method: 'POST', credentials: 'include' }) const res = await fetch(`${addr}/application/approve/${id}`, { method: 'POST', credentials: 'include' })
if (!res.ok) { if (!res.ok) {
console.error("Something went wrong approving the application") throw new Error("Something went wrong approving the application");
} }
return;
} }
export async function denyApplication(id: Number) { export async function denyApplication(id: Number) {
const res = await fetch(`${addr}/application/deny/${id}`, { method: 'POST', credentials: 'include' }) const res = await fetch(`${addr}/application/deny/${id}`, { method: 'POST', credentials: 'include' })
if (!res.ok) { if (!res.ok) {
console.error("Something went wrong denying the application") throw new Error("Something went wrong denyting the application");
} }
return;
} }
export async function restartApplication() { export async function restartApplication() {

View File

@@ -174,7 +174,7 @@ watch(() => showCoC.value, async () => {
<FormLabel>Have you ever served in the military?</FormLabel> <FormLabel>Have you ever served in the military?</FormLabel>
<FormControl> <FormControl>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Checkbox :checked="value ?? false" @update:checked="handleChange" :disabled="readOnly" /> <Checkbox :model-value="value" @update:model-value="handleChange" :disabled="readOnly" />
<span>Yes (checked) / No (unchecked)</span> <span>Yes (checked) / No (unchecked)</span>
</div> </div>
</FormControl> </FormControl>

View File

@@ -105,12 +105,21 @@ async function postApp(appData) {
} }
async function handleApprove(id) { async function handleApprove(id) {
console.log("hi"); try {
await approveApplication(id); await approveApplication(id);
loadData(await loadApplication(Number(route.params.id), true))
} catch (error) {
console.error(error);
}
} }
async function handleDeny(id) { async function handleDeny(id) {
await denyApplication(id); try {
await denyApplication(id);
loadData(await loadApplication(Number(route.params.id), true))
} catch (error) {
console.error(error);
}
} }
</script> </script>
@@ -164,7 +173,8 @@ async function handleDeny(id) {
</ApplicationForm> </ApplicationForm>
<div v-if="!newApp" class="pb-15"> <div v-if="!newApp" class="pb-15">
<h3 class="scroll-m-20 text-2xl font-semibold tracking-tight mb-4">Discussion</h3> <h3 class="scroll-m-20 text-2xl font-semibold tracking-tight mb-4">Discussion</h3>
<ApplicationChat :messages="chatData" @post="postComment" @post-internal="postCommentInternal" :admin-mode="finalMode === 'view-recruiter'"> <ApplicationChat :messages="chatData" @post="postComment" @post-internal="postCommentInternal"
:admin-mode="finalMode === 'view-recruiter'">
</ApplicationChat> </ApplicationChat>
</div> </div>
</div> </div>

View File

@@ -11,7 +11,7 @@ import {
TableRow, TableRow,
} from '@/components/ui/table' } from '@/components/ui/table'
import Button from '@/components/ui/button/Button.vue'; import Button from '@/components/ui/button/Button.vue';
import { onMounted, ref, watch } from 'vue'; import { computed, onMounted, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { CheckIcon, XIcon } from 'lucide-vue-next'; import { CheckIcon, XIcon } from 'lucide-vue-next';
import Application from './Application.vue'; import Application from './Application.vue';
@@ -52,36 +52,26 @@ function formatExact(iso) {
return isNaN(d) ? '' : exactFmt.format(d) return isNaN(d) ? '' : exactFmt.format(d)
} }
async function handleApprove(id) {
await approveApplication(id);
appList.value = await getAllApplications();
}
async function handleDeny(id) {
await denyApplication(id);
appList.value = await getAllApplications();
}
const router = useRouter(); const router = useRouter();
function openApplication(id) { function openApplication(id) {
if (!id) return;
router.push(`/administration/applications/${id}`) router.push(`/administration/applications/${id}`)
openPanel.value = true;
} }
function closeApplication() { function closeApplication() {
router.push('/administration/applications') router.push('/administration/applications')
openPanel.value = false;
} }
const route = useRoute(); const route = useRoute();
watch(() => route.params.id, (newId) => { watch(() => route.params.id, (newId) => {
if (newId === undefined) { if (newId === undefined) {
openPanel.value = false;
} }
}) })
const openPanel = ref(false); // const openPanel = ref(false);
const openPanel = computed(() => route.params.id !== undefined)
onMounted(async () => { onMounted(async () => {
appList.value = await getAllApplications(); appList.value = await getAllApplications();
@@ -102,7 +92,7 @@ onMounted(async () => {
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead>User</TableHead> <TableHead>User</TableHead>
<TableHead>Date Submitted</TableHead> <TableHead class="text-right">Date Submitted</TableHead>
<TableHead class="text-right">Status</TableHead> <TableHead class="text-right">Status</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
@@ -117,20 +107,10 @@ onMounted(async () => {
<TableCell class="font-medium"> <TableCell class="font-medium">
<MemberCard :memberId="app.member_id"></MemberCard> <MemberCard :memberId="app.member_id"></MemberCard>
</TableCell> </TableCell>
<TableCell :title="formatExact(app.submitted_at)"> <TableCell class="text-right" :title="formatExact(app.submitted_at)">
{{ formatAgo(app.submitted_at) }} {{ formatAgo(app.submitted_at) }}
</TableCell> </TableCell>
<TableCell v-if="app.app_status === ApplicationStatus.Pending"
class="inline-flex items-end gap-2">
<Button variant="success" @click.stop="handleApprove(app.id)">
<CheckIcon />
</Button>
<Button variant="destructive" @click.stop="handleDeny(app.id)">
<XIcon />
</Button>
</TableCell>
<TableCell class="text-right font-semibold" :class="[ <TableCell class="text-right font-semibold" :class="[
app.app_status === ApplicationStatus.Pending && 'text-yellow-500', app.app_status === ApplicationStatus.Pending && 'text-yellow-500',
app.app_status === ApplicationStatus.Accepted && 'text-green-500', app.app_status === ApplicationStatus.Accepted && 'text-green-500',