diff --git a/api/package-lock.json b/api/package-lock.json index 1b35021..8cbfa51 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -10,7 +10,7 @@ "license": "ISC", "dependencies": { "@sentry/node": "^10.27.0", - "chalk": "^5.6.2", + "@types/express-session": "^1.18.2", "connect-sqlite3": "^0.9.16", "cors": "^2.8.5", "dotenv": "^17.2.1", @@ -758,7 +758,6 @@ "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, "license": "MIT", "dependencies": { "@types/connect": "*", @@ -778,7 +777,6 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", - "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -790,7 +788,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -799,6 +796,15 @@ "@types/send": "*" } }, + "node_modules/@types/express-session": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-k+I0BxwVXsnEU2hV77cCobC08kIsn4y44C3gC0b46uxZVMaXA04lSPgRLR/bSL2w0t0ShJiG8o4jPzRG/nscFg==", + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/geojson": { "version": "7946.0.16", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", @@ -809,14 +815,12 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, "license": "MIT" }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, "license": "MIT" }, "node_modules/@types/morgan": { @@ -871,21 +875,18 @@ "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, "license": "MIT" }, "node_modules/@types/send": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.0.tgz", "integrity": "sha512-zBF6vZJn1IaMpg3xUF25VK3gd3l8zwE0ZLRX7dsQyQi+jp4E8mMDJNGDYnYse+bQhYwWERTxVwHpi3dMOq7RKQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -895,7 +896,6 @@ "version": "1.15.9", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.9.tgz", "integrity": "sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==", - "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -907,7 +907,6 @@ "version": "0.17.5", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", - "dev": true, "license": "MIT", "dependencies": { "@types/mime": "^1", @@ -1315,18 +1314,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -3235,9 +3222,9 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" diff --git a/api/package.json b/api/package.json index 261b26a..4168206 100644 --- a/api/package.json +++ b/api/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@sentry/node": "^10.27.0", - "chalk": "^5.6.2", + "@types/express-session": "^1.18.2", "connect-sqlite3": "^0.9.16", "cors": "^2.8.5", "dotenv": "^17.2.1", diff --git a/api/src/index.ts b/api/src/index.ts index c7e900d..f5331a8 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -5,24 +5,24 @@ import express = require('express'); import cors = require('cors'); import morgan = require('morgan'); const app = express() -import chalk from 'chalk'; + app.use(morgan((tokens: morgan.TokenIndexer, req: express.Request, res: express.Response) => { - const status = Number(tokens.status(req, res)); + return JSON.stringify({ + type: 'http', + timestamp: new Date().toISOString(), - // Colorize status code - const statusColor = status >= 500 ? chalk.red - : status >= 400 ? chalk.yellow - : status >= 300 ? chalk.cyan - : chalk.green; + method: tokens.method(req, res), + path: tokens.url(req, res), + status: Number(tokens.status(req, res)), + response_time_ms: Number(tokens['response-time'](req, res)), - return [ - chalk.gray(`[${new Date().toISOString()}]`), - chalk.blue.bold(tokens.method(req, res)), - tokens.url(req, res), - statusColor(status), - chalk.magenta(tokens['response-time'](req, res) + ' ms'), - chalk.yellow(`- User: ${req.user?.name ? `${req.user.name} (${req.user.id})` : 'Unauthenticated'}`), - ].join(' '); + ip: req.ip, + user_agent: req.headers['user-agent'], + + user: req.user + ? { id: req.user.id, name: req.user.name } + : null, + }); }, { skip: (req: express.Request) => { return req.originalUrl === '/members/me'; @@ -55,21 +55,27 @@ if (process.env.DISABLE_GLITCHTIP === "true") { //session setup import path = require('path'); +// import session = require('express-session'); import session = require('express-session'); import passport = require('passport'); const SQLiteStore = require('connect-sqlite3')(session); -app.use(session({ +const cookieOptions: session.CookieOptions = { + httpOnly: true, + sameSite: 'lax', + domain: process.env.CLIENT_DOMAIN, + maxAge: 1000 * 60 * 60 * 24 * 30, //30 days +} +const sessionOptions: session.SessionOptions = { secret: 'whatever', resave: false, saveUninitialized: false, store: new SQLiteStore({ db: 'sessions.db', dir: './' }), - cookie: { - httpOnly: true, - sameSite: 'lax', - domain: process.env.CLIENT_DOMAIN - } -})); + rolling: true, + cookie: cookieOptions +} + +app.use(session(sessionOptions)); app.use(passport.authenticate('session')); // Mount route modules diff --git a/api/src/routes/applications.ts b/api/src/routes/applications.ts index 5a98f07..9a1a732 100644 --- a/api/src/routes/applications.ts +++ b/api/src/routes/applications.ts @@ -79,9 +79,11 @@ router.get('/me', [requireLogin], async (req, res) => { try { let application = await getMemberApplication(userID); - - if (application === undefined) + + if (application === undefined) { res.sendStatus(204); + return; + } const comments: CommentRow[] = await getApplicationComments(application.id); diff --git a/api/src/routes/auth.ts b/api/src/routes/auth.ts index 9175393..4925da1 100644 --- a/api/src/routes/auth.ts +++ b/api/src/routes/auth.ts @@ -46,32 +46,35 @@ passport.use(new OpenIDConnectStrategy({ //lookup existing user const existing = await con.query(`SELECT id FROM members WHERE authentik_issuer = ? AND authentik_sub = ? LIMIT 1;`, [issuer, sub]); - let memberId: number; + let memberId: number | null = null; //if member exists if (existing.length > 0) { memberId = existing[0].id; } else { - //otherwise: create account + //otherwise: create account mode const jwt = parseJwt(jwtClaims); - const discordID = jwt.discord.id as number; + const discordID = jwt.discord?.id as number; //check if account is available to claim - memberId = await mapDiscordtoID(discordID); + if (discordID) + memberId = await mapDiscordtoID(discordID); - if (memberId === null) { - // create new account + if (discordID && memberId) { + // claim account + console.log("Claiming account"); + const result = await con.query( + `UPDATE members SET authentik_sub = ?, authentik_issuer = ? WHERE id = ?;`, + [sub, issuer, memberId] + ) + } else { + console.log("New Account"); + // new account const username = sub.username; const result = await con.query( `INSERT INTO members (name, authentik_sub, authentik_issuer) VALUES (?, ?, ?)`, [username, sub, issuer] ) memberId = Number(result.insertId); - } else { - // claim existing account - const result = await con.query( - `UPDATE members SET authentik_sub = ?, authentik_issuer = ? WHERE id = ?;`, - [sub, issuer, memberId] - ) } } diff --git a/api/src/routes/loa.ts b/api/src/routes/loa.ts index de218c6..65aa115 100644 --- a/api/src/routes/loa.ts +++ b/api/src/routes/loa.ts @@ -26,7 +26,7 @@ router.post("/", async (req: Request, res: Response) => { }); //admin posts LOA -router.post("/admin", [requireRole("17th Administrator")], async (req: Request, res: Response) => { +router.post("/admin", [requireRole(['17th Administrator', '17th HQ', '17th Command'])], async (req: Request, res: Response) => { let LOARequest = req.body as LOARequest; LOARequest.created_by = req.user.id; LOARequest.filed_date = new Date(); @@ -67,7 +67,7 @@ router.get("/history", async (req: Request, res: Response) => { } }) -router.get('/all', [requireRole("17th Administrator")], async (req: Request, res: Response) => { +router.get('/all', [requireRole(['17th Administrator', '17th HQ', '17th Command'])], async (req: Request, res: Response) => { try { const page = Number(req.query.page) || undefined; const pageSize = Number(req.query.pageSize) || undefined; @@ -107,7 +107,7 @@ router.post('/cancel/:id', async (req: Request, res: Response) => { }) //TODO: enforce admin only -router.post('/adminCancel/:id', [requireRole("17th Administrator")], async (req: Request, res: Response) => { +router.post('/adminCancel/:id', [requireRole(['17th Administrator', '17th HQ', '17th Command'])], async (req: Request, res: Response) => { let closer = req.user.id; try { await closeLOA(Number(req.params.id), closer); @@ -119,7 +119,7 @@ router.post('/adminCancel/:id', [requireRole("17th Administrator")], async (req: }) // TODO: Enforce admin only -router.post('/extend/:id', [requireRole("17th Administrator")], async (req: Request, res: Response) => { +router.post('/extend/:id', [requireRole(['17th Administrator', '17th HQ', '17th Command'])], async (req: Request, res: Response) => { const to: Date = req.body.to; if (!to) { diff --git a/api/src/routes/roles.ts b/api/src/routes/roles.ts index d0f0e68..d77093f 100644 --- a/api/src/routes/roles.ts +++ b/api/src/routes/roles.ts @@ -5,7 +5,8 @@ const ur = express.Router(); import { MemberState } from '@app/shared/types/member'; import pool from '../db'; import { requireLogin, requireMemberState, requireRole } from '../middleware/auth'; -import { assignUserGroup, createGroup } from '../services/rolesService'; +import { assignUserGroup, createGroup, getAllRoles, getRole, getUsersWithRole } from '../services/rolesService'; +import { Request, Response } from 'express'; r.use(requireLogin) ur.use(requireLogin) @@ -15,10 +16,16 @@ ur.post('/', [requireMemberState(MemberState.Member), requireRole("17th Administ try { const body = req.body; - assignUserGroup(body.member_id, body.role_id); + await assignUserGroup(body.member_id, body.role_id); res.sendStatus(201); } catch (err) { + if (err?.code === 'ER_DUP_ENTRY') { + return res.status(400).json({ + error: 'Member already has this role', + }); + } + console.error('Insert failed:', err); res.status(500).json({ error: 'Failed to add to group' }); } @@ -44,45 +51,39 @@ ur.delete('/', [requireMemberState(MemberState.Member), requireRole("17th Admini //get all roles r.get('/', [requireMemberState(MemberState.Member)], async (req, res) => { try { - var con = await pool.getConnection(); - - // Get all roles - const roles = await con.query('SELECT * FROM roles;'); - - // Get all members for each role - const membersRoles = await con.query(` - SELECT mr.role_id, v.* - FROM members_roles mr - JOIN view_member_rank_unit_status_latest v ON mr.member_id = v.member_id - `); - - - // Group members by role_id - const roleIdToMembers = {}; - for (const row of membersRoles) { - if (!roleIdToMembers[row.role_id]) roleIdToMembers[row.role_id] = []; - // Remove role_id from member object - const { role_id, ...member } = row; - roleIdToMembers[role_id].push(member); - } - - // Attach members to each role - const result = roles.map(role => ({ - ...role, - members: roleIdToMembers[role.id] || [] - })); - - res.json(result); + const roles = await getAllRoles(); + res.status(200).json(roles); } catch (err) { console.error(err); - res.status(500).json({ error: 'Internal server error' }); - } finally { - con.release(); + res.sendStatus(500); } }); +r.get('/:id/members', [requireMemberState(MemberState.Member)], async (req: Request, res: Response) => { + try { + const members = await getUsersWithRole(Number(req.params.id)); + res.status(200).json(members); + } catch (err) { + console.error(err); + res.sendStatus(500); + } +}) + + +r.get('/:id', [requireMemberState(MemberState.Member)], async (req: Request, res: Response) => { + try { + const role = await getRole(Number(req.params.id)); + res.status(200).json(role); + } catch (err) { + console.error(err); + res.sendStatus(500); + } +}) + + + //create a new role -r.post('/', [requireMemberState(MemberState.Member), requireRole("17th Administrator")], async (req, res) => { +r.post('/', [requireMemberState(MemberState.Member), requireRole("dev")], async (req, res) => { try { const { name, color, description } = req.body; if (!name || !color) { @@ -103,7 +104,7 @@ r.post('/', [requireMemberState(MemberState.Member), requireRole("17th Administr } }) -r.delete('/:id', [requireMemberState(MemberState.Member), requireRole("17th Administrator")], async (req, res) => { +r.delete('/:id', [requireMemberState(MemberState.Member), requireRole("dev")], async (req, res) => { try { const id = req.params.id; diff --git a/api/src/services/CourseSerivce.ts b/api/src/services/CourseSerivce.ts index 8b2f61df..d85ffd8 100644 --- a/api/src/services/CourseSerivce.ts +++ b/api/src/services/CourseSerivce.ts @@ -83,8 +83,10 @@ export async function insertCourseEvent(event: CourseEventDetails): Promise { const sql = `SELECT * FROM view_member_rank_unit_status_latest WHERE member_id = ?`; @@ -60,10 +61,50 @@ export async function getAllMembersLite(): Promise { return res; } -export async function getMembersFull(ids: number[]): Promise { - const sql = `SELECT * FROM view_member_rank_unit_status_latest WHERE member_id IN (?);`; - const res: Member[] = await pool.query(sql, [ids]); - return res; +export async function getMembersFull(ids: number[]): Promise { + const sql = ` + SELECT m.*, + COALESCE( + JSON_ARRAYAGG( + CASE + WHEN r.id IS NOT NULL THEN JSON_OBJECT( + 'id', r.id, + 'name', r.name, + 'color', r.color, + 'description', r.description + ) + END + ), + JSON_ARRAY() + ) AS roles + FROM view_member_rank_unit_status_latest m + LEFT JOIN members_roles mr ON m.member_id = mr.member_id + LEFT JOIN roles r ON mr.role_id = r.id + WHERE m.member_id IN (?) + GROUP BY m.member_id; + `; + + 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[] = JSON.parse(row.roles).map((r: string) => JSON.parse(r)); + + return { member, roles }; + }); } export async function mapDiscordtoID(id: number): Promise { diff --git a/api/src/services/rolesService.ts b/api/src/services/rolesService.ts index f9e724c..19eb228 100644 --- a/api/src/services/rolesService.ts +++ b/api/src/services/rolesService.ts @@ -1,8 +1,8 @@ +import { MemberLight } from '@app/shared/types/member'; import pool from '../db'; -import { Role } from '@app/shared/types/roles' +import { Role, RoleSummary } from '@app/shared/types/roles' export async function assignUserGroup(userID: number, roleID: number) { - const sql = `INSERT INTO members_roles (member_id, role_id) VALUES (?, ?);`; const params = [userID, roleID]; @@ -24,4 +24,34 @@ export async function getUserRoles(userID: number): Promise { WHERE mr.member_id = ?;`; return await pool.query(sql, [userID]); +} + +export async function getRole(id: number): Promise { + let res = await pool.query(`SELECT * FROM roles WHERE id = ?`, [id]) + return res[0] as Role; +} + +export async function getAllRoles(): Promise { + return await pool.query(`SELECT id, name, color FROM roles`); +} + +export async function getUsersWithRole(roleId: number): Promise { + const out = await pool.query( + ` + SELECT + m.member_id AS id, + m.member_name AS username, + m.displayName, + u.color + FROM members_roles mr + JOIN view_member_rank_unit_status_latest m + ON m.member_id = mr.member_id + LEFT JOIN units u + ON u.name = m.unit + WHERE mr.role_id = ? + `, + [roleId] + ) + + return out as MemberLight[] } \ No newline at end of file diff --git a/shared/types/member.ts b/shared/types/member.ts index 7caa9f0..eea7f85 100644 --- a/shared/types/member.ts +++ b/shared/types/member.ts @@ -34,6 +34,11 @@ export interface MemberLight { color: string } +export interface MemberCardDetails { + member: Member; + roles: Role[]; +} + export interface myData { member: Member; LOAs: LOARequest[]; diff --git a/shared/types/roles.ts b/shared/types/roles.ts index a232c52..08ab762 100644 --- a/shared/types/roles.ts +++ b/shared/types/roles.ts @@ -1,6 +1,14 @@ +import { MemberLight } from "./member"; + export interface Role { id: number; name: string; color?: string; description?: string; +} + +export interface RoleSummary { + id: number; + name: string; + color?: string; } \ No newline at end of file diff --git a/ui/src/api/member.ts b/ui/src/api/member.ts index 327a5ce..b6aeb47 100644 --- a/ui/src/api/member.ts +++ b/ui/src/api/member.ts @@ -1,4 +1,4 @@ -import { memberSettings, Member, MemberLight } from "@shared/types/member"; +import { memberSettings, Member, MemberLight, MemberCardDetails } from "@shared/types/member"; // @ts-ignore const addr = import.meta.env.VITE_APIHOST; @@ -71,7 +71,7 @@ export async function getLightMembers(ids: number[]): Promise { return response.json(); } -export async function getFullMembers(ids: number[]): Promise { +export async function getFullMembers(ids: number[]): Promise { if (ids.length === 0) return []; diff --git a/ui/src/api/roles.ts b/ui/src/api/roles.ts index 3fb67ee..6ec36e8 100644 --- a/ui/src/api/roles.ts +++ b/ui/src/api/roles.ts @@ -1,10 +1,5 @@ -export type Role = { - id: number; - name: string; - color: string; - description: string | null; - members: any[]; -}; +import { Member, MemberLight } from "@shared/types/member"; +import { Role } from "@shared/types/roles"; // @ts-ignore const addr = import.meta.env.VITE_APIHOST; @@ -22,6 +17,30 @@ export async function getRoles(): Promise { } } +export async function getRoleDetails(id: number): Promise { + const res = await fetch(`${addr}/roles/${id}`, { + credentials: 'include', + }) + + if (res.ok) { + return res.json() as Promise; + } else { + throw new Error("Could not load role"); + } +} + +export async function getRoleMembers(id: number): Promise { + const res = await fetch(`${addr}/roles/${id}/members`, { + credentials: 'include', + }) + + if (res.ok) { + return res.json(); + } else { + throw new Error("Could not load members"); + } +} + export async function createRole(name: string, color: string, description: string | null): Promise { const res = await fetch(`${addr}/roles`, { method: "POST", diff --git a/ui/src/components/loa/loaList.vue b/ui/src/components/loa/loaList.vue index a097af4..1785564 100644 --- a/ui/src/components/loa/loaList.vue +++ b/ui/src/components/loa/loaList.vue @@ -84,7 +84,7 @@ function loaStatus(loa: LOARequest): "Upcoming" | "Active" | "Overdue" | "Closed if (now < start) return "Upcoming"; if (now >= start && now <= end) return "Active"; - if (now > end) return "Overdue"; + if (now > loa.extended_till || end) return "Overdue"; return "Overdue"; // fallback } @@ -197,7 +197,7 @@ function setPage(pagenum: number) { - @@ -220,10 +220,11 @@ function setPage(pagenum: number) { - - @@ -233,18 +234,24 @@ function setPage(pagenum: number) {
-
- -

- Reason -

+
+ + +
+

+ Reason +

+ +
-

- {{ post.reason }} -

+
+ {{ post.reason || 'No reason provided.' }} +
+
+
diff --git a/ui/src/components/members/MemberCard.vue b/ui/src/components/members/MemberCard.vue index 6195998..7b4424c 100644 --- a/ui/src/components/members/MemberCard.vue +++ b/ui/src/components/members/MemberCard.vue @@ -1,7 +1,7 @@ + + \ No newline at end of file diff --git a/ui/src/components/roles/roleView.vue b/ui/src/components/roles/roleView.vue new file mode 100644 index 0000000..9d7437f --- /dev/null +++ b/ui/src/components/roles/roleView.vue @@ -0,0 +1,142 @@ + + + diff --git a/ui/src/components/tooltip/Tooltip.vue b/ui/src/components/tooltip/Tooltip.vue new file mode 100644 index 0000000..1e46eec --- /dev/null +++ b/ui/src/components/tooltip/Tooltip.vue @@ -0,0 +1,20 @@ + + + \ No newline at end of file diff --git a/ui/src/components/trainingReport/trainingReportForm.vue b/ui/src/components/trainingReport/trainingReportForm.vue index b68aeb0..4eea9b3 100644 --- a/ui/src/components/trainingReport/trainingReportForm.vue +++ b/ui/src/components/trainingReport/trainingReportForm.vue @@ -25,6 +25,7 @@ import Popover from "@/components/ui/popover/Popover.vue"; import PopoverTrigger from "@/components/ui/popover/PopoverTrigger.vue"; import PopoverContent from "@/components/ui/popover/PopoverContent.vue"; import Combobox from '../ui/combobox/Combobox.vue' +import Tooltip from '../tooltip/Tooltip.vue' const { handleSubmit, resetForm, errors, values, setFieldValue } = useForm({ @@ -50,7 +51,9 @@ watch(() => values.course_id, (newCourseId, oldCourseId) => { if (!oldCourseId) return; values.attendees.forEach((a, index) => { + // @ts-ignore setFieldValue(`attendees[${index}].passed_bookwork`, false); + // @ts-ignore setFieldValue(`attendees[${index}].passed_qual`, false); }); }); @@ -326,22 +329,13 @@ const filteredMembers = computed(() => {
-
- + - -
- This course does not have bookwork -
-
+
@@ -351,20 +345,12 @@ const filteredMembers = computed(() => {
-
+ - -
- This course does not have a qualification -
-
+
diff --git a/ui/src/components/ui/input-group/index.js b/ui/src/components/ui/input-group/index.js index f1081fd..ea6ba22 100644 --- a/ui/src/components/ui/input-group/index.js +++ b/ui/src/components/ui/input-group/index.js @@ -2,10 +2,10 @@ import { cva } from "class-variance-authority"; export { default as InputGroup } from "./InputGroup.vue"; export { default as InputGroupAddon } from "./InputGroupAddon.vue"; -export { default as InputGroupButton } from "./InputGroupButton.vue"; -export { default as InputGroupInput } from "./InputGroupInput.vue"; -export { default as InputGroupText } from "./InputGroupText.vue"; -export { default as InputGroupTextarea } from "./InputGroupTextarea.vue"; +// export { default as InputGroupButton } from "./InputGroupButton.vue"; +// export { default as InputGroupInput } from "./InputGroupInput.vue"; +// export { default as InputGroupText } from "./InputGroupText.vue"; +// export { default as InputGroupTextarea } from "./InputGroupTextarea.vue"; export const inputGroupAddonVariants = cva( "text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50", diff --git a/ui/src/components/ui/input/Input.vue b/ui/src/components/ui/input/Input.vue index dc16a12..64deba9 100644 --- a/ui/src/components/ui/input/Input.vue +++ b/ui/src/components/ui/input/Input.vue @@ -22,7 +22,7 @@ const modelValue = useVModel(props, "modelValue", emits, { data-slot="input" :class=" cn( - 'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm', + 'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm', 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]', 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive', props.class, diff --git a/ui/src/pages/Application.vue b/ui/src/pages/Application.vue index 428d52c..e4c8d88 100644 --- a/ui/src/pages/Application.vue +++ b/ui/src/pages/Application.vue @@ -20,6 +20,7 @@ const decisionDate = ref(null); const submitDate = ref(null); const loading = ref(true); const member_name = ref(); +const notFound = ref(false); const props = defineProps<{ mode?: "create" | "view-self" | "view-recruiter" | "view-self-id" @@ -29,6 +30,11 @@ const finalMode = ref<"create" | "view-self" | "view-recruiter" | "view-self-id" function loadData(raw: ApplicationFull) { + if (!raw) { + notFound.value = true; + return; + } + const data = raw.application; appID.value = data.id; @@ -129,6 +135,10 @@ async function handleDeny(id) {
You do not have permission to view this application.
+
+ Looks like you dont have an application, reach out to the administration team if you believe this is an + error. +
@@ -181,8 +191,7 @@ async function handleDeny(id) {
-
- +
\ No newline at end of file diff --git a/ui/src/pages/ManageRoles.vue b/ui/src/pages/ManageRoles.vue index cf437ff..492610a 100644 --- a/ui/src/pages/ManageRoles.vue +++ b/ui/src/pages/ManageRoles.vue @@ -9,7 +9,7 @@ import { CardTitle, } from '@/components/ui/card' import { onMounted, ref, computed, reactive, watch } from 'vue'; -import { addMemberToRole, createRole, deleteRole, getRoles, removeMemberFromRole, Role } from '@/api/roles'; +import { addMemberToRole, createRole, deleteRole, getRoles, removeMemberFromRole } from '@/api/roles'; import Badge from '@/components/ui/badge/Badge.vue'; import { Dialog, @@ -34,8 +34,11 @@ import { Plus, X } from 'lucide-vue-next'; import Separator from '@/components/ui/separator/Separator.vue'; import Input from '@/components/ui/input/Input.vue'; import Label from '@/components/ui/label/Label.vue'; -import { getMembers } from '@/api/member'; -import { Member } from '@shared/types/member'; +import { getAllLightMembers, getMembers } from '@/api/member'; +import { Member, MemberLight } from '@shared/types/member'; +import { Role } from '@shared/types/roles'; +import RoleView from '@/components/roles/roleView.vue'; +import { useRoute } from 'vue-router'; const roles = ref([]) const activeRole = ref(null) @@ -43,16 +46,9 @@ const showDialog = ref(false); const showCreateGroupDialog = ref(false); const addingMember = ref(false); const memberToAdd = ref(null); +const route = useRoute(); -const allMembers = ref([]) -const availableMembers = computed(() => { - if (!activeRole.value) return []; - return allMembers.value.filter( - member => !activeRole.value!.members.some( - roleMember => roleMember.member_id === member.member_id - ) - ); -}) +const allMembers = ref([]) type RoleDraft = { name: string @@ -117,141 +113,40 @@ async function handleCreateGroup() { } } -async function handleAddMember() { - //guard - if (memberToAdd.value == null) - return; - await addMemberToRole(memberToAdd.value.member_id, activeRole.value.id); - roles.value = await getRoles(); -} - -async function handleRemoveMember(memberId: number) { - removeMemberFromRole(memberId, activeRole.value.id); - roles.value = await getRoles(); -} - -async function handleDeleteRole() { - await deleteRole(activeRole.value.id); -} onMounted(async () => { roles.value = await getRoles(); - allMembers.value = await getMembers(); + allMembers.value = await getAllLightMembers(); })