Compare commits

...

22 Commits

Author SHA1 Message Date
76bf93b790 tweaked a few things to mitigate errors 2026-02-07 13:39:38 -05:00
d6bb2863c2 Merge branch 'main' of https://gitea.iceberg-gaming.com/17th-Ranger-Battalion-ORG/milsim-site-v4 into #179-suspensions 2026-02-07 13:25:35 -05:00
1101f0eb59 Fixed a whole lotta broken stuff by changing state from a string to a number 2026-02-07 13:25:15 -05:00
d321c83f49 updated db to support state history 2026-02-07 13:24:49 -05:00
2a64577e2d Merge pull request 'Fixed application form error' (#186) from Fixed-critical-application-form-bug into main
All checks were successful
Continuous Integration / Update Development (push) Successful in 3m38s
Reviewed-on: #186
2026-02-06 22:55:31 -06:00
59783ee93a Fixed age calculator visibility 2026-02-06 23:57:20 -05:00
bb01d08622 Fixed application form error 2026-02-06 23:55:09 -05:00
3dc5461783 Merge pull request 'Fixed annoying bullshit' (#184) from devcontainers into main
All checks were successful
Continuous Integration / Update Development (push) Successful in 2m51s
Reviewed-on: #184
2026-02-03 21:42:21 -06:00
d8455ccaa3 Merge branch 'main' into devcontainers 2026-02-03 21:42:12 -06:00
7ca617a51c Fixed annoying bullshit 2026-02-03 22:41:37 -05:00
0e2c5f8318 Merge pull request 'devcontainers' (#183) from devcontainers into main
All checks were successful
Continuous Integration / Update Development (push) Successful in 2m49s
Reviewed-on: #183
2026-02-03 21:27:50 -06:00
6811dc461c Merge branch 'main' into devcontainers 2026-02-03 21:27:30 -06:00
6f11bdb01d Merge branch 'devcontainers' of https://gitea.iceberg-gaming.com/17th-Ranger-Battalion-ORG/milsim-site-v4 into devcontainers 2026-02-03 22:27:03 -05:00
dd440a4e75 Cleaned up unused tables 2026-02-03 22:27:02 -05:00
2f7276a6c6 Merge pull request 'devcontainers' (#182) from devcontainers into main
All checks were successful
Continuous Integration / Update Development (push) Successful in 3m1s
Reviewed-on: #182
2026-02-03 21:23:10 -06:00
c18ef9aa8d Merge branch 'main' into devcontainers 2026-02-03 21:22:57 -06:00
3a5f9eb6f0 Merge branch 'devcontainers' of https://gitea.iceberg-gaming.com/17th-Ranger-Battalion-ORG/milsim-site-v4 into devcontainers 2026-02-03 22:22:52 -05:00
ab31b6e9f2 Corrected SP update handling 2026-02-03 22:22:50 -05:00
9ec30be6fb Merge pull request 'devcontainers' (#181) from devcontainers into main
All checks were successful
Continuous Integration / Update Development (push) Successful in 3m1s
Reviewed-on: #181
2026-02-03 20:40:22 -06:00
0c58e4045f Merge branch 'main' into devcontainers 2026-02-03 20:40:11 -06:00
ca23675dd1 Merge branch 'devcontainers' of https://gitea.iceberg-gaming.com/17th-Ranger-Battalion-ORG/milsim-site-v4 into devcontainers 2026-02-03 21:39:48 -05:00
e8805616c7 Fixed view creation breaking 2026-02-03 21:39:47 -05:00
25 changed files with 621 additions and 343 deletions

View 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
};

View 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
};

View File

@@ -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,
@@ -1374,34 +1325,10 @@ CREATE TABLE IF NOT EXISTS `units` (
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4;
CREATE TABLE `view_member_rank_unit_status_latest` (
`member_id` INT(11) NOT NULL,
`member_name` VARCHAR(1) NOT NULL COLLATE 'utf8mb4_general_ci',
`displayName` VARCHAR(1) NULL COLLATE 'utf8mb4_general_ci',
`member_state` ENUM('guest','applicant','member','retired','banned','denied') NOT NULL COLLATE 'utf8mb4_general_ci',
`rank_id` INT(11) NULL,
`rank` LONGTEXT NULL COLLATE 'utf8mb4_general_ci',
`rank_abv` VARCHAR(1) NULL COLLATE 'utf8mb4_general_ci',
`rank_date` DATE NULL,
`unit_id` INT(11) NULL,
`unit` VARCHAR(1) NULL COLLATE 'utf8mb4_general_ci',
`unit_date` DATE NULL,
`status` VARCHAR(1) NULL COLLATE 'utf8mb4_general_ci',
`status_date` DATE NULL,
`loa_until` DATE NULL
);
CREATE TABLE `view_member_settings` (
`id` INT(11) NOT NULL,
`displayName` VARCHAR(1) NULL COLLATE 'utf8mb4_general_ci'
);
DROP TABLE IF EXISTS `view_member_rank_unit_status_latest`;
CREATE ALGORITHM=UNDEFINED VIEW `view_member_rank_unit_status_latest` AS select `m`.`id` AS `member_id`,`m`.`name` AS `member_name`,`m`.`displayName` AS `displayName`,`m`.`state` AS `member_state`,`r`.`id` AS `rank_id`,`r`.`name` AS `rank`,`r`.`short_name` AS `rank_abv`,`mr`.`start_date` AS `rank_date`,`u`.`id` AS `unit_id`,`u`.`name` AS `unit`,`mu`.`start_date` AS `unit_date`,`s`.`name` AS `status`,`ms`.`start_date` AS `status_date`,`loa`.`loa_until` AS `loa_until` from (((((((`members` `m` left join (select `x`.`id` AS `id`,`x`.`member_id` AS `member_id`,`x`.`rank_id` AS `rank_id`,`x`.`authorized_by_id` AS `authorized_by_id`,`x`.`created_by_id` AS `created_by_id`,`x`.`reason` AS `reason`,`x`.`start_date` AS `start_date`,`x`.`end_date` AS `end_date`,`x`.`created_at` AS `created_at`,`x`.`updated_at` AS `updated_at`,`x`.`batch` AS `batch` from (`members_ranks` `x` join (select `members_ranks`.`member_id` AS `member_id`,max(`members_ranks`.`created_at`) AS `max_created` from `members_ranks` where `members_ranks`.`end_date` is null group by `members_ranks`.`member_id`) `y` on(`x`.`member_id` = `y`.`member_id` and `x`.`created_at` = `y`.`max_created`))) `mr` on(`mr`.`member_id` = `m`.`id`)) left join `ranks` `r` on(`r`.`id` = `mr`.`rank_id`)) left join (select `x`.`id` AS `id`,`x`.`member_id` AS `member_id`,`x`.`unit_id` AS `unit_id`,`x`.`authorized_by_id` AS `authorized_by_id`,`x`.`created_by_id` AS `created_by_id`,`x`.`reason` AS `reason`,`x`.`start_date` AS `start_date`,`x`.`end_date` AS `end_date`,`x`.`created_at` AS `created_at`,`x`.`updated_at` AS `updated_at` from (`members_unit` `x` join (select `members_unit`.`member_id` AS `member_id`,max(`members_unit`.`created_at`) AS `max_created` from `members_unit` where `members_unit`.`end_date` is null group by `members_unit`.`member_id`) `y` on(`x`.`member_id` = `y`.`member_id` and `x`.`created_at` = `y`.`max_created`))) `mu` on(`mu`.`member_id` = `m`.`id`)) left join `units` `u` on(`u`.`id` = `mu`.`unit_id`)) left join (select `x`.`id` AS `id`,`x`.`member_id` AS `member_id`,`x`.`status_id` AS `status_id`,`x`.`authorized_by_id` AS `authorized_by_id`,`x`.`created_by_id` AS `created_by_id`,`x`.`reason` AS `reason`,`x`.`start_date` AS `start_date`,`x`.`end_date` AS `end_date`,`x`.`created_at` AS `created_at`,`x`.`updated_at` AS `updated_at` from (`members_statuses` `x` join (select `members_statuses`.`member_id` AS `member_id`,max(`members_statuses`.`created_at`) AS `max_created` from `members_statuses` where `members_statuses`.`end_date` is null group by `members_statuses`.`member_id`) `y` on(`x`.`member_id` = `y`.`member_id` and `x`.`created_at` = `y`.`max_created`))) `ms` on(`ms`.`member_id` = `m`.`id`)) left join `statuses` `s` on(`s`.`id` = `ms`.`status_id`)) left join (select `x`.`id` AS `id`,`x`.`member_id` AS `member_id`,`x`.`filed_date` AS `filed_date`,`x`.`start_date` AS `start_date`,`x`.`end_date` AS `end_date`,`x`.`extended_till` AS `extended_till`,`x`.`type_id` AS `type_id`,`x`.`reason` AS `reason`,`x`.`created_by_id` AS `created_by_id`,`x`.`deleted` AS `deleted`,`x`.`expired` AS `expired`,`x`.`closed_by` AS `closed_by`,`x`.`ended_at` AS `ended_at`,`x`.`closed` AS `closed`,`x`.`created_at` AS `created_at`,`x`.`updated_at` AS `updated_at`,greatest(cast(`x`.`end_date` as date),cast(coalesce(`x`.`extended_till`,`x`.`end_date`) as date)) AS `loa_until` from (`leave_of_absences` `x` join (select `leave_of_absences`.`member_id` AS `member_id`,max(`leave_of_absences`.`created_at`) AS `max_created` from `leave_of_absences` where (`leave_of_absences`.`deleted` = 0 or `leave_of_absences`.`deleted` is null) and (`leave_of_absences`.`closed` = 0 or `leave_of_absences`.`closed` is null) group by `leave_of_absences`.`member_id`) `y` on(`x`.`member_id` = `y`.`member_id` and `x`.`created_at` = `y`.`max_created`))) `loa` on(`loa`.`member_id` = `m`.`id`)) order by `m`.`displayName`
CREATE OR REPLACE VIEW `view_member_rank_unit_status_latest` AS select `m`.`id` AS `member_id`,`m`.`name` AS `member_name`,`m`.`displayName` AS `displayName`,`m`.`state` AS `member_state`,`r`.`id` AS `rank_id`,`r`.`name` AS `rank`,`r`.`short_name` AS `rank_abv`,`mr`.`start_date` AS `rank_date`,`u`.`id` AS `unit_id`,`u`.`name` AS `unit`,`mu`.`start_date` AS `unit_date`,`s`.`name` AS `status`,`ms`.`start_date` AS `status_date`,`loa`.`loa_until` AS `loa_until` from (((((((`members` `m` left join (select `x`.`id` AS `id`,`x`.`member_id` AS `member_id`,`x`.`rank_id` AS `rank_id`,`x`.`authorized_by_id` AS `authorized_by_id`,`x`.`created_by_id` AS `created_by_id`,`x`.`reason` AS `reason`,`x`.`start_date` AS `start_date`,`x`.`end_date` AS `end_date`,`x`.`created_at` AS `created_at`,`x`.`updated_at` AS `updated_at`,`x`.`batch` AS `batch` from (`members_ranks` `x` join (select `members_ranks`.`member_id` AS `member_id`,max(`members_ranks`.`created_at`) AS `max_created` from `members_ranks` where `members_ranks`.`end_date` is null group by `members_ranks`.`member_id`) `y` on(`x`.`member_id` = `y`.`member_id` and `x`.`created_at` = `y`.`max_created`))) `mr` on(`mr`.`member_id` = `m`.`id`)) left join `ranks` `r` on(`r`.`id` = `mr`.`rank_id`)) left join (select `x`.`id` AS `id`,`x`.`member_id` AS `member_id`,`x`.`unit_id` AS `unit_id`,`x`.`authorized_by_id` AS `authorized_by_id`,`x`.`created_by_id` AS `created_by_id`,`x`.`reason` AS `reason`,`x`.`start_date` AS `start_date`,`x`.`end_date` AS `end_date`,`x`.`created_at` AS `created_at`,`x`.`updated_at` AS `updated_at` from (`members_unit` `x` join (select `members_unit`.`member_id` AS `member_id`,max(`members_unit`.`created_at`) AS `max_created` from `members_unit` where `members_unit`.`end_date` is null group by `members_unit`.`member_id`) `y` on(`x`.`member_id` = `y`.`member_id` and `x`.`created_at` = `y`.`max_created`))) `mu` on(`mu`.`member_id` = `m`.`id`)) left join `units` `u` on(`u`.`id` = `mu`.`unit_id`)) left join (select `x`.`id` AS `id`,`x`.`member_id` AS `member_id`,`x`.`status_id` AS `status_id`,`x`.`authorized_by_id` AS `authorized_by_id`,`x`.`created_by_id` AS `created_by_id`,`x`.`reason` AS `reason`,`x`.`start_date` AS `start_date`,`x`.`end_date` AS `end_date`,`x`.`created_at` AS `created_at`,`x`.`updated_at` AS `updated_at` from (`members_statuses` `x` join (select `members_statuses`.`member_id` AS `member_id`,max(`members_statuses`.`created_at`) AS `max_created` from `members_statuses` where `members_statuses`.`end_date` is null group by `members_statuses`.`member_id`) `y` on(`x`.`member_id` = `y`.`member_id` and `x`.`created_at` = `y`.`max_created`))) `ms` on(`ms`.`member_id` = `m`.`id`)) left join `statuses` `s` on(`s`.`id` = `ms`.`status_id`)) left join (select `x`.`id` AS `id`,`x`.`member_id` AS `member_id`,`x`.`filed_date` AS `filed_date`,`x`.`start_date` AS `start_date`,`x`.`end_date` AS `end_date`,`x`.`extended_till` AS `extended_till`,`x`.`type_id` AS `type_id`,`x`.`reason` AS `reason`,`x`.`created_by_id` AS `created_by_id`,`x`.`deleted` AS `deleted`,`x`.`expired` AS `expired`,`x`.`closed_by` AS `closed_by`,`x`.`ended_at` AS `ended_at`,`x`.`closed` AS `closed`,`x`.`created_at` AS `created_at`,`x`.`updated_at` AS `updated_at`,greatest(cast(`x`.`end_date` as date),cast(coalesce(`x`.`extended_till`,`x`.`end_date`) as date)) AS `loa_until` from (`leave_of_absences` `x` join (select `leave_of_absences`.`member_id` AS `member_id`,max(`leave_of_absences`.`created_at`) AS `max_created` from `leave_of_absences` where (`leave_of_absences`.`deleted` = 0 or `leave_of_absences`.`deleted` is null) and (`leave_of_absences`.`closed` = 0 or `leave_of_absences`.`closed` is null) group by `leave_of_absences`.`member_id`) `y` on(`x`.`member_id` = `y`.`member_id` and `x`.`created_at` = `y`.`max_created`))) `loa` on(`loa`.`member_id` = `m`.`id`)) order by `m`.`displayName`
;
DROP TABLE IF EXISTS `view_member_settings`;
CREATE ALGORITHM=UNDEFINED VIEW `view_member_settings` AS select `m`.`id` AS `id`,`m`.`displayName` AS `displayName` from `members` `m`
CREATE OR REPLACE VIEW `view_member_settings` AS select `m`.`id` AS `id`,`m`.`displayName` AS `displayName` from `members` `m`
;
/*!40103 SET TIME_ZONE=IFNULL(@OLD_TIME_ZONE, 'system') */;

View File

@@ -0,0 +1 @@
/* Replace with your SQL commands */

View File

@@ -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`;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -240,11 +240,12 @@ router.put('/:id/displayname', async (req, res) => {
router.post('/discharge', [requireLogin, requireMemberState(MemberState.Member), requireRole("17th Administrator")], async (req: Request, res: Response) => {
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();

View File

@@ -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);
}

View File

@@ -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
View File

@@ -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",

View File

@@ -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"
}
}

View File

@@ -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>

View File

@@ -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()}`, {

View File

@@ -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">

View File

@@ -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">

View File

@@ -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' : ''"

View File

@@ -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')

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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'
}
})

View File

@@ -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
}