Compare commits
19 Commits
0c58e4045f
...
#179-suspe
| Author | SHA1 | Date | |
|---|---|---|---|
| 76bf93b790 | |||
| d6bb2863c2 | |||
| 1101f0eb59 | |||
| d321c83f49 | |||
| 2a64577e2d | |||
| 59783ee93a | |||
| bb01d08622 | |||
| 3dc5461783 | |||
| d8455ccaa3 | |||
| 7ca617a51c | |||
| 0e2c5f8318 | |||
| 6811dc461c | |||
| 6f11bdb01d | |||
| dd440a4e75 | |||
| 2f7276a6c6 | |||
| c18ef9aa8d | |||
| 3a5f9eb6f0 | |||
| ab31b6e9f2 | |||
| 9ec30be6fb |
53
api/migrations/20260204025935-remove-unused-tables.js
Normal file
53
api/migrations/20260204025935-remove-unused-tables.js
Normal file
@@ -0,0 +1,53 @@
|
||||
'use strict';
|
||||
|
||||
var dbm;
|
||||
var type;
|
||||
var seed;
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var Promise;
|
||||
|
||||
/**
|
||||
* We receive the dbmigrate dependency from dbmigrate initially.
|
||||
* This enables us to not have to rely on NODE_PATH.
|
||||
*/
|
||||
exports.setup = function(options, seedLink) {
|
||||
dbm = options.dbmigrate;
|
||||
type = dbm.dataType;
|
||||
seed = seedLink;
|
||||
Promise = options.Promise;
|
||||
};
|
||||
|
||||
exports.up = function(db) {
|
||||
var filePath = path.join(__dirname, 'sqls', '20260204025935-remove-unused-tables-up.sql');
|
||||
return new Promise( function( resolve, reject ) {
|
||||
fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){
|
||||
if (err) return reject(err);
|
||||
console.log('received data: ' + data);
|
||||
|
||||
resolve(data);
|
||||
});
|
||||
})
|
||||
.then(function(data) {
|
||||
return db.runSql(data);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db) {
|
||||
var filePath = path.join(__dirname, 'sqls', '20260204025935-remove-unused-tables-down.sql');
|
||||
return new Promise( function( resolve, reject ) {
|
||||
fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){
|
||||
if (err) return reject(err);
|
||||
console.log('received data: ' + data);
|
||||
|
||||
resolve(data);
|
||||
});
|
||||
})
|
||||
.then(function(data) {
|
||||
return db.runSql(data);
|
||||
});
|
||||
};
|
||||
|
||||
exports._meta = {
|
||||
"version": 1
|
||||
};
|
||||
53
api/migrations/20260204140912-state-history-suspensions.js
Normal file
53
api/migrations/20260204140912-state-history-suspensions.js
Normal file
@@ -0,0 +1,53 @@
|
||||
'use strict';
|
||||
|
||||
var dbm;
|
||||
var type;
|
||||
var seed;
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var Promise;
|
||||
|
||||
/**
|
||||
* We receive the dbmigrate dependency from dbmigrate initially.
|
||||
* This enables us to not have to rely on NODE_PATH.
|
||||
*/
|
||||
exports.setup = function(options, seedLink) {
|
||||
dbm = options.dbmigrate;
|
||||
type = dbm.dataType;
|
||||
seed = seedLink;
|
||||
Promise = options.Promise;
|
||||
};
|
||||
|
||||
exports.up = function(db) {
|
||||
var filePath = path.join(__dirname, 'sqls', '20260204140912-state-history-suspensions-up.sql');
|
||||
return new Promise( function( resolve, reject ) {
|
||||
fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){
|
||||
if (err) return reject(err);
|
||||
console.log('received data: ' + data);
|
||||
|
||||
resolve(data);
|
||||
});
|
||||
})
|
||||
.then(function(data) {
|
||||
return db.runSql(data);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db) {
|
||||
var filePath = path.join(__dirname, 'sqls', '20260204140912-state-history-suspensions-down.sql');
|
||||
return new Promise( function( resolve, reject ) {
|
||||
fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){
|
||||
if (err) return reject(err);
|
||||
console.log('received data: ' + data);
|
||||
|
||||
resolve(data);
|
||||
});
|
||||
})
|
||||
.then(function(data) {
|
||||
return db.runSql(data);
|
||||
});
|
||||
};
|
||||
|
||||
exports._meta = {
|
||||
"version": 1
|
||||
};
|
||||
@@ -473,55 +473,6 @@ CREATE TABLE IF NOT EXISTS `members_unit` (
|
||||
CONSTRAINT `FK_members_unit_units` FOREIGN KEY (`unit_id`) REFERENCES `units` (`id`) ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=100 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `mission_attendee_roles` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(50) DEFAULT NULL,
|
||||
`short_name` varchar(50) DEFAULT NULL,
|
||||
`description` text DEFAULT NULL,
|
||||
`deleted` tinyint(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `role_name` (`name`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `mission_event_attendees` (
|
||||
`id` int(11) DEFAULT NULL,
|
||||
`member_id` int(11) NOT NULL,
|
||||
`event_id` int(11) NOT NULL,
|
||||
`member_role_id` int(11) DEFAULT NULL,
|
||||
`event_type` int(11) NOT NULL,
|
||||
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
|
||||
`updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
PRIMARY KEY (`member_id`,`event_id`) USING BTREE,
|
||||
KEY `fk_mission_event_attendees_role_id` (`member_role_id`),
|
||||
KEY `fk_mission_event_events_id` (`event_id`),
|
||||
CONSTRAINT `fk_member_event_member_id` FOREIGN KEY (`member_id`) REFERENCES `members` (`id`) ON UPDATE CASCADE,
|
||||
CONSTRAINT `fk_mission_event_attendees_role_id` FOREIGN KEY (`member_role_id`) REFERENCES `mission_attendee_roles` (`id`) ON UPDATE CASCADE,
|
||||
CONSTRAINT `fk_mission_event_events_id` FOREIGN KEY (`event_id`) REFERENCES `mission_events` (`id`) ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `mission_events` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`event_type_id` int(11) NOT NULL,
|
||||
`event_name` varchar(100) NOT NULL,
|
||||
`description` varchar(100) DEFAULT NULL,
|
||||
`mission_name` varchar(100) NOT NULL,
|
||||
`author_id` int(11) DEFAULT NULL,
|
||||
`map_id` int(11) DEFAULT NULL,
|
||||
`event_date` datetime NOT NULL,
|
||||
`guilded_event_id` int(11) DEFAULT NULL,
|
||||
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
|
||||
`updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
`deleted` tinyint(1) DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `event_name` (`event_name`),
|
||||
KEY `fk_events_author_member_id` (`author_id`) USING BTREE,
|
||||
KEY `fk_mission_event_type_id` (`event_type_id`),
|
||||
KEY `fk_mission_event_map_id` (`map_id`),
|
||||
CONSTRAINT `fk_events_author_id` FOREIGN KEY (`author_id`) REFERENCES `members` (`id`) ON UPDATE CASCADE,
|
||||
CONSTRAINT `fk_mission_event_map_id` FOREIGN KEY (`map_id`) REFERENCES `arma_maps` (`id`) ON UPDATE CASCADE,
|
||||
CONSTRAINT `fk_mission_event_type_id` FOREIGN KEY (`event_type_id`) REFERENCES `event_types` (`id`) ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `operation_campaigns` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(100) NOT NULL,
|
||||
@@ -1006,7 +957,7 @@ BEGIN
|
||||
AND ce.deleted = 0;
|
||||
END;
|
||||
|
||||
|
||||
DROP PROCEDURE IF EXISTS `sp_update_member_rank`;
|
||||
CREATE PROCEDURE IF NOT EXISTS `sp_update_member_rank`(
|
||||
IN `p_member_id` INT,
|
||||
IN `p_rank_id` INT,
|
||||
@@ -1110,7 +1061,7 @@ BEGIN
|
||||
END IF;
|
||||
END;
|
||||
|
||||
|
||||
DROP PROCEDURE IF EXISTS `sp_update_member_status`;
|
||||
CREATE PROCEDURE IF NOT EXISTS `sp_update_member_status`(
|
||||
IN `p_member_id` INT,
|
||||
IN `p_status_id` INT,
|
||||
@@ -1214,7 +1165,7 @@ BEGIN
|
||||
END IF;
|
||||
END;
|
||||
|
||||
|
||||
DROP PROCEDURE IF EXISTS `sp_update_member_unit`;
|
||||
CREATE PROCEDURE IF NOT EXISTS `sp_update_member_unit`(
|
||||
IN `p_member_id` INT,
|
||||
IN `p_unit_id` INT,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/* Replace with your SQL commands */
|
||||
@@ -0,0 +1,5 @@
|
||||
/* Replace with your SQL commands */
|
||||
|
||||
DROP PROCEDURE `sp_update_member_rank_Backup_1-27-2026`;
|
||||
DROP PROCEDURE `sp_update_member_status_Backup_1-27-2026`;
|
||||
DROP PROCEDURE `sp_update_member_unit_Backup_1-27-2026`;
|
||||
@@ -0,0 +1,14 @@
|
||||
/* Replace with your SQL commands */
|
||||
UPDATE members m
|
||||
JOIN account_states s ON m.state_id = s.id
|
||||
SET m.state_legacy = s.name;
|
||||
|
||||
ALTER TABLE members DROP FOREIGN KEY fk_members_state_id,
|
||||
DROP INDEX idx_members_state_id,
|
||||
DROP COLUMN state_id;
|
||||
|
||||
ALTER TABLE members
|
||||
RENAME COLUMN state_legacy TO state;
|
||||
|
||||
DROP TABLE IF EXISTS member_state_history;
|
||||
DROP TABLE IF EXISTS account_states;
|
||||
@@ -0,0 +1,47 @@
|
||||
/* Replace with your SQL commands */
|
||||
CREATE TABLE IF NOT EXISTS account_states (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(50) NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uq_account_states_name (name)
|
||||
);
|
||||
|
||||
INSERT IGNORE INTO account_states (name)
|
||||
VALUES ('guest'),
|
||||
('applicant'),
|
||||
('member'),
|
||||
('retired'),
|
||||
('discharged'),
|
||||
('suspended'),
|
||||
('banned'),
|
||||
('denied');
|
||||
|
||||
ALTER TABLE members
|
||||
RENAME COLUMN state TO state_legacy;
|
||||
|
||||
ALTER TABLE members
|
||||
ADD COLUMN state INT NOT NULL DEFAULT 1,
|
||||
ADD INDEX idx_members_state (state),
|
||||
ADD CONSTRAINT fk_members_state_id FOREIGN KEY (state) REFERENCES account_states(id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS member_state_history (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
member_id INT NOT NULL,
|
||||
state_id INT NOT NULL,
|
||||
reason VARCHAR(255),
|
||||
created_by_id INT,
|
||||
start_date DATE,
|
||||
end_date DATE,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_member_state_history_member_id (member_id),
|
||||
CONSTRAINT fk_member_state_history_member FOREIGN KEY (member_id) REFERENCES members(id),
|
||||
CONSTRAINT fk_member_state_type FOREIGN KEY (state_id) REFERENCES account_states(id),
|
||||
CONSTRAINT fk_member_state_history_created_by FOREIGN KEY (created_by_id) REFERENCES members(id)
|
||||
);
|
||||
|
||||
-- Convert member states to new system
|
||||
UPDATE members m
|
||||
JOIN account_states s ON m.state_legacy = s.name
|
||||
SET m.state = s.id;
|
||||
@@ -54,7 +54,7 @@ router.post('/', [requireLogin], async (req: Request, res: Response) => {
|
||||
try {
|
||||
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);
|
||||
|
||||
@@ -230,7 +230,7 @@ router.post('/approve/:id', [requireLogin, requireRole("Recruiter")], async (req
|
||||
await approveApplication(appID, approved_by);
|
||||
|
||||
//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])
|
||||
|
||||
@@ -262,7 +262,7 @@ router.post('/deny/:id', [requireLogin, requireRole("Recruiter")], async (req: R
|
||||
try {
|
||||
const app = await getApplicationByID(appID);
|
||||
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", {
|
||||
application: app.id,
|
||||
@@ -403,7 +403,7 @@ VALUES(?, ?, ?, 1);`
|
||||
router.post('/restart', async (req: Request, res: Response) => {
|
||||
const user = req.user.id;
|
||||
try {
|
||||
await setUserState(user, MemberState.Guest);
|
||||
await setUserState(user, MemberState.Guest, "Restarted Application", user);
|
||||
|
||||
logger.info('app', "Member restarted application", {
|
||||
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) => {
|
||||
try {
|
||||
var con = await pool.getConnection();
|
||||
let author = req.user.id;
|
||||
|
||||
con.beginTransaction();
|
||||
|
||||
var data: Discharge = req.body;
|
||||
setUserState(data.userID, MemberState.Retired, con);
|
||||
setUserState(data.userID, MemberState.Discharged, "Member Discharged", author, con);
|
||||
cancelLatestRank(data.userID, con);
|
||||
cancelLatestUnit(data.userID, con);
|
||||
con.commit();
|
||||
|
||||
@@ -6,43 +6,43 @@ import { memberCache } from "../../routes/auth";
|
||||
import * as mariadb from 'mariadb';
|
||||
|
||||
export async function getFilteredMembers(
|
||||
page: number = 1,
|
||||
pageSize: number = 15,
|
||||
search?: string,
|
||||
status?: string,
|
||||
unitId?: string
|
||||
page: number = 1,
|
||||
pageSize: number = 15,
|
||||
search?: string,
|
||||
status?: string,
|
||||
unitId?: string
|
||||
): Promise<PaginatedMembers> {
|
||||
try {
|
||||
const offset = (page - 1) * pageSize;
|
||||
const whereClauses: string[] = [];
|
||||
const params: any[] = [];
|
||||
try {
|
||||
const offset = (page - 1) * pageSize;
|
||||
const whereClauses: string[] = [];
|
||||
const params: any[] = [];
|
||||
|
||||
if (status && status !== 'all') {
|
||||
whereClauses.push(`m.state = ?`);
|
||||
params.push(status);
|
||||
}
|
||||
if (status && status !== 'all') {
|
||||
whereClauses.push(`m.state = ?`);
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
if (search) {
|
||||
whereClauses.push(`v.member_name LIKE ?`);
|
||||
params.push(`%${search}%`);
|
||||
}
|
||||
if (search) {
|
||||
whereClauses.push(`v.member_name LIKE ?`);
|
||||
params.push(`%${search}%`);
|
||||
}
|
||||
|
||||
if (unitId && unitId !== 'all') {
|
||||
whereClauses.push(`v.unit = ?`);
|
||||
params.push(unitId);
|
||||
}
|
||||
if (unitId && unitId !== 'all') {
|
||||
whereClauses.push(`v.unit = ?`);
|
||||
params.push(unitId);
|
||||
}
|
||||
|
||||
const whereClause = whereClauses.length > 0
|
||||
? ` WHERE ${whereClauses.join(' AND ')}`
|
||||
: '';
|
||||
const whereClause = whereClauses.length > 0
|
||||
? ` WHERE ${whereClauses.join(' AND ')}`
|
||||
: '';
|
||||
|
||||
// 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 [countResults]: any[] = await pool.query(countQuery, params);
|
||||
const total = Number(countResults?.total) || 0;
|
||||
// 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 [countResults]: any[] = await pool.query(countQuery, params);
|
||||
const total = Number(countResults?.total) || 0;
|
||||
|
||||
// DATA QUERY
|
||||
const dataQuery = `
|
||||
// DATA QUERY
|
||||
const dataQuery = `
|
||||
SELECT
|
||||
v.*,
|
||||
CASE
|
||||
@@ -60,106 +60,123 @@ export async function getFilteredMembers(
|
||||
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
|
||||
const members: Member[] = rows.map(row => ({
|
||||
member_id: Number(row.member_id),
|
||||
member_name: row.member_name,
|
||||
displayName: row.displayName,
|
||||
rank: row.rank,
|
||||
rank_date: row.rank_date,
|
||||
unit: row.unit,
|
||||
unit_date: row.unit_date,
|
||||
status: row.status,
|
||||
status_date: row.status_date,
|
||||
loa_until: row.loa_until ? new Date(row.loa_until) : undefined,
|
||||
}));
|
||||
// Map rows to Member type
|
||||
const members: Member[] = rows.map(row => ({
|
||||
member_id: Number(row.member_id),
|
||||
member_name: row.member_name,
|
||||
displayName: row.displayName,
|
||||
rank: row.rank,
|
||||
rank_date: row.rank_date,
|
||||
unit: row.unit,
|
||||
unit_date: row.unit_date,
|
||||
status: row.status,
|
||||
status_date: row.status_date,
|
||||
loa_until: row.loa_until ? new Date(row.loa_until) : undefined,
|
||||
}));
|
||||
|
||||
return {
|
||||
data: members,
|
||||
pagination: {
|
||||
page,
|
||||
pageSize,
|
||||
total,
|
||||
totalPages: Math.ceil(total / pageSize),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('app', 'Error fetching filtered members', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
data: members,
|
||||
pagination: {
|
||||
page,
|
||||
pageSize,
|
||||
total,
|
||||
totalPages: Math.ceil(total / pageSize),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('app', 'Error fetching filtered members', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUserData(userID: number): Promise<Member> {
|
||||
const sql = `SELECT * FROM view_member_rank_unit_status_latest WHERE member_id = ?`;
|
||||
const res: Member = await pool.query(sql, [userID]);
|
||||
return res[0] ?? null;
|
||||
const sql = `SELECT * FROM view_member_rank_unit_status_latest WHERE member_id = ?`;
|
||||
const res: Member = await pool.query(sql, [userID]);
|
||||
return res[0] ?? null;
|
||||
}
|
||||
|
||||
export async function setUserState(userID: number, state: MemberState, con: mariadb.Pool | mariadb.Connection = pool) {
|
||||
try {
|
||||
const sql = `UPDATE members
|
||||
SET state = ?
|
||||
WHERE id = ?;`;
|
||||
return await con.query(sql, [state, userID]);
|
||||
} catch (error) {
|
||||
logger.error('app', 'Error setting user state', error);
|
||||
} finally {
|
||||
memberCache.Invalidate(userID);
|
||||
}
|
||||
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 {
|
||||
if (isInternalConn) await con.beginTransaction();
|
||||
|
||||
await endLatestMemberState(userID, con);
|
||||
|
||||
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> {
|
||||
let out = await pool.query(`SELECT state FROM members WHERE id = ?`, [user]);
|
||||
return (out[0].state as MemberState);
|
||||
let out = await pool.query(`SELECT state FROM members WHERE id = ?`, [user]);
|
||||
return (out[0].state as MemberState);
|
||||
}
|
||||
|
||||
export async function getMemberSettings(id: number): Promise<memberSettings> {
|
||||
const sql = `SELECT * FROM view_member_settings WHERE id = ?`;
|
||||
let out: memberSettings[] = await pool.query(sql, [id]);
|
||||
const sql = `SELECT * FROM view_member_settings WHERE id = ?`;
|
||||
let out: memberSettings[] = await pool.query(sql, [id]);
|
||||
|
||||
if (out.length != 1)
|
||||
throw new Error("Could not get user settings");
|
||||
if (out.length != 1)
|
||||
throw new Error("Could not get user settings");
|
||||
|
||||
return out[0];
|
||||
return out[0];
|
||||
}
|
||||
|
||||
export async function setUserSettings(id: number, settings: memberSettings) {
|
||||
const sql = `UPDATE view_member_settings SET
|
||||
const sql = `UPDATE view_member_settings SET
|
||||
displayName = ?
|
||||
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[]> {
|
||||
const sql = `SELECT m.member_id AS id,
|
||||
const sql = `SELECT m.member_id AS id,
|
||||
m.member_name AS username,
|
||||
m.displayName,
|
||||
u.color
|
||||
FROM view_member_rank_unit_status_latest m
|
||||
LEFT JOIN units u ON u.name = m.unit
|
||||
WHERE member_id IN (?);`;
|
||||
const res: MemberLight[] = await pool.query(sql, [ids]);
|
||||
return res;
|
||||
const res: MemberLight[] = await pool.query(sql, [ids]);
|
||||
return res;
|
||||
}
|
||||
|
||||
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.displayName,
|
||||
u.color
|
||||
FROM view_member_rank_unit_status_latest m
|
||||
LEFT JOIN units u ON u.name = m.unit;`;
|
||||
|
||||
const res: MemberLight[] = await pool.query(sql);
|
||||
return res;
|
||||
const res: MemberLight[] = await pool.query(sql);
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function getMembersFull(ids: number[]): Promise<MemberCardDetails[]> {
|
||||
const sql = `
|
||||
const sql = `
|
||||
SELECT m.*,
|
||||
COALESCE(
|
||||
JSON_ARRAYAGG(
|
||||
@@ -181,30 +198,60 @@ export async function getMembersFull(ids: number[]): Promise<MemberCardDetails[]
|
||||
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 => {
|
||||
const member: Member = {
|
||||
member_id: row.member_id,
|
||||
member_name: row.member_name,
|
||||
displayName: row.displayName,
|
||||
rank: row.rank,
|
||||
rank_date: row.rank_date,
|
||||
unit: row.unit,
|
||||
unit_date: row.unit_date,
|
||||
status: row.status,
|
||||
status_date: row.status_date,
|
||||
loa_until: row.loa_until ? new Date(row.loa_until) : undefined,
|
||||
};
|
||||
// roles comes as array of strings; parse each one
|
||||
const roles: Role[] = row.roles;
|
||||
return rows.map(row => {
|
||||
const member: Member = {
|
||||
member_id: row.member_id,
|
||||
member_name: row.member_name,
|
||||
displayName: row.displayName,
|
||||
rank: row.rank,
|
||||
rank_date: row.rank_date,
|
||||
unit: row.unit,
|
||||
unit_date: row.unit_date,
|
||||
status: row.status,
|
||||
status_date: row.status_date,
|
||||
loa_until: row.loa_until ? new Date(row.loa_until) : undefined,
|
||||
};
|
||||
// roles comes as array of strings; parse each one
|
||||
const roles: Role[] = row.roles;
|
||||
|
||||
return { member, roles };
|
||||
});
|
||||
return { member, roles };
|
||||
});
|
||||
}
|
||||
|
||||
export async function mapDiscordtoID(id: number): Promise<number | null> {
|
||||
const sql = `SELECT id FROM members WHERE discord_id = ?;`
|
||||
let res = await pool.query(sql, [id]);
|
||||
return res.length > 0 ? res[0].id : null;
|
||||
const sql = `SELECT id FROM members WHERE discord_id = ?;`
|
||||
let res = await pool.query(sql, [id]);
|
||||
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 enum MemberState {
|
||||
Guest = "guest",
|
||||
Applicant = "applicant",
|
||||
Member = "member",
|
||||
Retired = "retired",
|
||||
Banned = "banned",
|
||||
Denied = "denied"
|
||||
Guest = 1,
|
||||
Applicant = 2,
|
||||
Member = 3,
|
||||
Retired = 4,
|
||||
Discharged = 5,
|
||||
Suspended = 6,
|
||||
Banned = 7,
|
||||
Denied = 8
|
||||
}
|
||||
|
||||
export type Member = {
|
||||
|
||||
108
ui/package-lock.json
generated
108
ui/package-lock.json
generated
@@ -35,7 +35,8 @@
|
||||
"@types/node": "^24.2.1",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"vite": "^7.0.6",
|
||||
"vite-plugin-vue-devtools": "^8.0.0"
|
||||
"vite-plugin-vue-devtools": "^8.0.0",
|
||||
"vue-tsc": "^3.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
@@ -1884,6 +1885,35 @@
|
||||
"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": {
|
||||
"version": "1.5.0",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "3.5.18",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.18.tgz",
|
||||
@@ -2171,6 +2217,13 @@
|
||||
"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": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansis/-/ansis-4.1.0.tgz",
|
||||
@@ -3123,6 +3176,13 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
@@ -3216,6 +3276,13 @@
|
||||
"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": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
@@ -3646,6 +3713,21 @@
|
||||
"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": {
|
||||
"version": "7.10.0",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "3.5.18",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.18.tgz",
|
||||
@@ -3974,6 +4063,23 @@
|
||||
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
|
||||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"@types/node": "^24.2.1",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"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>
|
||||
import { RouterView } from 'vue-router';
|
||||
import Button from './components/ui/button/Button.vue';
|
||||
import { useUserStore } from './stores/user';
|
||||
import Alert from './components/ui/alert/Alert.vue';
|
||||
import AlertDescription from './components/ui/alert/AlertDescription.vue';
|
||||
import Navbar from './components/Navigation/Navbar.vue';
|
||||
import { cancelLOA } from './api/loa';
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router';
|
||||
import Button from './components/ui/button/Button.vue';
|
||||
import { useUserStore } from './stores/user';
|
||||
import Alert from './components/ui/alert/Alert.vue';
|
||||
import AlertDescription from './components/ui/alert/AlertDescription.vue';
|
||||
import Navbar from './components/Navigation/Navbar.vue';
|
||||
import { cancelLOA } from './api/loa';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return "";
|
||||
return new Date(dateStr).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return "";
|
||||
return new Date(dateStr).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
const environment = import.meta.env.VITE_ENVIRONMENT;
|
||||
const version = import.meta.env.VITE_APPLICATION_VERSION;
|
||||
//@ts-ignore
|
||||
const environment = import.meta.env.VITE_ENVIRONMENT;
|
||||
//@ts-ignore
|
||||
const version = import.meta.env.VITE_APPLICATION_VERSION;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -36,12 +38,15 @@ const version = import.meta.env.VITE_APPLICATION_VERSION;
|
||||
</Alert>
|
||||
<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">
|
||||
<p 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 v-else>
|
||||
LOA expired on <strong>{{ formatDate(userStore.user?.LOAs?.[0].extended_till || userStore.user?.LOAs?.[0].end_date) }}</strong>
|
||||
</p>
|
||||
<p
|
||||
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 v-else>
|
||||
LOA expired on <strong>{{ formatDate(userStore.user?.LOAs?.[0].extended_till ||
|
||||
userStore.user?.LOAs?.[0].end_date) }}</strong>
|
||||
</p>
|
||||
<Button variant="secondary"
|
||||
@click="async () => { await cancelLOA(userStore.user.LOAs?.[0].id); userStore.loadUser(); }">End
|
||||
LOA</Button>
|
||||
@@ -52,5 +57,3 @@ const version = import.meta.env.VITE_APPLICATION_VERSION;
|
||||
<RouterView class="flex-1 min-h-0"></RouterView>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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
|
||||
const addr = import.meta.env.VITE_APIHOST;
|
||||
@@ -18,7 +18,7 @@ export async function getMembersFiltered(params: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
search?: string;
|
||||
status?: string;
|
||||
status?: string | MemberState;
|
||||
unitId?: string;
|
||||
} = {}): Promise<PaginatedMembers> {
|
||||
|
||||
@@ -27,7 +27,7 @@ export async function getMembersFiltered(params: {
|
||||
if (params.page) query.append('page', params.page.toString());
|
||||
if (params.pageSize) query.append('pageSize', params.pageSize.toString());
|
||||
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);
|
||||
|
||||
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 DropdownMenuGroup from '../ui/dropdown-menu/DropdownMenuGroup.vue';
|
||||
import DropdownMenuSeparator from '../ui/dropdown-menu/DropdownMenuSeparator.vue';
|
||||
import { MemberState } from '@shared/types/member';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const auth = useAuth();
|
||||
@@ -51,7 +52,7 @@ function blurAfter() {
|
||||
<img src="/17RBN_Logo.png" class="w-10 h-10 object-contain"></img>
|
||||
</RouterLink>
|
||||
<!-- 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>
|
||||
<NavigationMenuList class="gap-3">
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ const fallbackInitials = {
|
||||
|
||||
const props = defineProps<{
|
||||
readOnly: boolean,
|
||||
data: ApplicationData,
|
||||
data: ApplicationData | null,
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['submit']);
|
||||
@@ -110,14 +110,17 @@ watch(() => showCoC.value, async () => {
|
||||
});
|
||||
|
||||
function convertToAge(dob: string) {
|
||||
|
||||
if (dob === undefined) return "";
|
||||
const [month, day, year] = dob.split('/').map(Number);
|
||||
|
||||
let dobDate = new Date(year, month - 1, day);
|
||||
|
||||
return Math.floor(
|
||||
let out = Math.floor(
|
||||
(Date.now() - dobDate.getTime()) / (1000 * 60 * 60 * 24 * 365.2425)
|
||||
);
|
||||
|
||||
return Number.isNaN(out) ? "" : out;
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -132,7 +135,7 @@ function convertToAge(dob: string) {
|
||||
<FormControl>
|
||||
<template class="flex items-center gap-10">
|
||||
<DateInput :model-value="(value as string) ?? ''" :disabled="readOnly" @update:model-value="handleChange" />
|
||||
<p class="text-muted-foreground">Age: {{ convertToAge(value) }}</p>
|
||||
<p v-if="props.readOnly" class="text-muted-foreground">Age: {{ convertToAge(value) }}</p>
|
||||
</template>
|
||||
</FormControl>
|
||||
<div class="h-4">
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Calendar } from 'lucide-vue-next';
|
||||
import MemberCard from '../members/MemberCard.vue';
|
||||
import Spinner from '../ui/spinner/Spinner.vue';
|
||||
import { CopyLink } from '@/lib/copyLink';
|
||||
import { MemberState } from '@shared/types/member';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
@@ -86,7 +87,7 @@ async function setAttendance(state: CalendarAttendance) {
|
||||
|
||||
const canEditEvent = computed(() => {
|
||||
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)
|
||||
return true;
|
||||
});
|
||||
@@ -231,7 +232,7 @@ defineExpose({ forceReload })
|
||||
<CircleAlert></CircleAlert> This event has been cancelled
|
||||
</div>
|
||||
</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">
|
||||
<Button variant="outline" class="flex-1"
|
||||
: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";
|
||||
|
||||
|
||||
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
|
||||
|
||||
app.use(createPinia())
|
||||
const pinia = createPinia()
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
|
||||
if (import.meta.env.VITE_DISABLE_GLITCHTIP === "true") {
|
||||
@@ -38,7 +33,7 @@ if (import.meta.env.VITE_DISABLE_GLITCHTIP === "true") {
|
||||
});
|
||||
}
|
||||
|
||||
app.component("FormInput", FormInput)
|
||||
app.component("FormCheckbox", FormCheckbox)
|
||||
// app.component("FormInput", FormInput)
|
||||
// app.component("FormCheckbox", FormCheckbox)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useCalendarEvents } from '@/composables/useCalendarEvents'
|
||||
import { useCalendarNavigation } from '@/composables/useCalendarNavigation'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { CalendarOptions } from '@fullcalendar/core'
|
||||
import { MemberState } from '@shared/types/member'
|
||||
|
||||
const monthLabels = [
|
||||
'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
|
||||
function onDateClick(arg: { dateStr: string }) {
|
||||
if (!userStore.isLoggedIn) return;
|
||||
if (userStore.state !== 'member') return;
|
||||
if (userStore.state !== MemberState.Member) return;
|
||||
dialogRef.value?.openDialog(arg.dateStr);
|
||||
}
|
||||
|
||||
@@ -198,7 +199,7 @@ onMounted(() => {
|
||||
@click="goToday">
|
||||
Today
|
||||
</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"
|
||||
@click="onCreateEvent">
|
||||
<Plus class="h-4 w-4" />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { getWelcomeMessage } from '@/api/docs';
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { MemberState } from '@shared/types/member';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
@@ -14,7 +15,7 @@ function goToApplication() {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (user.state == 'member') {
|
||||
if (user.state == MemberState.Member) {
|
||||
let policy = await getWelcomeMessage() as any;
|
||||
welcomeRef.value.innerHTML = policy;
|
||||
}
|
||||
@@ -25,7 +26,7 @@ const welcomeRef = ref<HTMLElement>(null);
|
||||
|
||||
<template>
|
||||
<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">
|
||||
<!-- bookstack -->
|
||||
</div>
|
||||
|
||||
@@ -1,90 +1,93 @@
|
||||
<script setup lang="ts">
|
||||
import ApplicationForm from '@/components/application/ApplicationForm.vue';
|
||||
import Button from '@/components/ui/button/Button.vue';
|
||||
import {
|
||||
Stepper,
|
||||
StepperDescription,
|
||||
StepperIndicator,
|
||||
StepperItem,
|
||||
StepperSeparator,
|
||||
StepperTitle,
|
||||
StepperTrigger,
|
||||
} from '@/components/ui/stepper'
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { Check, Circle, Dot, Users, X } from 'lucide-vue-next'
|
||||
import { computed, ref } from 'vue';
|
||||
import Application from './Application.vue';
|
||||
import { restartApplication } from '@/api/application';
|
||||
import ApplicationForm from '@/components/application/ApplicationForm.vue';
|
||||
import Button from '@/components/ui/button/Button.vue';
|
||||
import {
|
||||
Stepper,
|
||||
StepperDescription,
|
||||
StepperIndicator,
|
||||
StepperItem,
|
||||
StepperSeparator,
|
||||
StepperTitle,
|
||||
StepperTrigger,
|
||||
} from '@/components/ui/stepper'
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { Check, Circle, Dot, Users, X } from 'lucide-vue-next'
|
||||
import { computed, ref } from 'vue';
|
||||
import Application from './Application.vue';
|
||||
import { restartApplication } from '@/api/application';
|
||||
import { MemberState } from '@shared/types/member';
|
||||
|
||||
function goToLogin() {
|
||||
const redirectUrl = encodeURIComponent(window.location.origin + '/join')
|
||||
//@ts-ignore
|
||||
const addr = import.meta.env.VITE_APIHOST;
|
||||
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;
|
||||
function goToLogin() {
|
||||
const redirectUrl = encodeURIComponent(window.location.origin + '/join')
|
||||
//@ts-ignore
|
||||
const addr = import.meta.env.VITE_APIHOST;
|
||||
window.location.href = `${addr}/login?redirect=${redirectUrl}`;
|
||||
}
|
||||
})
|
||||
|
||||
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() {
|
||||
await restartApplication();
|
||||
await userStore.loadUser();
|
||||
reloadKey.value++;
|
||||
}
|
||||
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 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>
|
||||
|
||||
<template>
|
||||
@@ -104,7 +107,8 @@ async function restartApp() {
|
||||
size="icon" class="z-10 rounded-full shrink-0"
|
||||
:class="[state === 'active' && 'ring-2 ring-ring ring-offset-2 ring-offset-background']">
|
||||
<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" />
|
||||
</template>
|
||||
<Circle v-if="state === 'active'" />
|
||||
@@ -160,7 +164,7 @@ async function restartApp() {
|
||||
</div>
|
||||
<div v-if="finalPanel === '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">
|
||||
Welcome to the 17th Ranger Battalion
|
||||
</h1>
|
||||
@@ -232,7 +236,7 @@ async function restartApp() {
|
||||
</div>
|
||||
</div>
|
||||
<!-- 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">
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-left">
|
||||
Application Not Approved
|
||||
@@ -263,7 +267,8 @@ async function restartApp() {
|
||||
<Button class="w-min" @click="restartApp">New Application</Button>
|
||||
</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">
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-left">
|
||||
You have retired from the 17th Ranger Battalion
|
||||
|
||||
@@ -135,11 +135,15 @@ onMounted(() => {
|
||||
const isDischargeOpen = ref(false)
|
||||
const targetMember = ref(null)
|
||||
|
||||
function openDischargeModal(member) {
|
||||
function openDischargeModal(member: Member) {
|
||||
targetMember.value = member
|
||||
isDischargeOpen.value = true
|
||||
}
|
||||
|
||||
function suspendMember(member: Member) {
|
||||
|
||||
}
|
||||
|
||||
function handleDischargeSuccess(data) {
|
||||
fetchMembers();
|
||||
}
|
||||
@@ -186,8 +190,8 @@ function handleDischargeSuccess(data) {
|
||||
</Select>
|
||||
<div v-if="filters.status !== 'all' || filters.unitId !== 'all'"
|
||||
class="h-4 w-[1px] bg-border mx-1" />
|
||||
<Button v-if="filters.status !== MemberState.Member || filters.unitId !== 'all'" variant="ghost" size="sm"
|
||||
class="h-8 px-2 text-xs text-muted-foreground"
|
||||
<Button v-if="filters.status !== MemberState.Member || filters.unitId !== 'all'" variant="ghost"
|
||||
size="sm" class="h-8 px-2 text-xs text-muted-foreground"
|
||||
@click="filters.status = MemberState.Member; filters.unitId = 'all'">
|
||||
Clear Filters
|
||||
</Button>
|
||||
@@ -250,6 +254,10 @@ function handleDischargeSuccess(data) {
|
||||
class="text-destructive focus:bg-destructive focus:text-destructive-foreground font-medium">
|
||||
Discharge Member
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="suspendMember(member)"
|
||||
class="text-destructive focus:bg-destructive focus:text-destructive-foreground font-medium">
|
||||
Suspend Member
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { MemberState } from '@shared/types/member';
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const router = createRouter({
|
||||
@@ -48,6 +49,7 @@ const router = createRouter({
|
||||
]
|
||||
})
|
||||
|
||||
//@ts-ignore
|
||||
const addr = import.meta.env.VITE_APIHOST;
|
||||
|
||||
|
||||
@@ -69,12 +71,12 @@ router.beforeEach(async (to) => {
|
||||
|
||||
|
||||
// Must be a member
|
||||
if (to.meta.memberOnly && user.state !== 'member') {
|
||||
if (to.meta.memberOnly && user.state !== MemberState.Member) {
|
||||
return '/unauthorized'
|
||||
}
|
||||
|
||||
// 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'
|
||||
}
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { myData } from '@shared/types/member'
|
||||
import { MemberState, myData } from '@shared/types/member'
|
||||
|
||||
|
||||
const POLL_INTERVAL = 10_000
|
||||
@@ -10,7 +10,7 @@ export const useUserStore = defineStore('user', () => {
|
||||
const user = ref<myData>(null)
|
||||
const roles = computed(() => new Set(user.value?.roles?.map(r => r.name) ?? []));
|
||||
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 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))
|
||||
}
|
||||
|
||||
//watcher to kick you off a page if your perms are revoked
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
watch(user, (newUser) => {
|
||||
@@ -46,7 +47,7 @@ export const useUserStore = defineStore('user', () => {
|
||||
const currentRoute = route.meta
|
||||
|
||||
// Member-only route
|
||||
if (currentRoute.memberOnly && state.value !== 'member') {
|
||||
if (currentRoute.memberOnly && state.value !== MemberState.Member) {
|
||||
router.replace('/unauthorized')
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user