From 47de7b9ebb5ee290dde76cac9f935a23d91db616 Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Tue, 21 Oct 2025 12:38:00 -0400 Subject: [PATCH] finalized application acceptance system and started making types shared across front and backend --- .../{applications.js => applications.ts} | 97 +++++++------------ api/src/routes/ranks.js | 2 +- api/src/services/applicationService.ts | 71 ++++++++++++++ api/src/services/memberService.ts | 15 +++ api/src/services/rankService.js | 22 ----- api/src/services/rankService.ts | 32 ++++++ api/src/services/statusService.ts | 6 ++ api/tsconfig.json | 5 +- shared/package.json | 6 ++ shared/types/application.ts | 57 +++++++++++ ui/jsconfig.json | 3 +- ui/src/api/application.ts | 8 +- .../application/ApplicationForm.vue | 11 ++- ui/src/pages/Application.vue | 13 +-- ui/src/pages/ManageApplications.vue | 10 +- ui/src/router/index.js | 11 +-- ui/src/stores/user.ts | 1 - 17 files changed, 259 insertions(+), 111 deletions(-) rename api/src/routes/{applications.js => applications.ts} (62%) create mode 100644 api/src/services/applicationService.ts delete mode 100644 api/src/services/rankService.js create mode 100644 api/src/services/rankService.ts create mode 100644 api/src/services/statusService.ts create mode 100644 shared/package.json create mode 100644 shared/types/application.ts diff --git a/api/src/routes/applications.js b/api/src/routes/applications.ts similarity index 62% rename from api/src/routes/applications.js rename to api/src/routes/applications.ts index bcfe1b7..c1f441a 100644 --- a/api/src/routes/applications.js +++ b/api/src/routes/applications.ts @@ -2,23 +2,22 @@ const express = require('express'); const router = express.Router(); import pool from '../db'; +import { approveApplication, createApplication, getApplicationByID, getApplicationComments, getApplicationList, getMemberApplication } from '../services/applicationService'; +import { MemberState, setUserState } from '../services/memberService'; +import { getRankByName, insertMemberRank } from '../services/rankService'; +import { ApplicationFull, CommentRow } from "@app/shared/types/application" +import { assignUserToStatus } from '../services/statusService'; // POST /application router.post('/', async (req, res) => { try { const App = req.body?.App || {}; + const memberID = req.user.id; - // TODO: replace with current user ID - const memberId = 1; - - const sql = `INSERT INTO applications (member_id, app_version, app_data) VALUES (?, ?, ?);`; const appVersion = 1; - const params = [memberId, appVersion, JSON.stringify(App)] - - console.log(params) - - await pool.query(sql, params); + createApplication(memberID, appVersion, JSON.stringify(App)) + setUserState(memberID, MemberState.Applicant); res.sendStatus(201); } catch (err) { @@ -30,18 +29,7 @@ router.post('/', async (req, res) => { // GET /application/all router.get('/all', async (req, res) => { try { - const sql = `SELECT - member.name AS member_name, - app.id, - app.member_id, - app.submitted_at, - app.app_status - FROM applications AS app - LEFT JOIN members AS member - ON member.id = app.member_id;` - - const rows = await pool.query(sql); - + const rows = await getApplicationList(); res.status(200).json(rows); } catch (err) { console.error(err); @@ -49,47 +37,35 @@ router.get('/all', async (req, res) => { } }); +router.get('/me', async (req, res) => { + let userID = req.user.id; + + console.log("application/me") + + let app = getMemberApplication(userID); + console.log(app); +}) + // GET /application/:id router.get('/:id', async (req, res) => { let appID = req.params.id; - //TODO: Replace with real user Authorization and whatnot - // if the application is not "me" and I am not a recruiter, deny access to the application (return 403 or whatever) - if (appID === "me") - appID = 2; - try { const conn = await pool.getConnection() - const application = await conn.query( - `SELECT app.*, - member.name AS member_name - FROM applications AS app - INNER JOIN members AS member ON member.id = app.member_id - WHERE app.id = ?;`, - [appID] - ); + const application = await getApplicationByID(appID); if (!Array.isArray(application) || application.length === 0) { conn.release(); return res.status(204).json("Application Not Found"); } - const comments = await conn.query(`SELECT app.id AS comment_id, - app.post_content, - app.poster_id, - app.post_time, - app.last_modified, - member.name AS poster_name - FROM application_comments AS app - INNER JOIN members AS member ON member.id = app.poster_id - WHERE app.application_id = ?;`, - [appID]); + const comments: CommentRow[] = await getApplicationComments(appID); conn.release() - const output = { - application: application[0], + const output: ApplicationFull = { + application, comments, } return res.status(200).json(output); @@ -104,26 +80,27 @@ router.get('/:id', async (req, res) => { router.post('/approve/:id', async (req, res) => { const appID = req.params.id; - const sql = ` - UPDATE applications - SET approved_at = NOW() - WHERE id = ? - AND approved_at IS NULL - AND denied_at IS NULL - `; try { - const result = await pool.execute(sql, appID); + const app = await getApplicationByID(appID); + const result = await approveApplication(appID); - console.log(result); + console.log("START"); + console.log(app, result); - if (result.affectedRows === 0) { - res.status(400).json('Something went wrong approving the application'); + //guard against failures + if (result.affectedRows != 1) { + throw new Error("Something went wrong approving the application"); } - if (result.affectedRows == 1) { - res.sendStatus(200); - } + console.log(app.member_id); + //update user profile + await setUserState(app.member_id, MemberState.Member); + let nextRank = await getRankByName('Recruit') + await insertMemberRank(app.member_id, nextRank.id); + //assign user to "pending basic" + await assignUserToStatus(app.member_id, 1); + res.sendStatus(200); } catch (err) { console.error('Approve failed:', err); res.status(500).json({ error: 'Failed to approve application' }); diff --git a/api/src/routes/ranks.js b/api/src/routes/ranks.js index 63b1694..cb8c4b1 100644 --- a/api/src/routes/ranks.js +++ b/api/src/routes/ranks.js @@ -7,7 +7,7 @@ const { getAllRanks, insertMemberRank } = require('../services/rankService') ur.post('/', async (req, res) => {3 try { const change = req.body?.change; - await insertMemberRank(change); + await insertMemberRank(change.member_id, change.rank_id, change.date); res.sendStatus(201); } catch (err) { diff --git a/api/src/services/applicationService.ts b/api/src/services/applicationService.ts new file mode 100644 index 0000000..8e94a08 --- /dev/null +++ b/api/src/services/applicationService.ts @@ -0,0 +1,71 @@ +import { ApplicationListRow, ApplicationRow, CommentRow } from "@app/shared/types/application"; +import pool from "../db"; + +export async function createApplication(memberID: number, appVersion: number, app: string) { + const sql = `INSERT INTO applications (member_id, app_version, app_data) VALUES (?, ?, ?);`; + const params = [memberID, appVersion, JSON.stringify(app)] + return await pool.query(sql, params); +} + +export async function getMemberApplication(memberID: number): Promise { + const sql = `SELECT app.*, + member.name AS member_name + FROM applications AS app + INNER JOIN members AS member ON member.id = app.member_id + WHERE app.member_id = ?;`; + + let app: ApplicationRow[] = await pool.query(sql, [memberID]); + return app[0]; +} + +export async function getApplicationByID(appID: number): Promise { + const sql = + `SELECT app.*, + member.name AS member_name + FROM applications AS app + INNER JOIN members AS member ON member.id = app.member_id + WHERE app.id = ?;`; + let app: ApplicationRow[] = await pool.query(sql, [appID]); + return app[0]; +} + +export async function getApplicationList(): Promise { + const sql = `SELECT + member.name AS member_name, + app.id, + app.member_id, + app.submitted_at, + app.app_status + FROM applications AS app + LEFT JOIN members AS member + ON member.id = app.member_id;` + + const rows: ApplicationListRow[] = await pool.query(sql); + return rows; +} + +export async function approveApplication(id) { + const sql = ` + UPDATE applications + SET approved_at = NOW() + WHERE id = ? + AND approved_at IS NULL + AND denied_at IS NULL + `; + + const result = await pool.execute(sql, id); + return result; +} + +export async function getApplicationComments(appID: number): Promise { + return await pool.query(`SELECT app.id AS comment_id, + app.post_content, + app.poster_id, + app.post_time, + app.last_modified, + member.name AS poster_name + FROM application_comments AS app + INNER JOIN members AS member ON member.id = app.poster_id + WHERE app.application_id = ?;`, + [appID]); +} \ No newline at end of file diff --git a/api/src/services/memberService.ts b/api/src/services/memberService.ts index 8e0b6ac..98045da 100644 --- a/api/src/services/memberService.ts +++ b/api/src/services/memberService.ts @@ -1,8 +1,23 @@ import pool from "../db"; +export enum MemberState { + Guest = "guest", + Applicant = "applicant", + Member = "member", + Retired = "retired", + Banned = "banned", + Denied = "denied" +} + export async function getUserData(userID: number) { const sql = `SELECT * FROM members WHERE id = ?`; const res = await pool.query(sql, [userID]); return res[0] ?? null; } +export async function setUserState(userID: number, state: MemberState) { + const sql = `UPDATE members + SET state = ? + WHERE id = ?;`; + return await pool.query(sql, [state, userID]); +} \ No newline at end of file diff --git a/api/src/services/rankService.js b/api/src/services/rankService.js deleted file mode 100644 index 6724760..0000000 --- a/api/src/services/rankService.js +++ /dev/null @@ -1,22 +0,0 @@ -import pool from "../db"; - -async function getAllRanks() { - const rows = await pool.query( - 'SELECT id, name, short_name, sort_id FROM ranks;' - ); - return rows; -} - -async function insertMemberRank(change) { - const sql = ` - INSERT INTO members_ranks (member_id, rank_id, event_date) - VALUES (?, ?, ?); - `; - const params = [change.member_id, change.rank_id, change.date]; - await pool.query(sql, params); -} - -module.exports = { - getAllRanks, - insertMemberRank -}; diff --git a/api/src/services/rankService.ts b/api/src/services/rankService.ts new file mode 100644 index 0000000..f29a8b3 --- /dev/null +++ b/api/src/services/rankService.ts @@ -0,0 +1,32 @@ +import pool from "../db"; + +export async function getAllRanks() { + const rows = await pool.query( + 'SELECT id, name, short_name, sort_id FROM ranks;' + ); + return rows; +} + +export async function getRankByName(name: string) { + const rows = await pool.query(`SELECT id, name, short_name, sort_id FROM ranks WHERE name = ?`, [name]); + + if (rows.length === 0) + throw new Error("Could not find rank: " + name); + + return rows[0]; +} + +export async function insertMemberRank(member_id: number, rank_id: number, date: Date): Promise; +export async function insertMemberRank(member_id: number, rank_id: number): Promise; + +export async function insertMemberRank(member_id: number, rank_id: number, date?: Date): Promise { + const sql = date + ? `INSERT INTO members_ranks (member_id, rank_id, event_date) VALUES (?, ?, ?);` + : `INSERT INTO members_ranks (member_id, rank_id, event_date) VALUES (?, ?, NOW());`; + + const params = date + ? [member_id, rank_id, date] + : [member_id, rank_id]; + + await pool.query(sql, params); +} diff --git a/api/src/services/statusService.ts b/api/src/services/statusService.ts new file mode 100644 index 0000000..7a62f3a --- /dev/null +++ b/api/src/services/statusService.ts @@ -0,0 +1,6 @@ +import pool from "../db" + +export async function assignUserToStatus(userID: number, statusID: number) { + const sql = `INSERT INTO members_statuses (member_id, status_id, event_date) VALUES (?, ?, NOW())` + await pool.execute(sql, [userID, statusID]); +} diff --git a/api/tsconfig.json b/api/tsconfig.json index 60ac138..8893066 100644 --- a/api/tsconfig.json +++ b/api/tsconfig.json @@ -6,7 +6,10 @@ "types": [ "node", "express" - ] + ], + "paths": { + "@app/shared/*": ["../shared/*"] + } }, "include": [ "./src/**/*" diff --git a/shared/package.json b/shared/package.json new file mode 100644 index 0000000..374484e --- /dev/null +++ b/shared/package.json @@ -0,0 +1,6 @@ +{ + "name": "@app/shared", + "version": "1.0.0", + "main": "index.ts", + "type": "module" +} \ No newline at end of file diff --git a/shared/types/application.ts b/shared/types/application.ts new file mode 100644 index 0000000..0c5b1a4 --- /dev/null +++ b/shared/types/application.ts @@ -0,0 +1,57 @@ +export interface ApplicationData { + dob: string; + name: string; + playtime: number; + hobbies: string; + military: boolean; + communities: string; + joinReason: string; + milsimAttraction: string; + referral: string; + steamProfile: string; + timezone: string; + canAttendSaturday: boolean; + interests: string; + aknowledgeRules: boolean; +} + +export interface ApplicationRow { + id: number; + member_id: number; + app_version: number; + app_data: ApplicationData; + + submitted_at: string; // ISO datetime from DB (e.g., "2025-08-25T18:04:29.000Z") + updated_at: string | null; + approved_at: string | null; + denied_at: string | null; + + app_status: Status; // generated column + decision_at: string | null; // generated column + + // present when you join members (e.g., SELECT a.*, m.name AS member_name) + member_name: string; +} + +export interface CommentRow { + comment_id: number; + post_content: string; + poster_id: number; + post_time: string; + last_modified: string | null; + poster_name: string; +} + +export interface ApplicationFull { + application: ApplicationRow; + comments: CommentRow[]; +} + +//for get all applications route +export interface ApplicationListRow { + id: number; + member_id: number; + member_name: string | null; // because LEFT JOIN means it might be null + submitted_at: Date; + app_status: string; // or enum if you have one +} \ No newline at end of file diff --git a/ui/jsconfig.json b/ui/jsconfig.json index 5a1f2d2..225a4a4 100644 --- a/ui/jsconfig.json +++ b/ui/jsconfig.json @@ -1,7 +1,8 @@ { "compilerOptions": { "paths": { - "@/*": ["./src/*"] + "@/*": ["./src/*"], + "@shared": ["../shared/*"] } }, "exclude": ["node_modules", "dist"] diff --git a/ui/src/api/application.ts b/ui/src/api/application.ts index 14e72de..7ef6086 100644 --- a/ui/src/api/application.ts +++ b/ui/src/api/application.ts @@ -32,6 +32,7 @@ export interface ApplicationData { aknowledgeRules: boolean; } +//reflects how applications are stored in the database export interface ApplicationRow { id: number; member_id: number; @@ -43,7 +44,7 @@ export interface ApplicationRow { approved_at: string | null; denied_at: string | null; - app_status: Status; // generated column + app_status: ApplicationStatus; // generated column decision_at: string | null; // generated column // present when you join members (e.g., SELECT a.*, m.name AS member_name) @@ -64,7 +65,7 @@ export interface ApplicationFull { } -export enum Status { +export enum ApplicationStatus { Pending = "Pending", Accepted = "Accepted", Denied = "Denied", @@ -90,6 +91,7 @@ export async function postApplication(val: any) { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(out), + credentials: 'include', }) return res; } @@ -109,7 +111,7 @@ export async function postChatMessage(message: any, post_id: number) { return await response.json(); } -export async function getAllApplications() { +export async function getAllApplications(): Promise { const res = await fetch(`${addr}/application/all`) if (res.ok) { diff --git a/ui/src/components/application/ApplicationForm.vue b/ui/src/components/application/ApplicationForm.vue index 039cacf..6ab7da9 100644 --- a/ui/src/components/application/ApplicationForm.vue +++ b/ui/src/components/application/ApplicationForm.vue @@ -59,9 +59,13 @@ async function onSubmit(val: any) { onMounted(() => { if (props.data !== null) { - initialValues.value = { ...props.data } + const parsed = typeof props.data === "string" + ? JSON.parse(props.data) + : props.data; + + initialValues.value = { ...parsed }; } else { - initialValues.value = { ...fallbackInitials } + initialValues.value = { ...fallbackInitials }; } }) @@ -76,8 +80,7 @@ onMounted(() => { What is your date of birth? - + diff --git a/ui/src/pages/Application.vue b/ui/src/pages/Application.vue index d8b4ffe..2558340 100644 --- a/ui/src/pages/Application.vue +++ b/ui/src/pages/Application.vue @@ -2,7 +2,7 @@ import ApplicationChat from '@/components/application/ApplicationChat.vue'; import ApplicationForm from '@/components/application/ApplicationForm.vue'; import { onMounted, ref } from 'vue'; -import { ApplicationData, approveApplication, denyApplication, loadApplication, postApplication, postChatMessage, Status } from '@/api/application'; +import { ApplicationData, approveApplication, denyApplication, loadApplication, postApplication, postChatMessage, ApplicationStatus } from '@/api/application'; import { useRoute } from 'vue-router'; import Button from '@/components/ui/button/Button.vue'; import { CheckIcon, XIcon } from 'lucide-vue-next'; @@ -12,7 +12,7 @@ const appID = ref(null); const chatData = ref([]) const readOnly = ref(false); const newApp = ref(null); -const status = ref(null); +const status = ref(null); const decisionDate = ref(null); const submitDate = ref(null); const loading = ref(true); @@ -54,6 +54,7 @@ async function postComment(comment) { } async function postApp(appData) { + console.log("test") const res = await postApplication(appData); if (res.ok) { readOnly.value = true; @@ -89,11 +90,11 @@ async function handleDeny(id) {

{{ status }}

-

{{ status }}: {{ +

{{ status }}: {{ decisionDate.toLocaleString("en-US", { year: "numeric", month: "long", diff --git a/ui/src/pages/ManageApplications.vue b/ui/src/pages/ManageApplications.vue index a54d7e3..aff1810 100644 --- a/ui/src/pages/ManageApplications.vue +++ b/ui/src/pages/ManageApplications.vue @@ -1,5 +1,5 @@