From 62defe5b6d701eb7fe34d87ee2927244aed5538a Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Wed, 10 Dec 2025 21:30:41 -0500 Subject: [PATCH 01/14] LOA backend refactor --- api/src/routes/loa.js | 56 ---------------------- api/src/routes/loa.ts | 80 +++++++++++++++++++++++++++++++ api/src/services/loaService.ts | 29 +++++++++++ shared/types/loa.ts | 24 ++++++++++ ui/src/api/loa.ts | 46 ++++++++++++++---- ui/src/components/loa/loaForm.vue | 3 +- 6 files changed, 172 insertions(+), 66 deletions(-) delete mode 100644 api/src/routes/loa.js create mode 100644 api/src/routes/loa.ts create mode 100644 api/src/services/loaService.ts create mode 100644 shared/types/loa.ts diff --git a/api/src/routes/loa.js b/api/src/routes/loa.js deleted file mode 100644 index c14bf24..0000000 --- a/api/src/routes/loa.js +++ /dev/null @@ -1,56 +0,0 @@ -const express = require('express'); -const router = express.Router(); - -import pool from '../db'; - -//post a new LOA -router.post("/", async (req, res) => { - const { member_id, filed_date, start_date, end_date, reason } = req.body; - - if (!member_id || !filed_date || !start_date || !end_date) { - return res.status(400).json({ error: "Missing required fields" }); - } - - try { - const result = await pool.query( - `INSERT INTO leave_of_absences - (member_id, filed_date, start_date, end_date, reason) - VALUES (?, ?, ?, ?, ?)`, - [member_id, filed_date, start_date, end_date, reason] - ); - res.sendStatus(201); - } catch (error) { - console.error(error); - res.status(500).send('Something went wrong', error); - } -}); - -//get my current LOA -router.get("/me", async (req, res) => { - //TODO: implement current user getter - const user = 89; - - try { - const result = await pool.query("SELECT * FROM leave_of_absences WHERE member_id = ?", [user]) - res.status(200).json(result) - } catch (error) { - console.error(error); - res.status(500).send(error); - } -}) - -router.get('/all', async (req, res) => { - try { - const result = await pool.query( - `SELECT loa.*, members.name - FROM leave_of_absences AS loa - INNER JOIN members ON loa.member_id = members.id; - `); - res.status(200).json(result) - } catch (error) { - console.error(error); - res.status(500).send(error); - } -}) - -module.exports = router; diff --git a/api/src/routes/loa.ts b/api/src/routes/loa.ts new file mode 100644 index 0000000..054977e --- /dev/null +++ b/api/src/routes/loa.ts @@ -0,0 +1,80 @@ +const express = require('express'); +const router = express.Router(); + +import { Request, Response } from 'express'; +import pool from '../db'; +import { createNewLOA, getAllLOA, getLoaTypes, getUserLOA } from '../services/loaService'; +import { LOARequest } from '@app/shared/types/loa'; + +//member posts LOA +router.post("/", async (req: Request, res: Response) => { + let LOARequest = req.body as LOARequest; + LOARequest.member_id = req.user.id; + LOARequest.created_by = req.user.id; + + console.log(LOARequest); + + try { + // const result = await pool.query( + // `INSERT INTO leave_of_absences + // (member_id, filed_date, start_date, end_date, reason) + // VALUES (?, ?, ?, ?, ?)`, + // [member_id, filed_date, start_date, end_date, reason] + // ); + await createNewLOA(LOARequest); + res.sendStatus(201); + } catch (error) { + console.error(error); + res.status(500).send(error); + } +}); + +//admin posts LOA +router.post("/admin", async (req: Request, res: Response) => { + let LOARequest = req.body as LOARequest; + LOARequest.created_by = req.user.id; + + console.log(LOARequest); + + try { + await createNewLOA(LOARequest); + res.sendStatus(201); + } catch (error) { + console.error(error); + res.status(500).send(error); + } +}); + +//get my current LOA +router.get("/me", async (req: Request, res: Response) => { + const user = req.user.id; + try { + const result = await getUserLOA(user); + res.status(200).json(result) + } catch (error) { + console.error(error); + res.status(500).send(error); + } +}) + +router.get('/all', async (req, res) => { + try { + const result = await getAllLOA(); + res.status(200).json(result) + } catch (error) { + console.error(error); + res.status(500).send(error); + } +}) + +router.get('/types', async (req: Request, res: Response) => { + try { + let out = await getLoaTypes(); + res.status(200).json(out); + } catch (error) { + res.status(500).json(error); + console.error(error); + } +}) + +module.exports = router; diff --git a/api/src/services/loaService.ts b/api/src/services/loaService.ts new file mode 100644 index 0000000..ae65128 --- /dev/null +++ b/api/src/services/loaService.ts @@ -0,0 +1,29 @@ +import pool from "../db"; +import { LOARequest, LOAType } from '@app/shared/types/loa' + +export async function getLoaTypes(): Promise { + return await pool.query('SELECT * FROM leave_of_absences_types;'); +} + +export async function getAllLOA(): Promise { + let res: LOARequest[] = await pool.query( + `SELECT loa.*, members.name, t.name AS type_name + FROM leave_of_absences AS loa + LEFT JOIN members ON loa.member_id = members.id + LEFT JOIN leave_of_absences_types AS t ON loa.type_id = t.id; + `) as LOARequest[]; + return res; +} + +export async function getUserLOA(userId: number): Promise { + const result: LOARequest[] = await pool.query("SELECT * FROM leave_of_absences WHERE member_id = ?", [userId]) + return result; +} + +export async function createNewLOA(data: LOARequest) { + const sql = `INSERT INTO leave_of_absences + (member_id, filed_date, start_date, end_date, type_id, reason) + VALUES (?, ?, ?, ?, ?, ?)`; + await pool.query(sql, [data.member_id, data.filed_date, data.start_date, data.end_date, data.type_id, data.reason]) + return; +} \ No newline at end of file diff --git a/shared/types/loa.ts b/shared/types/loa.ts new file mode 100644 index 0000000..c87bf87 --- /dev/null +++ b/shared/types/loa.ts @@ -0,0 +1,24 @@ +export interface LOARequest { + id?: number; + member_id?: number; + filed_date?: string; // ISO 8601 string + start_date: string; // ISO 8601 string + end_date: string; // ISO 8601 string + extended_till?: string; + type_id?: number; + reason?: string; + expired?: boolean; + closed?: boolean; + closed_by?: number; + created_by?: number; + + name?: string; //member name + type_name?: string; +}; + +export interface LOAType { + id: number; + name: string; + max_length_days: number; + extendable: boolean; +} \ No newline at end of file diff --git a/ui/src/api/loa.ts b/ui/src/api/loa.ts index 6f9314b..93168cb 100644 --- a/ui/src/api/loa.ts +++ b/ui/src/api/loa.ts @@ -1,12 +1,4 @@ -export type LOARequest = { - id?: number; - name?: string; - member_id: number; - filed_date: string; // ISO 8601 string - start_date: string; // ISO 8601 string - end_date: string; // ISO 8601 string - reason?: string; -}; +import { LOARequest, LOAType } from '@shared/types/loa' // @ts-ignore const addr = import.meta.env.VITE_APIHOST; @@ -17,6 +9,7 @@ export async function submitLOA(request: LOARequest): Promise<{ id?: number; err "Content-Type": "application/json", }, body: JSON.stringify(request), + credentials: 'include', }); if (res.ok) { @@ -26,6 +19,24 @@ export async function submitLOA(request: LOARequest): Promise<{ id?: number; err } } +export async function adminSubmitLOA(request: LOARequest): Promise<{ id?: number; error?: string }> { + const res = await fetch(`${addr}/loa/admin`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(request), + credentials: 'include', + }); + + if (res.ok) { + return res.json(); + } else { + return { error: "Failed to submit LOA" }; + } +} + + export async function getMyLOA(): Promise { const res = await fetch(`${addr}/loa/me`, { method: "GET", @@ -60,3 +71,20 @@ export function getAllLOAs(): Promise { } }); } + +export async function getLoaTypes(): Promise { + const res = await fetch(`${addr}/loa/types`, { + method: "GET", + credentials: 'include', + }); + + if (res.ok) { + const out = res.json(); + if (!out) { + return null; + } + return out; + } else { + return null; + } +} \ No newline at end of file diff --git a/ui/src/components/loa/loaForm.vue b/ui/src/components/loa/loaForm.vue index e970f0e..f234fb7 100644 --- a/ui/src/components/loa/loaForm.vue +++ b/ui/src/components/loa/loaForm.vue @@ -18,7 +18,8 @@ import { RangeCalendar } from "@/components/ui/range-calendar" import { cn } from "@/lib/utils"; import { CalendarIcon } from "lucide-vue-next" import Textarea from "@/components/ui/textarea/Textarea.vue"; -import { LOARequest, submitLOA } from "@/api/loa"; // <-- import the submit function +import { submitLOA } from "@/api/loa"; // <-- import the submit function +import { LOARequest } from "@shared/types/loa"; const members = ref([]) const currentMember = ref(null); From 92c0d657ea25115614df7db12ea4de1be0bcac69 Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Thu, 11 Dec 2025 10:25:29 -0500 Subject: [PATCH 02/14] overhauled form system to new modern form --- api/src/routes/loa.ts | 6 - shared/schemas/loaSchema.ts | 51 ++++++ ui/src/components/loa/loaForm.vue | 290 ++++++++++++++++++------------ 3 files changed, 228 insertions(+), 119 deletions(-) create mode 100644 shared/schemas/loaSchema.ts diff --git a/api/src/routes/loa.ts b/api/src/routes/loa.ts index 054977e..7bd9492 100644 --- a/api/src/routes/loa.ts +++ b/api/src/routes/loa.ts @@ -15,12 +15,6 @@ router.post("/", async (req: Request, res: Response) => { console.log(LOARequest); try { - // const result = await pool.query( - // `INSERT INTO leave_of_absences - // (member_id, filed_date, start_date, end_date, reason) - // VALUES (?, ?, ?, ?, ?)`, - // [member_id, filed_date, start_date, end_date, reason] - // ); await createNewLOA(LOARequest); res.sendStatus(201); } catch (error) { diff --git a/shared/schemas/loaSchema.ts b/shared/schemas/loaSchema.ts new file mode 100644 index 0000000..30c94c2 --- /dev/null +++ b/shared/schemas/loaSchema.ts @@ -0,0 +1,51 @@ +import * as z from "zod"; +import { LOAType } from "../types/loa"; + +export const loaTypeSchema = z.object({ + id: z.number(), + name: z.string(), + max_length_days: z.number(), +}); + +export const loaSchema = z.object({ + member_id: z.number(), + start_date: z.date(), + end_date: z.date(), + type: loaTypeSchema, + reason: z.string(), +}) + .superRefine((data, ctx) => { + const { start_date, end_date, type } = data; + + const today = new Date(); + today.setHours(0, 0, 0, 0); + + if (start_date < today) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["start_date"], + message: "Start date cannot be in the past.", + }); + } + + // 1. end > start + if (end_date <= start_date) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["end_date"], + message: "End date must be after start date.", + }); + } + + // 2. calculate max + const maxEnd = new Date(start_date); + maxEnd.setDate(maxEnd.getDate() + type.max_length_days); + + if (end_date > maxEnd) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["end_date"], + message: `This LOA type allows a maximum of ${type.max_length_days} days.`, + }); + } + }); diff --git a/ui/src/components/loa/loaForm.vue b/ui/src/components/loa/loaForm.vue index f234fb7..ec8cfd7 100644 --- a/ui/src/components/loa/loaForm.vue +++ b/ui/src/components/loa/loaForm.vue @@ -1,13 +1,15 @@ \ No newline at end of file From dd472a5283a3739a7263597d077a192041e912d5 Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Thu, 11 Dec 2025 11:00:02 -0500 Subject: [PATCH 03/14] updated datepicker dependency --- ui/src/components/loa/loaForm.vue | 19 +- ui/src/components/ui/calendar/Calendar.vue | 164 ++++++++++++++++-- .../components/ui/calendar/CalendarCell.vue | 2 +- .../ui/calendar/CalendarCellTrigger.vue | 2 +- .../components/ui/calendar/CalendarGrid.vue | 2 +- .../ui/calendar/CalendarGridBody.vue | 2 +- .../ui/calendar/CalendarGridHead.vue | 2 +- .../ui/calendar/CalendarGridRow.vue | 2 +- .../ui/calendar/CalendarHeadCell.vue | 4 +- .../components/ui/calendar/CalendarHeader.vue | 7 +- .../ui/calendar/CalendarHeading.vue | 2 +- .../ui/calendar/CalendarNextButton.vue | 3 +- .../ui/calendar/CalendarPrevButton.vue | 3 +- .../ui/native-select/NativeSelect.vue | 51 ++++++ .../ui/native-select/NativeSelectOptGroup.vue | 19 ++ .../ui/native-select/NativeSelectOption.vue | 19 ++ ui/src/components/ui/native-select/index.js | 3 + 17 files changed, 275 insertions(+), 31 deletions(-) create mode 100644 ui/src/components/ui/native-select/NativeSelect.vue create mode 100644 ui/src/components/ui/native-select/NativeSelectOptGroup.vue create mode 100644 ui/src/components/ui/native-select/NativeSelectOption.vue create mode 100644 ui/src/components/ui/native-select/index.js diff --git a/ui/src/components/loa/loaForm.vue b/ui/src/components/loa/loaForm.vue index ec8cfd7..f49407d 100644 --- a/ui/src/components/loa/loaForm.vue +++ b/ui/src/components/loa/loaForm.vue @@ -10,8 +10,9 @@ import { fromDate, getLocalTimeZone, parseDate, + today, } from "@internationalized/date" -import type { DateRange } from "reka-ui" +import type { DateRange, DateValue } from "reka-ui" import type { Ref } from "vue" import Popover from "@/components/ui/popover/Popover.vue"; import PopoverTrigger from "@/components/ui/popover/PopoverTrigger.vue"; @@ -84,6 +85,11 @@ onMounted(async () => { console.log(currentMember.value); resetForm({ values: { member_id: currentMember.value?.member_id } }); }); +import type { LayoutTypes } from '@/components/ui/calendar' + +const defaultPlaceholder = today(getLocalTimeZone()) +const date = ref(today(getLocalTimeZone())) as Ref +const layout = ref('month-and-year') @@ -175,7 +181,7 @@ onMounted(async () => { @@ -204,7 +210,8 @@ onMounted(async () => { + :default-placeholder="defaultPlaceholder" layout="month-and-year"> +
@@ -233,5 +240,11 @@ onMounted(async () => {
+ +
+ +
+ \ No newline at end of file diff --git a/ui/src/components/ui/calendar/Calendar.vue b/ui/src/components/ui/calendar/Calendar.vue index e3f69e2..3d421ec 100644 --- a/ui/src/components/ui/calendar/Calendar.vue +++ b/ui/src/components/ui/calendar/Calendar.vue @@ -1,7 +1,14 @@ \ No newline at end of file From 8cdbb99d6f81287486fdaeb4b2559954e186746e Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Thu, 11 Dec 2025 11:25:09 -0500 Subject: [PATCH 05/14] implemented constraints on datepickers --- ui/src/components/loa/loaForm.vue | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/ui/src/components/loa/loaForm.vue b/ui/src/components/loa/loaForm.vue index 8186f0d..cc7f050 100644 --- a/ui/src/components/loa/loaForm.vue +++ b/ui/src/components/loa/loaForm.vue @@ -1,7 +1,7 @@