diff --git a/api/src/routes/ranks.ts b/api/src/routes/ranks.ts index 5eff733..af22a03 100644 --- a/api/src/routes/ranks.ts +++ b/api/src/routes/ranks.ts @@ -1,6 +1,7 @@ import { MemberState } from "@app/shared/types/member"; import { requireLogin, requireMemberState, requireRole } from "../middleware/auth"; -import { getAllRanks, insertMemberRank } from "../services/rankService"; +import { batchInsertMemberRank, getAllRanks, getPromotionHistorySummary, getPromotionsOnDay, insertMemberRank } from "../services/rankService"; +import { BatchPromotion, BatchPromotionMember } from '@app/shared/schemas/promotionSchema' import express = require('express'); const r = express.Router(); @@ -11,11 +12,13 @@ r.use(requireLogin) ur.use(requireLogin) //insert a new latest rank for a user -ur.post('/', [requireRole(["17th Command", "17th Administrator", "17th HQ"]), requireMemberState(MemberState.Member)], async (req, res) => { - 3 +ur.post('/', [requireRole(["17th Command", "17th Administrator", "17th HQ"]), requireMemberState(MemberState.Member)], async (req: express.Request, res: express.Response) => { try { - const change = req.body?.change; - await insertMemberRank(change.member_id, change.rank_id, change.date); + const change = req.body.promotions as BatchPromotionMember[]; + const author = req.user.id; + if (!change) res.sendStatus(400); + + await batchInsertMemberRank(change, author); res.sendStatus(201); } catch (err) { @@ -24,6 +27,31 @@ ur.post('/', [requireRole(["17th Command", "17th Administrator", "17th HQ"]), re } }); +ur.get('/', async (req: express.Request, res: express.Response) => { + try { + const promos = await getPromotionHistorySummary(); + console.log(promos); + res.status(200).json(promos); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +ur.get('/:day', async (req: express.Request, res: express.Response) => { + try { + if (!req.params.day) res.sendStatus(400); + + let day = new Date(req.params.day) + const promos = await getPromotionsOnDay(day); + console.log(promos); + res.status(200).json(promos); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Internal server error' }); + } +}) + //get all ranks r.get('/', async (req, res) => { try { diff --git a/api/src/services/rankService.ts b/api/src/services/rankService.ts index ada4ea0..08b6804 100644 --- a/api/src/services/rankService.ts +++ b/api/src/services/rankService.ts @@ -1,4 +1,8 @@ +import { BatchPromotion, BatchPromotionMember } from "@app/shared/schemas/promotionSchema"; +import { PromotionSummary } from "@app/shared/types/rank" import pool from "../db"; +import { PagedData } from "@app/shared/types/pagination"; +import { toDateTime } from "@app/shared/utils/time"; export async function getAllRanks() { const rows = await pool.query( @@ -30,3 +34,78 @@ export async function insertMemberRank(member_id: number, rank_id: number, date? await pool.query(sql, params); } + + +export async function batchInsertMemberRank(promos: BatchPromotionMember[], author: number) { + try { + var con = await pool.getConnection(); + console.log(promos); + promos.forEach(p => { + con.query(`CALL sp_update_member_rank(?, ?, ?, ?, ?, ?)`, [p.member_id, p.rank_id, author, author, "Rank Change", toDateTime(new Date(p.start_date))]) + }); + + con.commit(); + return + } catch (error) { + throw error; //pass it up + } finally { + con.release(); + } +} + +export async function getPromotionHistorySummary(page: number = 1, pageSize: number = 15): Promise> { + + const offset = (page - 1) * pageSize; + + let sql = `SELECT + DATE(start_date) AS entry_day + FROM + members_ranks + GROUP BY + entry_day + ORDER BY + entry_day DESC + LIMIT ? OFFSET ?;` + + let promoList: PromotionSummary[] = await pool.query(sql, [pageSize, offset]) as PromotionSummary[]; + + let loaCount = Number((await pool.query(`SELECT + COUNT(*) AS total_grouped_days_count + FROM + ( + SELECT DISTINCT DATE(start_date) + FROM members_ranks + ) AS grouped_days;`))[0]); + + console.log(loaCount); + let pageCount = loaCount / pageSize; + + let output: PagedData = { data: promoList, pagination: { page: page, pageSize: pageSize, total: loaCount, totalPages: pageCount } } + return output; +} + +export async function getPromotionsOnDay(day: Date): Promise { + + // Convert the Date object to a 'YYYY-MM-DD' string for the SQL filter + // This assumes pool.query is used with a database that accepts this format for comparison. + const dayString = day.toISOString().split('T')[0]; + + // SQL query to fetch all records from members_unit for the specified day + let sql = ` + SELECT + member_id, + unit_id AS rank_id, -- Using unit_id as a proxy for rank_id based on the data structure + start_date + FROM + members_unit + WHERE + DATE(start_date) = ? + ORDER BY + start_date ASC; + `; + + + let batchPromotion = await pool.query(sql, [dayString]) as BatchPromotion[]; + + return batchPromotion; +} \ No newline at end of file diff --git a/shared/schemas/promotionSchema.ts b/shared/schemas/promotionSchema.ts index 80ebaac..6c78b75 100644 --- a/shared/schemas/promotionSchema.ts +++ b/shared/schemas/promotionSchema.ts @@ -6,7 +6,6 @@ export const batchPromotionMemberSchema = z.object({ start_date: z.string().refine((val) => !isNaN(Date.parse(val)), { message: "Must be a valid date", }), - reason: z.string({ required_error: "Reason is required" }).max(50, "Reason too long"), }); export const batchPromotionSchema = z.object({ @@ -27,3 +26,7 @@ export const batchPromotionSchema = z.object({ } }); }); + + +export type BatchPromotion = z.infer; +export type BatchPromotionMember = z.infer; \ No newline at end of file diff --git a/shared/types/rank.ts b/shared/types/rank.ts index 2b68591..f08ef68 100644 --- a/shared/types/rank.ts +++ b/shared/types/rank.ts @@ -5,3 +5,7 @@ export type Rank = { category: string sortOrder: number } + +export interface PromotionSummary { + entry_day: Date; +} \ No newline at end of file diff --git a/ui/src/api/rank.ts b/ui/src/api/rank.ts index 278d100..52f8631 100644 --- a/ui/src/api/rank.ts +++ b/ui/src/api/rank.ts @@ -1,4 +1,6 @@ +import { BatchPromotion } from '@shared/schemas/promotionSchema'; import { Rank } from '@shared/types/rank' + // @ts-ignore const addr = import.meta.env.VITE_APIHOST; @@ -15,19 +17,19 @@ export async function getAllRanks(): Promise { } // Placeholder: submit a rank change -export async function submitRankChange(member_id: number, rank_id: number, date: string): Promise<{ ok: boolean }> { +export async function submitRankChange(promo: BatchPromotion) { const res = await fetch(`${addr}/memberRanks`, { method: "POST", headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ change: { member_id, rank_id, date } }), + credentials: 'include', + body: JSON.stringify(promo), }) if (res.ok) { - return { ok: true } + return } else { - console.error("Failed to submit rank change") - return { ok: false } + throw new Error("Failed to submit rank change: Server error"); } } \ No newline at end of file diff --git a/ui/src/components/promotions/promotionForm.vue b/ui/src/components/promotions/promotionForm.vue index f338718..786c6b0 100644 --- a/ui/src/components/promotions/promotionForm.vue +++ b/ui/src/components/promotions/promotionForm.vue @@ -21,29 +21,32 @@ import Button from '../ui/button/Button.vue'; import FieldError from '../ui/field/FieldError.vue'; import { getAllLightMembers } from '@/api/member'; import { Rank } from '@shared/types/rank'; -import { getAllRanks } from '@/api/rank'; +import { getAllRanks, submitRankChange } from '@/api/rank'; import { error } from 'console'; import Input from '../ui/input/Input.vue'; import Field from '../ui/field/Field.vue'; -const { handleSubmit, errors, values } = useForm({ +const { handleSubmit, errors, values, resetForm } = useForm({ validationSchema: toTypedSchema(batchPromotionSchema), validateOnMount: false, }) const submitForm = handleSubmit( - (vals) => { - console.log("VALID SUBMIT", vals); - }, - (errors) => { - console.log("INVALID SUBMIT", errors); + async (vals) => { + try { + let output = vals; + output.promotions.map(p => p.start_date = new Date(p.start_date).toISOString()) + await submitRankChange(output); + formSubmitted.value = true; + } catch (error) { + submitError.value = error; + console.error(error); + } } ); -function onSubmit(vals) { - console.log('hi') - console.log(vals); -} +const submitError = ref(null); +const formSubmitted = ref(false); const allmembers = ref([]); const allRanks = ref([]); @@ -105,157 +108,146 @@ onMounted(async () => { diff --git a/ui/src/components/promotions/promotionList.vue b/ui/src/components/promotions/promotionList.vue new file mode 100644 index 0000000..a8ccb77 --- /dev/null +++ b/ui/src/components/promotions/promotionList.vue @@ -0,0 +1,175 @@ + + diff --git a/ui/src/pages/RankChange.vue b/ui/src/pages/RankChange.vue index b320f07..f5fb314 100644 --- a/ui/src/pages/RankChange.vue +++ b/ui/src/pages/RankChange.vue @@ -1,120 +1,24 @@ \ No newline at end of file + +
+ +
+ + +
+
+ +
+
+ + + +