Fixed a whole lotta broken stuff by changing state from a string to a number
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -6,43 +6,43 @@ import { memberCache } from "../../routes/auth";
|
|||||||
import * as mariadb from 'mariadb';
|
import * as mariadb from 'mariadb';
|
||||||
|
|
||||||
export async function getFilteredMembers(
|
export async function getFilteredMembers(
|
||||||
page: number = 1,
|
page: number = 1,
|
||||||
pageSize: number = 15,
|
pageSize: number = 15,
|
||||||
search?: string,
|
search?: string,
|
||||||
status?: string,
|
status?: string,
|
||||||
unitId?: string
|
unitId?: string
|
||||||
): Promise<PaginatedMembers> {
|
): Promise<PaginatedMembers> {
|
||||||
try {
|
try {
|
||||||
const offset = (page - 1) * pageSize;
|
const offset = (page - 1) * pageSize;
|
||||||
const whereClauses: string[] = [];
|
const whereClauses: string[] = [];
|
||||||
const params: any[] = [];
|
const params: any[] = [];
|
||||||
|
|
||||||
if (status && status !== 'all') {
|
if (status && status !== 'all') {
|
||||||
whereClauses.push(`m.state = ?`);
|
whereClauses.push(`m.state = ?`);
|
||||||
params.push(status);
|
params.push(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
whereClauses.push(`v.member_name LIKE ?`);
|
whereClauses.push(`v.member_name LIKE ?`);
|
||||||
params.push(`%${search}%`);
|
params.push(`%${search}%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unitId && unitId !== 'all') {
|
if (unitId && unitId !== 'all') {
|
||||||
whereClauses.push(`v.unit = ?`);
|
whereClauses.push(`v.unit = ?`);
|
||||||
params.push(unitId);
|
params.push(unitId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const whereClause = whereClauses.length > 0
|
const whereClause = whereClauses.length > 0
|
||||||
? ` WHERE ${whereClauses.join(' AND ')}`
|
? ` WHERE ${whereClauses.join(' AND ')}`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
// COUNT QUERY
|
// COUNT QUERY
|
||||||
const countQuery = `SELECT COUNT(*) as total FROM view_member_rank_unit_status_latest v INNER JOIN members m ON v.member_id = m.id ${whereClause}`;
|
const countQuery = `SELECT COUNT(*) as total FROM view_member_rank_unit_status_latest v INNER JOIN members m ON v.member_id = m.id ${whereClause}`;
|
||||||
const [countResults]: any[] = await pool.query(countQuery, params);
|
const [countResults]: any[] = await pool.query(countQuery, params);
|
||||||
const total = Number(countResults?.total) || 0;
|
const total = Number(countResults?.total) || 0;
|
||||||
|
|
||||||
// DATA QUERY
|
// DATA QUERY
|
||||||
const dataQuery = `
|
const dataQuery = `
|
||||||
SELECT
|
SELECT
|
||||||
v.*,
|
v.*,
|
||||||
CASE
|
CASE
|
||||||
@@ -60,106 +60,123 @@ export async function getFilteredMembers(
|
|||||||
LIMIT ? OFFSET ?
|
LIMIT ? OFFSET ?
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const rows: any[] = await pool.query(dataQuery, [...params, pageSize, offset]);
|
const rows: any[] = await pool.query(dataQuery, [...params, pageSize, offset]);
|
||||||
|
|
||||||
// Map rows to Member type
|
// Map rows to Member type
|
||||||
const members: Member[] = rows.map(row => ({
|
const members: Member[] = rows.map(row => ({
|
||||||
member_id: Number(row.member_id),
|
member_id: Number(row.member_id),
|
||||||
member_name: row.member_name,
|
member_name: row.member_name,
|
||||||
displayName: row.displayName,
|
displayName: row.displayName,
|
||||||
rank: row.rank,
|
rank: row.rank,
|
||||||
rank_date: row.rank_date,
|
rank_date: row.rank_date,
|
||||||
unit: row.unit,
|
unit: row.unit,
|
||||||
unit_date: row.unit_date,
|
unit_date: row.unit_date,
|
||||||
status: row.status,
|
status: row.status,
|
||||||
status_date: row.status_date,
|
status_date: row.status_date,
|
||||||
loa_until: row.loa_until ? new Date(row.loa_until) : undefined,
|
loa_until: row.loa_until ? new Date(row.loa_until) : undefined,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: members,
|
data: members,
|
||||||
pagination: {
|
pagination: {
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
total,
|
total,
|
||||||
totalPages: Math.ceil(total / pageSize),
|
totalPages: Math.ceil(total / pageSize),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('app', 'Error fetching filtered members', {
|
logger.error('app', 'Error fetching filtered members', {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: error instanceof Error ? error.message : String(error),
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserData(userID: number): Promise<Member> {
|
export async function getUserData(userID: number): Promise<Member> {
|
||||||
const sql = `SELECT * FROM view_member_rank_unit_status_latest WHERE member_id = ?`;
|
const sql = `SELECT * FROM view_member_rank_unit_status_latest WHERE member_id = ?`;
|
||||||
const res: Member = await pool.query(sql, [userID]);
|
const res: Member = await pool.query(sql, [userID]);
|
||||||
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) {
|
||||||
try {
|
const isInternalConn = !externalCon;
|
||||||
const sql = `UPDATE members
|
const con = (externalCon || await pool.getConnection()) as mariadb.PoolConnection;
|
||||||
SET state = ?
|
|
||||||
WHERE id = ?;`;
|
try {
|
||||||
return await con.query(sql, [state, userID]);
|
if (isInternalConn) await con.beginTransaction();
|
||||||
} catch (error) {
|
|
||||||
logger.error('app', 'Error setting user state', error);
|
await endLatestMemberState(userID, con);
|
||||||
} finally {
|
|
||||||
memberCache.Invalidate(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) {
|
||||||
|
if (isInternalConn) {
|
||||||
|
await con.rollback();
|
||||||
|
}
|
||||||
|
logger.error('app', 'Error setting user state', error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
memberCache.Invalidate(userID);
|
||||||
|
if (isInternalConn && con) con.release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserState(user: number): Promise<MemberState> {
|
export async function getUserState(user: number): Promise<MemberState> {
|
||||||
let out = await pool.query(`SELECT state FROM members WHERE id = ?`, [user]);
|
let out = await pool.query(`SELECT state FROM members WHERE id = ?`, [user]);
|
||||||
return (out[0].state as MemberState);
|
return (out[0].state as MemberState);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMemberSettings(id: number): Promise<memberSettings> {
|
export async function getMemberSettings(id: number): Promise<memberSettings> {
|
||||||
const sql = `SELECT * FROM view_member_settings WHERE id = ?`;
|
const sql = `SELECT * FROM view_member_settings WHERE id = ?`;
|
||||||
let out: memberSettings[] = await pool.query(sql, [id]);
|
let out: memberSettings[] = await pool.query(sql, [id]);
|
||||||
|
|
||||||
if (out.length != 1)
|
if (out.length != 1)
|
||||||
throw new Error("Could not get user settings");
|
throw new Error("Could not get user settings");
|
||||||
|
|
||||||
return out[0];
|
return out[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setUserSettings(id: number, settings: memberSettings) {
|
export async function setUserSettings(id: number, settings: memberSettings) {
|
||||||
const sql = `UPDATE view_member_settings SET
|
const sql = `UPDATE view_member_settings SET
|
||||||
displayName = ?
|
displayName = ?
|
||||||
WHERE id = ?;`;
|
WHERE id = ?;`;
|
||||||
let result = await pool.query(sql, [settings.displayName, id])
|
let result = await pool.query(sql, [settings.displayName, id])
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMembersLite(ids: number[]): Promise<MemberLight[]> {
|
export async function getMembersLite(ids: number[]): Promise<MemberLight[]> {
|
||||||
const sql = `SELECT m.member_id AS id,
|
const sql = `SELECT m.member_id AS id,
|
||||||
m.member_name AS username,
|
m.member_name AS username,
|
||||||
m.displayName,
|
m.displayName,
|
||||||
u.color
|
u.color
|
||||||
FROM view_member_rank_unit_status_latest m
|
FROM view_member_rank_unit_status_latest m
|
||||||
LEFT JOIN units u ON u.name = m.unit
|
LEFT JOIN units u ON u.name = m.unit
|
||||||
WHERE member_id IN (?);`;
|
WHERE member_id IN (?);`;
|
||||||
const res: MemberLight[] = await pool.query(sql, [ids]);
|
const res: MemberLight[] = await pool.query(sql, [ids]);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllMembersLite(): Promise<MemberLight[]> {
|
export async function getAllMembersLite(): Promise<MemberLight[]> {
|
||||||
const sql = `SELECT m.member_id AS id,
|
const sql = `SELECT m.member_id AS id,
|
||||||
m.member_name AS username,
|
m.member_name AS username,
|
||||||
m.displayName,
|
m.displayName,
|
||||||
u.color
|
u.color
|
||||||
FROM view_member_rank_unit_status_latest m
|
FROM view_member_rank_unit_status_latest m
|
||||||
LEFT JOIN units u ON u.name = m.unit;`;
|
LEFT JOIN units u ON u.name = m.unit;`;
|
||||||
|
|
||||||
const res: MemberLight[] = await pool.query(sql);
|
const res: MemberLight[] = await pool.query(sql);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMembersFull(ids: number[]): Promise<MemberCardDetails[]> {
|
export async function getMembersFull(ids: number[]): Promise<MemberCardDetails[]> {
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT m.*,
|
SELECT m.*,
|
||||||
COALESCE(
|
COALESCE(
|
||||||
JSON_ARRAYAGG(
|
JSON_ARRAYAGG(
|
||||||
@@ -181,30 +198,60 @@ export async function getMembersFull(ids: number[]): Promise<MemberCardDetails[]
|
|||||||
GROUP BY m.member_id;
|
GROUP BY m.member_id;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const rows: any[] = await pool.query(sql, [ids]);
|
const rows: any[] = await pool.query(sql, [ids]);
|
||||||
|
|
||||||
return rows.map(row => {
|
return rows.map(row => {
|
||||||
const member: Member = {
|
const member: Member = {
|
||||||
member_id: row.member_id,
|
member_id: row.member_id,
|
||||||
member_name: row.member_name,
|
member_name: row.member_name,
|
||||||
displayName: row.displayName,
|
displayName: row.displayName,
|
||||||
rank: row.rank,
|
rank: row.rank,
|
||||||
rank_date: row.rank_date,
|
rank_date: row.rank_date,
|
||||||
unit: row.unit,
|
unit: row.unit,
|
||||||
unit_date: row.unit_date,
|
unit_date: row.unit_date,
|
||||||
status: row.status,
|
status: row.status,
|
||||||
status_date: row.status_date,
|
status_date: row.status_date,
|
||||||
loa_until: row.loa_until ? new Date(row.loa_until) : undefined,
|
loa_until: row.loa_until ? new Date(row.loa_until) : undefined,
|
||||||
};
|
};
|
||||||
// roles comes as array of strings; parse each one
|
// roles comes as array of strings; parse each one
|
||||||
const roles: Role[] = row.roles;
|
const roles: Role[] = row.roles;
|
||||||
|
|
||||||
return { member, roles };
|
return { member, roles };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function mapDiscordtoID(id: number): Promise<number | null> {
|
export async function mapDiscordtoID(id: number): Promise<number | null> {
|
||||||
const sql = `SELECT id FROM members WHERE discord_id = ?;`
|
const sql = `SELECT id FROM members WHERE discord_id = ?;`
|
||||||
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);
|
||||||
}
|
}
|
||||||
@@ -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
108
ui/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,12 +38,15 @@ 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()">
|
||||||
</p>
|
LOA until <strong>{{ formatDate(userStore.user?.LOAs?.[0].extended_till ||
|
||||||
<p v-else>
|
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>
|
<p v-else>
|
||||||
|
LOA expired on <strong>{{ formatDate(userStore.user?.LOAs?.[0].extended_till ||
|
||||||
|
userStore.user?.LOAs?.[0].end_date) }}</strong>
|
||||||
|
</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
|
||||||
LOA</Button>
|
LOA</Button>
|
||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -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()}`, {
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|
||||||
|
|||||||
@@ -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' : ''"
|
||||||
|
|||||||
@@ -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") {
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,90 +1,93 @@
|
|||||||
<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,
|
||||||
StepperItem,
|
StepperItem,
|
||||||
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();
|
|
||||||
|
|
||||||
const steps = computed(() => {
|
|
||||||
const isDenied = userStore.state === 'denied'
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
step: 1,
|
|
||||||
title: 'Create account',
|
|
||||||
description: 'Begin by setting up your account',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 2,
|
|
||||||
title: 'Submit application',
|
|
||||||
description: 'Provide a few details about yourself',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 3,
|
|
||||||
title: 'Application review',
|
|
||||||
description: 'Our team will review your submission',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 4,
|
|
||||||
title: isDenied ? 'Application denied' : 'Acceptance',
|
|
||||||
description: isDenied
|
|
||||||
? 'Your application was not approved'
|
|
||||||
: 'Get started with the 17th Rangers',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
const currentStep = computed<number>(() => {
|
|
||||||
if (!userStore.isLoggedIn)
|
|
||||||
return 1;
|
|
||||||
switch (userStore.state) {
|
|
||||||
case "guest":
|
|
||||||
return 2;
|
|
||||||
break;
|
|
||||||
case "applicant":
|
|
||||||
return 3;
|
|
||||||
break;
|
|
||||||
case "member":
|
|
||||||
return 5;
|
|
||||||
break;
|
|
||||||
case "denied":
|
|
||||||
return 5;
|
|
||||||
break;
|
|
||||||
case "retired":
|
|
||||||
return 5;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
const finalPanel = ref<'app' | 'message'>('message');
|
let userStore = useUserStore();
|
||||||
|
|
||||||
const reloadKey = ref(0);
|
const steps = computed(() => {
|
||||||
|
const isDenied = userStore.state === MemberState.Denied
|
||||||
|
|
||||||
async function restartApp() {
|
return [
|
||||||
await restartApplication();
|
{
|
||||||
await userStore.loadUser();
|
step: 1,
|
||||||
reloadKey.value++;
|
title: 'Create account',
|
||||||
}
|
description: 'Begin by setting up your account',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
step: 2,
|
||||||
|
title: 'Submit application',
|
||||||
|
description: 'Provide a few details about yourself',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
step: 3,
|
||||||
|
title: 'Application review',
|
||||||
|
description: 'Our team will review your submission',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
step: 4,
|
||||||
|
title: isDenied ? 'Application denied' : 'Acceptance',
|
||||||
|
description: isDenied
|
||||||
|
? 'Your application was not approved'
|
||||||
|
: 'Get started with the 17th Rangers',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentStep = computed<number>(() => {
|
||||||
|
if (!userStore.isLoggedIn)
|
||||||
|
return 1;
|
||||||
|
switch (userStore.state) {
|
||||||
|
case MemberState.Guest:
|
||||||
|
return 2;
|
||||||
|
break;
|
||||||
|
case MemberState.Applicant:
|
||||||
|
return 3;
|
||||||
|
break;
|
||||||
|
case MemberState.Member:
|
||||||
|
return 5;
|
||||||
|
break;
|
||||||
|
case MemberState.Denied:
|
||||||
|
return 5;
|
||||||
|
break;
|
||||||
|
case MemberState.Retired:
|
||||||
|
return 5;
|
||||||
|
case MemberState.Discharged:
|
||||||
|
return 5;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const finalPanel = ref<'app' | 'message'>('message');
|
||||||
|
|
||||||
|
const reloadKey = ref(0);
|
||||||
|
|
||||||
|
async function restartApp() {
|
||||||
|
await restartApplication();
|
||||||
|
await userStore.loadUser();
|
||||||
|
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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user