Fixed a whole lotta broken stuff by changing state from a string to a number

This commit is contained in:
2026-02-07 13:25:15 -05:00
parent d321c83f49
commit 1101f0eb59
17 changed files with 435 additions and 260 deletions

View File

@@ -54,7 +54,7 @@ router.post('/', [requireLogin], async (req: Request, res: Response) => {
try { try {
let appID = await createApplication(memberID, appVersion, JSON.stringify(App)); let appID = await createApplication(memberID, appVersion, JSON.stringify(App));
await setUserState(memberID, MemberState.Applicant); await setUserState(memberID, MemberState.Applicant, "Application Submitted", memberID);
res.sendStatus(201); res.sendStatus(201);
@@ -230,7 +230,7 @@ router.post('/approve/:id', [requireLogin, requireRole("Recruiter")], async (req
await approveApplication(appID, approved_by); await approveApplication(appID, approved_by);
//update user profile //update user profile
await setUserState(app.member_id, MemberState.Member); await setUserState(app.member_id, MemberState.Member, "Application Accepted", approved_by);
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])
@@ -262,7 +262,7 @@ router.post('/deny/:id', [requireLogin, requireRole("Recruiter")], async (req: R
try { try {
const app = await getApplicationByID(appID); const app = await getApplicationByID(appID);
await denyApplication(appID, approver); await denyApplication(appID, approver);
await setUserState(app.member_id, MemberState.Denied); await setUserState(app.member_id, MemberState.Denied, "Application Denied", approver);
logger.info('app', "Member application approved", { logger.info('app', "Member application approved", {
application: app.id, application: app.id,
@@ -403,7 +403,7 @@ VALUES(?, ?, ?, 1);`
router.post('/restart', async (req: Request, res: Response) => { router.post('/restart', async (req: Request, res: Response) => {
const user = req.user.id; const user = req.user.id;
try { try {
await setUserState(user, MemberState.Guest); await setUserState(user, MemberState.Guest, "Restarted Application", user);
logger.info('app', "Member restarted application", { logger.info('app', "Member restarted application", {
user: user user: user

View File

@@ -240,11 +240,12 @@ router.put('/:id/displayname', async (req, res) => {
router.post('/discharge', [requireLogin, requireMemberState(MemberState.Member), requireRole("17th Administrator")], async (req: Request, res: Response) => { router.post('/discharge', [requireLogin, requireMemberState(MemberState.Member), requireRole("17th Administrator")], async (req: Request, res: Response) => {
try { try {
var con = await pool.getConnection(); var con = await pool.getConnection();
let author = req.user.id;
con.beginTransaction(); con.beginTransaction();
var data: Discharge = req.body; var data: Discharge = req.body;
setUserState(data.userID, MemberState.Retired, con); setUserState(data.userID, MemberState.Discharged, "Member Discharged", author, con);
cancelLatestRank(data.userID, con); cancelLatestRank(data.userID, con);
cancelLatestUnit(data.userID, con); cancelLatestUnit(data.userID, con);
con.commit(); con.commit();

View File

@@ -99,16 +99,33 @@ export async function getUserData(userID: number): Promise<Member> {
return res[0] ?? null; return res[0] ?? null;
} }
export async function setUserState(userID: number, state: MemberState, con: mariadb.Pool | mariadb.Connection = pool) { export async function setUserState(userID: number, state: MemberState, reason: string, creatorID: number, externalCon?: mariadb.Connection | mariadb.PoolConnection) {
const isInternalConn = !externalCon;
const con = (externalCon || await pool.getConnection()) as mariadb.PoolConnection;
try { try {
const sql = `UPDATE members if (isInternalConn) await con.beginTransaction();
SET state = ?
WHERE id = ?;`; await endLatestMemberState(userID, con);
return await con.query(sql, [state, userID]);
const sql = `UPDATE members SET state = ? WHERE id = ?;`;
await con.query(sql, [state, userID]);
const insertHistorySql = `INSERT INTO member_state_history
(member_id, state_id, reason, created_by_id, start_date, end_date)
VALUES (?, ?, ?, ?, NOW(), NULL);`;
await con.query(insertHistorySql, [userID, state, reason, creatorID]);
if (isInternalConn) await con.commit();
} catch (error) { } catch (error) {
if (isInternalConn) {
await con.rollback();
}
logger.error('app', 'Error setting user state', error); logger.error('app', 'Error setting user state', error);
throw error;
} finally { } finally {
memberCache.Invalidate(userID); memberCache.Invalidate(userID);
if (isInternalConn && con) con.release();
} }
} }
@@ -208,3 +225,33 @@ export async function mapDiscordtoID(id: number): Promise<number | null> {
let res = await pool.query(sql, [id]); let res = await pool.query(sql, [id]);
return res.length > 0 ? res[0].id : null; return res.length > 0 ? res[0].id : null;
} }
export async function endLatestMemberState(memberID: number, con: mariadb.Pool | mariadb.Connection = pool) {
const sql = `UPDATE member_state_history
SET end_date = NOW(),
updated_at = NOW()
WHERE id = (
SELECT id
FROM (
SELECT id
FROM member_state_history
WHERE member_id = ?
AND end_date IS NULL
ORDER BY start_date DESC,
created_at DESC
LIMIT 1
) AS x
);`;
try {
let res = await con.query(sql, [memberID]);
console.log(res);
} catch (error) {
logger.error('app', 'Error ending latest member state', {
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
let res = await pool.query(sql, [memberID]);
console.log(res);
}

View File

@@ -9,12 +9,14 @@ export interface memberSettings {
export type PaginatedMembers = PagedData<Member>; export type PaginatedMembers = PagedData<Member>;
export enum MemberState { export enum MemberState {
Guest = "guest", Guest = 1,
Applicant = "applicant", Applicant = 2,
Member = "member", Member = 3,
Retired = "retired", Retired = 4,
Banned = "banned", Discharged = 5,
Denied = "denied" Suspended = 6,
Banned = 7,
Denied = 8
} }
export type Member = { export type Member = {

108
ui/package-lock.json generated
View File

@@ -35,7 +35,8 @@
"@types/node": "^24.2.1", "@types/node": "^24.2.1",
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.1",
"vite": "^7.0.6", "vite": "^7.0.6",
"vite-plugin-vue-devtools": "^8.0.0" "vite-plugin-vue-devtools": "^8.0.0",
"vue-tsc": "^3.2.4"
}, },
"engines": { "engines": {
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"
@@ -1884,6 +1885,35 @@
"vue": "^3.2.25" "vue": "^3.2.25"
} }
}, },
"node_modules/@volar/language-core": {
"version": "2.4.27",
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.27.tgz",
"integrity": "sha512-DjmjBWZ4tJKxfNC1F6HyYERNHPYS7L7OPFyCrestykNdUZMFYzI9WTyvwPcaNaHlrEUwESHYsfEw3isInncZxQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@volar/source-map": "2.4.27"
}
},
"node_modules/@volar/source-map": {
"version": "2.4.27",
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.27.tgz",
"integrity": "sha512-ynlcBReMgOZj2i6po+qVswtDUeeBRCTgDurjMGShbm8WYZgJ0PA4RmtebBJ0BCYol1qPv3GQF6jK7C9qoVc7lg==",
"dev": true,
"license": "MIT"
},
"node_modules/@volar/typescript": {
"version": "2.4.27",
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.27.tgz",
"integrity": "sha512-eWaYCcl/uAPInSK2Lze6IqVWaBu/itVqR5InXcHXFyles4zO++Mglt3oxdgj75BDcv1Knr9Y93nowS8U3wqhxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@volar/language-core": "2.4.27",
"path-browserify": "^1.0.1",
"vscode-uri": "^3.0.8"
}
},
"node_modules/@vue/babel-helper-vue-transform-on": { "node_modules/@vue/babel-helper-vue-transform-on": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz", "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz",
@@ -2083,6 +2113,22 @@
"rfdc": "^1.4.1" "rfdc": "^1.4.1"
} }
}, },
"node_modules/@vue/language-core": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.4.tgz",
"integrity": "sha512-bqBGuSG4KZM45KKTXzGtoCl9cWju5jsaBKaJJe3h5hRAAWpZUuj5G+L+eI01sPIkm4H6setKRlw7E85wLdDNew==",
"dev": true,
"license": "MIT",
"dependencies": {
"@volar/language-core": "2.4.27",
"@vue/compiler-dom": "^3.5.0",
"@vue/shared": "^3.5.0",
"alien-signals": "^3.0.0",
"muggle-string": "^0.4.1",
"path-browserify": "^1.0.1",
"picomatch": "^4.0.2"
}
},
"node_modules/@vue/reactivity": { "node_modules/@vue/reactivity": {
"version": "3.5.18", "version": "3.5.18",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.18.tgz", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.18.tgz",
@@ -2171,6 +2217,13 @@
"vue": "^3.5.0" "vue": "^3.5.0"
} }
}, },
"node_modules/alien-signals": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz",
"integrity": "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==",
"dev": true,
"license": "MIT"
},
"node_modules/ansis": { "node_modules/ansis": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansis/-/ansis-4.1.0.tgz", "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.1.0.tgz",
@@ -3123,6 +3176,13 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/muggle-string": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
"integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
"dev": true,
"license": "MIT"
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -3216,6 +3276,13 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
"dev": true,
"license": "MIT"
},
"node_modules/path-key": { "node_modules/path-key": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -3646,6 +3713,21 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "7.10.0", "version": "7.10.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
@@ -3932,6 +4014,13 @@
"vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0"
} }
}, },
"node_modules/vscode-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
"integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
"dev": true,
"license": "MIT"
},
"node_modules/vue": { "node_modules/vue": {
"version": "3.5.18", "version": "3.5.18",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.18.tgz", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.18.tgz",
@@ -3974,6 +4063,23 @@
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/vue-tsc": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.4.tgz",
"integrity": "sha512-xj3YCvSLNDKt1iF9OcImWHhmYcihVu9p4b9s4PGR/qp6yhW+tZJaypGxHScRyOrdnHvaOeF+YkZOdKwbgGvp5g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@volar/typescript": "2.4.27",
"@vue/language-core": "3.2.4"
},
"bin": {
"vue-tsc": "bin/vue-tsc.js"
},
"peerDependencies": {
"typescript": ">=5.0.0"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -39,6 +39,7 @@
"@types/node": "^24.2.1", "@types/node": "^24.2.1",
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.1",
"vite": "^7.0.6", "vite": "^7.0.6",
"vite-plugin-vue-devtools": "^8.0.0" "vite-plugin-vue-devtools": "^8.0.0",
"vue-tsc": "^3.2.4"
} }
} }

View File

@@ -1,25 +1,27 @@
<script setup> <script setup lang="ts">
import { RouterView } from 'vue-router'; import { RouterView } from 'vue-router';
import Button from './components/ui/button/Button.vue'; import Button from './components/ui/button/Button.vue';
import { useUserStore } from './stores/user'; import { useUserStore } from './stores/user';
import Alert from './components/ui/alert/Alert.vue'; import Alert from './components/ui/alert/Alert.vue';
import AlertDescription from './components/ui/alert/AlertDescription.vue'; import AlertDescription from './components/ui/alert/AlertDescription.vue';
import Navbar from './components/Navigation/Navbar.vue'; import Navbar from './components/Navigation/Navbar.vue';
import { cancelLOA } from './api/loa'; import { cancelLOA } from './api/loa';
const userStore = useUserStore(); const userStore = useUserStore();
function formatDate(dateStr) { function formatDate(dateStr) {
if (!dateStr) return ""; if (!dateStr) return "";
return new Date(dateStr).toLocaleDateString("en-US", { return new Date(dateStr).toLocaleDateString("en-US", {
year: "numeric", year: "numeric",
month: "short", month: "short",
day: "numeric", day: "numeric",
}); });
} }
const environment = import.meta.env.VITE_ENVIRONMENT; //@ts-ignore
const version = import.meta.env.VITE_APPLICATION_VERSION; const environment = import.meta.env.VITE_ENVIRONMENT;
//@ts-ignore
const version = import.meta.env.VITE_APPLICATION_VERSION;
</script> </script>
<template> <template>
@@ -36,11 +38,14 @@ const version = import.meta.env.VITE_APPLICATION_VERSION;
</Alert> </Alert>
<Alert v-if="userStore.user?.LOAs?.[0]" class="m-2 mx-auto max-w-5xl" variant="info"> <Alert v-if="userStore.user?.LOAs?.[0]" class="m-2 mx-auto max-w-5xl" variant="info">
<AlertDescription class="flex flex-row items-center text-nowrap gap-5 mx-auto"> <AlertDescription class="flex flex-row items-center text-nowrap gap-5 mx-auto">
<p v-if="new Date(userStore.user?.LOAs?.[0].extended_till || userStore.user?.LOAs?.[0].end_date) > new Date()"> <p
LOA until <strong>{{ formatDate(userStore.user?.LOAs?.[0].extended_till || userStore.user?.LOAs?.[0].end_date) }}</strong> v-if="new Date(userStore.user?.LOAs?.[0].extended_till || userStore.user?.LOAs?.[0].end_date) > new Date()">
LOA until <strong>{{ formatDate(userStore.user?.LOAs?.[0].extended_till ||
userStore.user?.LOAs?.[0].end_date) }}</strong>
</p> </p>
<p v-else> <p v-else>
LOA expired on <strong>{{ formatDate(userStore.user?.LOAs?.[0].extended_till || userStore.user?.LOAs?.[0].end_date) }}</strong> LOA expired on <strong>{{ formatDate(userStore.user?.LOAs?.[0].extended_till ||
userStore.user?.LOAs?.[0].end_date) }}</strong>
</p> </p>
<Button variant="secondary" <Button variant="secondary"
@click="async () => { await cancelLOA(userStore.user.LOAs?.[0].id); userStore.loadUser(); }">End @click="async () => { await cancelLOA(userStore.user.LOAs?.[0].id); userStore.loadUser(); }">End
@@ -52,5 +57,3 @@ const version = import.meta.env.VITE_APPLICATION_VERSION;
<RouterView class="flex-1 min-h-0"></RouterView> <RouterView class="flex-1 min-h-0"></RouterView>
</div> </div>
</template> </template>
<style scoped></style>

View File

@@ -1,5 +1,5 @@
import { Discharge } from "@shared/schemas/dischargeSchema"; import { Discharge } from "@shared/schemas/dischargeSchema";
import { memberSettings, Member, MemberLight, MemberCardDetails, PaginatedMembers } from "@shared/types/member"; import { memberSettings, Member, MemberLight, MemberCardDetails, PaginatedMembers, MemberState } from "@shared/types/member";
// @ts-ignore // @ts-ignore
const addr = import.meta.env.VITE_APIHOST; const addr = import.meta.env.VITE_APIHOST;
@@ -18,7 +18,7 @@ export async function getMembersFiltered(params: {
page?: number; page?: number;
pageSize?: number; pageSize?: number;
search?: string; search?: string;
status?: string; status?: string | MemberState;
unitId?: string; unitId?: string;
} = {}): Promise<PaginatedMembers> { } = {}): Promise<PaginatedMembers> {
@@ -27,7 +27,7 @@ export async function getMembersFiltered(params: {
if (params.page) query.append('page', params.page.toString()); if (params.page) query.append('page', params.page.toString());
if (params.pageSize) query.append('pageSize', params.pageSize.toString()); if (params.pageSize) query.append('pageSize', params.pageSize.toString());
if (params.search) query.append('search', params.search); if (params.search) query.append('search', params.search);
if (params.status && params.status !== 'all') query.append('status', params.status); if (params.status && params.status !== 'all') query.append('status', String(params.status));
if (params.unitId && params.unitId !== 'all') query.append('unitId', params.unitId); if (params.unitId && params.unitId !== 'all') query.append('unitId', params.unitId);
const response = await fetch(`${addr}/members/filtered?${query.toString()}`, { const response = await fetch(`${addr}/members/filtered?${query.toString()}`, {

View File

@@ -21,6 +21,7 @@ import { useAuth } from '@/composables/useAuth';
import { ArrowUpRight, CircleArrowOutUpRight } from 'lucide-vue-next'; import { ArrowUpRight, CircleArrowOutUpRight } from 'lucide-vue-next';
import DropdownMenuGroup from '../ui/dropdown-menu/DropdownMenuGroup.vue'; import DropdownMenuGroup from '../ui/dropdown-menu/DropdownMenuGroup.vue';
import DropdownMenuSeparator from '../ui/dropdown-menu/DropdownMenuSeparator.vue'; import DropdownMenuSeparator from '../ui/dropdown-menu/DropdownMenuSeparator.vue';
import { MemberState } from '@shared/types/member';
const userStore = useUserStore(); const userStore = useUserStore();
const auth = useAuth(); const auth = useAuth();
@@ -51,7 +52,7 @@ function blurAfter() {
<img src="/17RBN_Logo.png" class="w-10 h-10 object-contain"></img> <img src="/17RBN_Logo.png" class="w-10 h-10 object-contain"></img>
</RouterLink> </RouterLink>
<!-- Member navigation --> <!-- Member navigation -->
<div v-if="auth.accountStatus.value == 'member'" class="h-15 flex items-center justify-center"> <div v-if="auth.accountStatus.value == MemberState.Member" class="h-15 flex items-center justify-center">
<NavigationMenu> <NavigationMenu>
<NavigationMenuList class="gap-3"> <NavigationMenuList class="gap-3">

View File

@@ -15,6 +15,7 @@ import { Calendar } from 'lucide-vue-next';
import MemberCard from '../members/MemberCard.vue'; import MemberCard from '../members/MemberCard.vue';
import Spinner from '../ui/spinner/Spinner.vue'; import Spinner from '../ui/spinner/Spinner.vue';
import { CopyLink } from '@/lib/copyLink'; import { CopyLink } from '@/lib/copyLink';
import { MemberState } from '@shared/types/member';
const route = useRoute(); const route = useRoute();
@@ -86,7 +87,7 @@ async function setAttendance(state: CalendarAttendance) {
const canEditEvent = computed(() => { const canEditEvent = computed(() => {
if (!userStore.isLoggedIn) return false; if (!userStore.isLoggedIn) return false;
if (userStore.state !== 'member') return false; if (userStore.state !== MemberState.Member) return false;
if (userStore.user.member.member_id == activeEvent.value.creator_id) if (userStore.user.member.member_id == activeEvent.value.creator_id)
return true; return true;
}); });
@@ -231,7 +232,7 @@ defineExpose({ forceReload })
<CircleAlert></CircleAlert> This event has been cancelled <CircleAlert></CircleAlert> This event has been cancelled
</div> </div>
</section> </section>
<section v-if="isPast && userStore.state === 'member'" class="w-full"> <section v-if="isPast && userStore.state === MemberState.Member" class="w-full">
<ButtonGroup class="flex w-full justify-center"> <ButtonGroup class="flex w-full justify-center">
<Button variant="outline" class="flex-1" <Button variant="outline" class="flex-1"
:class="myAttendance?.status === CalendarAttendance.Attending ? 'border-2 border-primary text-primary' : ''" :class="myAttendance?.status === CalendarAttendance.Attending ? 'border-2 border-primary text-primary' : ''"

View File

@@ -10,14 +10,9 @@ import FormInput from './components/form/FormInput.vue'
import * as Sentry from "@sentry/vue"; import * as Sentry from "@sentry/vue";
const app = createApp(App) const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.use(createPinia())
app.use(router) app.use(router)
if (import.meta.env.VITE_DISABLE_GLITCHTIP === "true") { if (import.meta.env.VITE_DISABLE_GLITCHTIP === "true") {

View File

@@ -12,6 +12,7 @@ import { useCalendarEvents } from '@/composables/useCalendarEvents'
import { useCalendarNavigation } from '@/composables/useCalendarNavigation' import { useCalendarNavigation } from '@/composables/useCalendarNavigation'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { CalendarOptions } from '@fullcalendar/core' import { CalendarOptions } from '@fullcalendar/core'
import { MemberState } from '@shared/types/member'
const monthLabels = [ const monthLabels = [
'January', 'February', 'March', 'April', 'May', 'June', 'January', 'February', 'March', 'April', 'May', 'June',
@@ -50,7 +51,7 @@ const dialogRef = ref<any>(null)
// NEW: handle day/time slot clicks to start creating an event // NEW: handle day/time slot clicks to start creating an event
function onDateClick(arg: { dateStr: string }) { function onDateClick(arg: { dateStr: string }) {
if (!userStore.isLoggedIn) return; if (!userStore.isLoggedIn) return;
if (userStore.state !== 'member') return; if (userStore.state !== MemberState.Member) return;
dialogRef.value?.openDialog(arg.dateStr); dialogRef.value?.openDialog(arg.dateStr);
} }
@@ -198,7 +199,7 @@ onMounted(() => {
@click="goToday"> @click="goToday">
Today Today
</button> </button>
<button v-if="userStore.isLoggedIn && userStore.state === 'member'" <button v-if="userStore.isLoggedIn && userStore.state === MemberState.Member"
class="cursor-pointer ml-1 inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:opacity-90" class="cursor-pointer ml-1 inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:opacity-90"
@click="onCreateEvent"> @click="onCreateEvent">
<Plus class="h-4 w-4" /> <Plus class="h-4 w-4" />

View File

@@ -2,6 +2,7 @@
import { getWelcomeMessage } from '@/api/docs'; import { getWelcomeMessage } from '@/api/docs';
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { MemberState } from '@shared/types/member';
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@@ -14,7 +15,7 @@ function goToApplication() {
} }
onMounted(async () => { onMounted(async () => {
if (user.state == 'member') { if (user.state == MemberState.Member) {
let policy = await getWelcomeMessage() as any; let policy = await getWelcomeMessage() as any;
welcomeRef.value.innerHTML = policy; welcomeRef.value.innerHTML = policy;
} }
@@ -25,7 +26,7 @@ const welcomeRef = ref<HTMLElement>(null);
<template> <template>
<div> <div>
<div v-if="user.state == 'member'" class="mt-10"> <div v-if="user.state == MemberState.Member" class="mt-10">
<div ref="welcomeRef" class="bookstack-container"> <div ref="welcomeRef" class="bookstack-container">
<!-- bookstack --> <!-- bookstack -->
</div> </div>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import ApplicationForm from '@/components/application/ApplicationForm.vue'; import ApplicationForm from '@/components/application/ApplicationForm.vue';
import Button from '@/components/ui/button/Button.vue'; import Button from '@/components/ui/button/Button.vue';
import { import {
Stepper, Stepper,
StepperDescription, StepperDescription,
StepperIndicator, StepperIndicator,
@@ -9,24 +9,25 @@ import {
StepperSeparator, StepperSeparator,
StepperTitle, StepperTitle,
StepperTrigger, StepperTrigger,
} from '@/components/ui/stepper' } from '@/components/ui/stepper'
import { useUserStore } from '@/stores/user'; import { useUserStore } from '@/stores/user';
import { Check, Circle, Dot, Users, X } from 'lucide-vue-next' import { Check, Circle, Dot, Users, X } from 'lucide-vue-next'
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import Application from './Application.vue'; import Application from './Application.vue';
import { restartApplication } from '@/api/application'; import { restartApplication } from '@/api/application';
import { MemberState } from '@shared/types/member';
function goToLogin() { function goToLogin() {
const redirectUrl = encodeURIComponent(window.location.origin + '/join') const redirectUrl = encodeURIComponent(window.location.origin + '/join')
//@ts-ignore //@ts-ignore
const addr = import.meta.env.VITE_APIHOST; const addr = import.meta.env.VITE_APIHOST;
window.location.href = `${addr}/login?redirect=${redirectUrl}`; window.location.href = `${addr}/login?redirect=${redirectUrl}`;
} }
let userStore = useUserStore(); let userStore = useUserStore();
const steps = computed(() => { const steps = computed(() => {
const isDenied = userStore.state === 'denied' const isDenied = userStore.state === MemberState.Denied
return [ return [
{ {
@@ -52,39 +53,41 @@ const steps = computed(() => {
: 'Get started with the 17th Rangers', : 'Get started with the 17th Rangers',
}, },
] ]
}) })
const currentStep = computed<number>(() => { const currentStep = computed<number>(() => {
if (!userStore.isLoggedIn) if (!userStore.isLoggedIn)
return 1; return 1;
switch (userStore.state) { switch (userStore.state) {
case "guest": case MemberState.Guest:
return 2; return 2;
break; break;
case "applicant": case MemberState.Applicant:
return 3; return 3;
break; break;
case "member": case MemberState.Member:
return 5; return 5;
break; break;
case "denied": case MemberState.Denied:
return 5; return 5;
break; break;
case "retired": case MemberState.Retired:
return 5;
case MemberState.Discharged:
return 5; return 5;
break; break;
} }
}) })
const finalPanel = ref<'app' | 'message'>('message'); const finalPanel = ref<'app' | 'message'>('message');
const reloadKey = ref(0); const reloadKey = ref(0);
async function restartApp() { async function restartApp() {
await restartApplication(); await restartApplication();
await userStore.loadUser(); await userStore.loadUser();
reloadKey.value++; reloadKey.value++;
} }
</script> </script>
<template> <template>
@@ -104,7 +107,8 @@ async function restartApp() {
size="icon" class="z-10 rounded-full shrink-0" size="icon" class="z-10 rounded-full shrink-0"
:class="[state === 'active' && 'ring-2 ring-ring ring-offset-2 ring-offset-background']"> :class="[state === 'active' && 'ring-2 ring-ring ring-offset-2 ring-offset-background']">
<template v-if="state === 'completed'"> <template v-if="state === 'completed'">
<X v-if="step.step === 4 && userStore.state === 'denied'" class="size-5" /> <X v-if="step.step === 4 && userStore.state === MemberState.Denied"
class="size-5" />
<Check v-else class="size-5" /> <Check v-else class="size-5" />
</template> </template>
<Circle v-if="state === 'active'" /> <Circle v-if="state === 'active'" />
@@ -160,7 +164,7 @@ async function restartApp() {
</div> </div>
<div v-if="finalPanel === 'message'"> <div v-if="finalPanel === 'message'">
<!-- Accepted message --> <!-- Accepted message -->
<div v-if="userStore.state === 'member'"> <div v-if="userStore.state === MemberState.Member">
<h1 class="text-3xl sm:text-4xl font-bold mb-4 text-left"> <h1 class="text-3xl sm:text-4xl font-bold mb-4 text-left">
Welcome to the 17th Ranger Battalion Welcome to the 17th Ranger Battalion
</h1> </h1>
@@ -232,7 +236,7 @@ async function restartApp() {
</div> </div>
</div> </div>
<!-- Denied message --> <!-- Denied message -->
<div v-else-if="userStore.state === 'denied'"> <div v-else-if="userStore.state === MemberState.Denied">
<div class="w-full max-w-2xl flex flex-col gap-8"> <div class="w-full max-w-2xl flex flex-col gap-8">
<h1 class="text-3xl sm:text-4xl font-bold text-left"> <h1 class="text-3xl sm:text-4xl font-bold text-left">
Application Not Approved Application Not Approved
@@ -263,7 +267,8 @@ async function restartApp() {
<Button class="w-min" @click="restartApp">New Application</Button> <Button class="w-min" @click="restartApp">New Application</Button>
</div> </div>
</div> </div>
<div v-else-if="userStore.state === 'retired'"> <div
v-else-if="userStore.state === MemberState.Discharged || userStore.state === MemberState.Retired">
<div class="w-full max-w-2xl flex flex-col gap-8"> <div class="w-full max-w-2xl flex flex-col gap-8">
<h1 class="text-3xl sm:text-4xl font-bold text-left"> <h1 class="text-3xl sm:text-4xl font-bold text-left">
You have retired from the 17th Ranger Battalion You have retired from the 17th Ranger Battalion

View File

@@ -135,11 +135,15 @@ onMounted(() => {
const isDischargeOpen = ref(false) const isDischargeOpen = ref(false)
const targetMember = ref(null) const targetMember = ref(null)
function openDischargeModal(member) { function openDischargeModal(member: Member) {
targetMember.value = member targetMember.value = member
isDischargeOpen.value = true isDischargeOpen.value = true
} }
function suspendMember(member: Member) {
}
function handleDischargeSuccess(data) { function handleDischargeSuccess(data) {
fetchMembers(); fetchMembers();
} }
@@ -186,8 +190,8 @@ function handleDischargeSuccess(data) {
</Select> </Select>
<div v-if="filters.status !== 'all' || filters.unitId !== 'all'" <div v-if="filters.status !== 'all' || filters.unitId !== 'all'"
class="h-4 w-[1px] bg-border mx-1" /> class="h-4 w-[1px] bg-border mx-1" />
<Button v-if="filters.status !== MemberState.Member || filters.unitId !== 'all'" variant="ghost" size="sm" <Button v-if="filters.status !== MemberState.Member || filters.unitId !== 'all'" variant="ghost"
class="h-8 px-2 text-xs text-muted-foreground" size="sm" class="h-8 px-2 text-xs text-muted-foreground"
@click="filters.status = MemberState.Member; filters.unitId = 'all'"> @click="filters.status = MemberState.Member; filters.unitId = 'all'">
Clear Filters Clear Filters
</Button> </Button>
@@ -250,6 +254,10 @@ function handleDischargeSuccess(data) {
class="text-destructive focus:bg-destructive focus:text-destructive-foreground font-medium"> class="text-destructive focus:bg-destructive focus:text-destructive-foreground font-medium">
Discharge Member Discharge Member
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem @click="suspendMember(member)"
class="text-destructive focus:bg-destructive focus:text-destructive-foreground font-medium">
Suspend Member
</DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</TableCell> </TableCell>

View File

@@ -1,4 +1,5 @@
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { MemberState } from '@shared/types/member';
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({ const router = createRouter({
@@ -48,6 +49,7 @@ const router = createRouter({
] ]
}) })
//@ts-ignore
const addr = import.meta.env.VITE_APIHOST; const addr = import.meta.env.VITE_APIHOST;
@@ -69,12 +71,12 @@ router.beforeEach(async (to) => {
// Must be a member // Must be a member
if (to.meta.memberOnly && user.state !== 'member') { if (to.meta.memberOnly && user.state !== MemberState.Member) {
return '/unauthorized' return '/unauthorized'
} }
// Must have specific role // Must have specific role
if (to.meta.roles && !user.hasRole('Dev') && !user.hasAnyRole(to.meta.roles)) { if (to.meta.roles && !user.hasRole('Dev') && !user.hasAnyRole(to.meta.roles as string[])) {
return '/unauthorized' return '/unauthorized'
} }
}) })

View File

@@ -1,7 +1,7 @@
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { myData } from '@shared/types/member' import { MemberState, myData } from '@shared/types/member'
const POLL_INTERVAL = 10_000 const POLL_INTERVAL = 10_000
@@ -10,7 +10,7 @@ export const useUserStore = defineStore('user', () => {
const user = ref<myData>(null) const user = ref<myData>(null)
const roles = computed(() => new Set(user.value?.roles?.map(r => r.name) ?? [])); const roles = computed(() => new Set(user.value?.roles?.map(r => r.name) ?? []));
const loaded = ref(false); const loaded = ref(false);
const state = computed<string | undefined>(() => user.value?.state || undefined); const state = computed<MemberState | undefined>(() => user.value?.state || undefined);
const isLoggedIn = computed(() => user.value !== null) const isLoggedIn = computed(() => user.value !== null)
const displayName = computed(() => user.value?.member.displayName || user.value?.member.member_name) const displayName = computed(() => user.value?.member.displayName || user.value?.member.member_name)
@@ -38,6 +38,7 @@ export const useUserStore = defineStore('user', () => {
return requiredRoles.some(r => roles.value.has(r)) return requiredRoles.some(r => roles.value.has(r))
} }
//watcher to kick you off a page if your perms are revoked
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
watch(user, (newUser) => { watch(user, (newUser) => {
@@ -46,7 +47,7 @@ export const useUserStore = defineStore('user', () => {
const currentRoute = route.meta const currentRoute = route.meta
// Member-only route // Member-only route
if (currentRoute.memberOnly && state.value !== 'member') { if (currentRoute.memberOnly && state.value !== MemberState.Member) {
router.replace('/unauthorized') router.replace('/unauthorized')
return return
} }