Compare commits

..

14 Commits

Author SHA1 Message Date
b8c6590159 Merge pull request 'Fix #147 prevent double clicking submit button' (#154) from #147-Training-Report-Double-Posting into main
All checks were successful
Continuous Integration / Update Development (push) Successful in 2m29s
Continuous Deployment / Update Deployment (push) Successful in 2m53s
Reviewed-on: #154
2026-01-17 10:13:26 -06:00
52bea200c8 Fix #147 prevent double clicking submit button 2026-01-17 11:14:31 -05:00
7fff220053 Merge pull request '#120-LOA-Extension-Bug' (#153) from #120-LOA-Extension-Bug into main
All checks were successful
Continuous Integration / Update Development (push) Successful in 2m40s
Reviewed-on: #153
2026-01-17 09:24:05 -06:00
afbb771061 improved full details panel 2026-01-17 10:25:18 -05:00
cdf8f57eb5 Added support for extended visuals 2026-01-17 10:25:06 -05:00
3ff28de269 Merge pull request 'removed crash causing log line' (#152) from #151-logging-crash into main
All checks were successful
Continuous Integration / Update Development (push) Successful in 2m38s
Reviewed-on: #152
2026-01-17 00:02:25 -06:00
f26a334487 removed crash causing log line 2026-01-17 01:03:30 -05:00
c14475258d Merge pull request '#134-Calendar-Upgrades' (#150) from #134-Calendar-Upgrades into main
All checks were successful
Continuous Integration / Update Development (push) Successful in 2m33s
Reviewed-on: #150
2026-01-16 18:37:02 -06:00
dd21d12dd5 Merge branch 'main' into #134-Calendar-Upgrades 2026-01-16 18:36:54 -06:00
a4f762e793 Merge pull request 'Promotions-Fixes' (#149) from Promotions-Fixes into main
Some checks failed
Continuous Integration / Update Development (push) Has been cancelled
Reviewed-on: #149
2026-01-16 18:36:38 -06:00
5fdb0b45f0 Improved spacing on attendees list to reduce panel width issues as mentioned in #134 2026-01-15 20:34:20 -05:00
f58d0114eb Mobile UX improvements for calendar 2026-01-15 20:22:31 -05:00
f4abc51198 Tweaked banner width because it was annoying me 2026-01-15 20:00:03 -05:00
7d5e9c33bf Fixed calendar layout reactivity issue 2026-01-15 19:57:29 -05:00
10 changed files with 138 additions and 321 deletions

View File

@@ -151,6 +151,7 @@ router.get('/callback', (req, res, next) => {
router.get('/logout', [requireLogin], function (req, res, next) { router.get('/logout', [requireLogin], function (req, res, next) {
req.logout(function (err) { req.logout(function (err) {
if (err) { return next(err); } if (err) { return next(err); }
req.session.destroy((err) => { req.session.destroy((err) => {
@@ -168,10 +169,6 @@ router.get('/logout', [requireLogin], function (req, res, next) {
returnTo: process.env.CLIENT_URL returnTo: process.env.CLIENT_URL
}; };
logger.info('auth', `Member logged out`, {
user: req.user.id,
});
res.redirect(process.env.AUTH_END_SESSION_URI + '?' + querystring.stringify(params)); res.redirect(process.env.AUTH_END_SESSION_URI + '?' + querystring.stringify(params));
}) })
}); });

View File

@@ -29,12 +29,12 @@ const version = import.meta.env.VITE_APPLICATION_VERSION;
background-position: center;"> background-position: center;">
<div class="sticky top-0 bg-background z-50"> <div class="sticky top-0 bg-background z-50">
<Navbar class="flex"></Navbar> <Navbar class="flex"></Navbar>
<Alert v-if="environment == 'dev'" class="m-2 mx-auto w-5xl" variant="info"> <Alert v-if="environment == 'dev'" class="m-2 mx-auto max-w-5xl" variant="info">
<AlertDescription class="flex flex-row items-center text-nowrap gap-5 mx-auto"> <AlertDescription class="flex flex-row items-center text-wrap gap-5 mx-auto">
<p>Development environment (v{{ version }}). Features may be incomplete or unavailable.</p> <p>Development environment (v{{ version }}). Features may be incomplete or unavailable.</p>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
<Alert v-if="userStore.user?.LOAs?.[0]" class="m-2 mx-auto w-5xl" variant="info"> <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"> <AlertDescription class="flex flex-row items-center text-nowrap gap-5 mx-auto">
<p>You are on LOA until <strong>{{ formatDate(userStore.user?.LOAs?.[0].extended_till || <p>You are on LOA until <strong>{{ formatDate(userStore.user?.LOAs?.[0].extended_till ||
userStore.user?.LOAs?.[0].end_date) }}</strong></p> userStore.user?.LOAs?.[0].end_date) }}</strong></p>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { RouterLink, useRouter } from 'vue-router'; import { RouterLink } from 'vue-router';
import Separator from '../ui/separator/Separator.vue'; import Separator from '../ui/separator/Separator.vue';
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
import { import {
@@ -18,10 +18,9 @@ import NavigationMenuTrigger from '../ui/navigation-menu/NavigationMenuTrigger.v
import NavigationMenuContent from '../ui/navigation-menu/NavigationMenuContent.vue'; import NavigationMenuContent from '../ui/navigation-menu/NavigationMenuContent.vue';
import { navigationMenuTriggerStyle } from '../ui/navigation-menu/' import { navigationMenuTriggerStyle } from '../ui/navigation-menu/'
import { useAuth } from '@/composables/useAuth'; import { useAuth } from '@/composables/useAuth';
import { ArrowUpRight, ChevronDown, ChevronUp, CircleArrowOutUpRight, LogIn, LogOut, Menu, Settings, X } from 'lucide-vue-next'; import { ArrowUpRight, CircleArrowOutUpRight } from 'lucide-vue-next';
import DropdownMenuGroup from '../ui/dropdown-menu/DropdownMenuGroup.vue'; import DropdownMenuGroup from '../ui/dropdown-menu/DropdownMenuGroup.vue';
import DropdownMenuSeparator from '../ui/dropdown-menu/DropdownMenuSeparator.vue'; import DropdownMenuSeparator from '../ui/dropdown-menu/DropdownMenuSeparator.vue';
import { computed, nextTick, ref } from 'vue';
const userStore = useUserStore(); const userStore = useUserStore();
const auth = useAuth(); const auth = useAuth();
@@ -41,135 +40,11 @@ function blurAfter() {
(document.activeElement as HTMLElement)?.blur(); (document.activeElement as HTMLElement)?.blur();
}); });
} }
type NavItem = {
title: string;
to?: string;
href?: string;
status?: 'member' | 'guest';
isExternal?: boolean;
roles?: string[];
items?: NavItem[];
};
const navConfig: NavItem[] = [
{
title: 'Calendar',
to: '/calendar',
},
{
title: 'Documents',
href: 'https://docs.iceberg-gaming.com',
status: 'member',
isExternal: true
},
{
title: 'Forms',
status: 'member',
items: [
{ title: 'Leave of Absence', to: '/loa' },
{ title: 'Training Report', to: '/trainingReport' },
]
},
{
title: 'Administration',
status: 'member',
roles: ['17th Administrator', '17th HQ', '17th Command', 'Recruiter'],
items: [
{
title: 'Leave of Absence',
to: '/administration/loa',
roles: ['17th Administrator', '17th HQ', '17th Command']
},
{
title: 'Promotions',
to: '/administration/rankChange',
roles: ['17th Administrator', '17th HQ', '17th Command']
},
{
title: 'Recruitment',
to: '/administration/applications',
roles: ['Recruiter']
},
{
title: 'Role Management',
to: '/administration/roles',
roles: ['17th Administrator']
},
]
},
{
title: 'Join',
to: '/join',
status: 'guest',
},
];
const filteredNav = computed(() => {
return navConfig.flatMap(item => {
const filtered: NavItem[] = [];
// 1. Check Login Requirements
const isLoggedIn = userStore.isLoggedIn;
// 2. Determine visibility based on status
let shouldShow = false;
if (!item.status) {
// Public items - always show
shouldShow = true;
} else if (item.status === 'guest') {
// Show if NOT logged in OR logged in as guest (but NOT a member)
shouldShow = !isLoggedIn || auth.accountStatus.value === 'guest';
} else if (item.status === 'member') {
// Show ONLY if logged in as member
shouldShow = isLoggedIn && auth.accountStatus.value === 'member';
}
// 3. Check Role Requirements (if status check passed)
if (shouldShow && item.roles) {
shouldShow = auth.hasAnyRole(item.roles);
}
if (shouldShow) {
if (item.items) {
const filteredItems = item.items.filter(subItem =>
!subItem.roles || auth.hasAnyRole(subItem.roles)
);
filtered.push({ ...item, items: filteredItems });
} else {
filtered.push(item);
}
}
return filtered;
});
})
const isMobileMenuOpen = ref(false);
const expandedMenu = ref(null);
const router = useRouter();
function openMobileMenu() {
expandedMenu.value = null;
isMobileMenuOpen.value = true;
}
function closeMobileMenu() {
isMobileMenuOpen.value = false;
expandedMenu.value = null;
}
function mobileNavigateTo(to: string) {
closeMobileMenu();
router.push(to);
}
</script> </script>
<template> <template>
<div class="w-full border-b"> <div class="w-full border-b">
<div class="hidden lg:flex max-w-screen-3xl w-full mx-auto items-center justify-between pr-10 pl-7"> <div class="max-w-screen-3xl w-full mx-auto flex items-center justify-between pr-10 pl-7">
<!-- left side --> <!-- left side -->
<div class="flex items-center gap-7"> <div class="flex items-center gap-7">
<RouterLink to="/"> <RouterLink to="/">
@@ -321,109 +196,6 @@ function mobileNavigateTo(to: string) {
<a v-else :href="APIHOST + '/login'">Login</a> <a v-else :href="APIHOST + '/login'">Login</a>
</div> </div>
</div> </div>
<!-- <Separator></Separator> -->
<!-- mobile navigation -->
<div class="flex flex-col lg:hidden w-full" :class="isMobileMenuOpen ? 'h-screen' : ''">
<div class="flex items-center justify-between w-full p-2">
<!-- <RouterLink to="/">
<img src="/17RBN_Logo.png" class="w-10 h-10 object-contain"></img>
</RouterLink> -->
<button @click="mobileNavigateTo('/')">
<img src="/17RBN_Logo.png" class="w-10 h-10 object-contain"></img>
</button>
<Button v-if="!isMobileMenuOpen" variant="ghost" size="icon" @click="openMobileMenu()">
<Menu class="size-7" />
</Button>
<Button v-else variant="ghost" size="icon" @click="closeMobileMenu()">
<X class="size-7" />
</Button>
</div>
<div v-if="isMobileMenuOpen" class="flex flex-col h-[calc(100vh-60px)] overflow-hidden">
<div class="flex-1 overflow-y-auto px-2 py-3 space-y-0.5">
<div v-for="navItem in filteredNav" :key="navItem.title" class="group">
<template v-if="!navItem.items">
<a v-if="navItem.isExternal" :href="navItem.href" target="_blank"
class="flex items-center justify-between w-full px-4 py-2.5 text-base font-medium rounded-md hover:bg-accent active:bg-accent transition-colors">
<span class="flex items-center gap-2">
{{ navItem.title }}
<ArrowUpRight class="h-3.5 w-3.5 opacity-50" />
</span>
</a>
<button v-else @click="mobileNavigateTo(navItem.to)"
class="w-full text-left px-4 py-2.5 text-base font-medium rounded-md hover:bg-accent active:bg-accent transition-colors">
{{ navItem.title }}
</button>
</template>
<div v-else class="space-y-0.5">
<button @click="expandedMenu = expandedMenu === navItem.title ? null : navItem.title"
class="flex items-center justify-between w-full px-4 py-2.5 text-base font-medium rounded-md transition-colors"
:class="expandedMenu === navItem.title ? 'bg-accent/50 text-primary' : 'hover:bg-accent'">
{{ navItem.title }}
<ChevronDown class="h-4 w-4 transition-transform duration-200"
:class="expandedMenu === navItem.title ? 'rotate-180' : ''" />
</button>
<div v-if="expandedMenu === navItem.title"
class="ml-4 mr-2 border-l border-border space-y-0.5">
<button v-for="subNavItem in navItem.items" :key="subNavItem.title"
@click="mobileNavigateTo(subNavItem.to)"
class="w-full text-left px-6 py-2 text-sm text-muted-foreground hover:text-foreground active:text-primary transition-colors">
{{ subNavItem.title }}
</button>
</div>
</div>
</div>
</div>
<div class="p-3 border-t bg-background mt-auto">
<div v-if="userStore.isLoggedIn" class="space-y-3">
<div class="flex items-center justify-between px-2">
<div class="flex gap-3">
<!-- <div
class="size-8 rounded-full bg-primary/10 flex items-center justify-center text-xs font-bold text-primary">
{{ userStore.displayName?.charAt(0) }}
</div> -->
<div class="flex flex-col leading-tight">
<span class="text-sm font-semibold">
{{ userStore.displayName || userStore.user.member.member_name }}
</span>
<span v-if="userStore.displayName"
class="text-[10px] uppercase tracking-wider text-muted-foreground">
{{ userStore.user.member.member_name }}
</span>
</div>
</div>
<div class="flex items-center gap-3">
<Button variant="ghost" size="icon" @click="mobileNavigateTo('/profile')">
<Settings class="size-6"></Settings>
</Button>
<Button variant="ghost" size="icon" @click="logout()">
<LogOut class="size-6 text-destructive"></LogOut>
</Button>
</div>
</div>
<div class="flex gap-2">
<!-- <Button variant="outline" size="xs" class="flex-1 h-8 text-xs"
@click="mobileNavigateTo('/profile')">Profile</Button> -->
<Button variant="secondary" size="xs" class="flex-1 h-8 text-xs"
@click="mobileNavigateTo('/join')">My
Application</Button>
<Button variant="secondary" size="xs" class="flex-1 h-8 text-xs"
@click="mobileNavigateTo('/applications')">Application History</Button>
</div>
</div>
<a v-else :href="APIHOST + '/login'" class="block">
<Button class="w-full text-sm h-10">
<LogIn></LogIn> Login
</Button>
</a>
</div>
</div>
</div>
</div> </div>
</template> </template>

View File

@@ -31,9 +31,14 @@ watch(
{ immediate: true } { immediate: true }
); );
watch(loaded, (value) => {
if (value) emit('load');
});
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'close'): void (e: 'close'): void
(e: 'reload'): void (e: 'reload'): void
(e: 'load'): void
(e: 'edit', event: CalendarEvent): void (e: 'edit', event: CalendarEvent): void
}>() }>()
@@ -179,7 +184,7 @@ defineExpose({ forceReload })
<template> <template>
<div v-if="loaded"> <div v-if="loaded">
<!-- Header --> <!-- Header -->
<div class="flex items-center justify-between gap-3 border-b px-4 py-3 "> <div class="flex items-center justify-between gap-3 border-b border-border px-4 py-3 ">
<h2 class="text-lg font-semibold break-after-all"> <h2 class="text-lg font-semibold break-after-all">
{{ activeEvent?.name || 'Event' }} {{ activeEvent?.name || 'Event' }}
</h2> </h2>
@@ -227,14 +232,14 @@ defineExpose({ forceReload })
</div> </div>
</section> </section>
<section v-if="isPast && userStore.state === 'member'" class="w-full"> <section v-if="isPast && userStore.state === 'member'" class="w-full">
<ButtonGroup class="flex w-full"> <ButtonGroup class="flex w-full justify-center">
<Button variant="outline" <Button variant="outline" class="flex-1"
:class="myAttendance?.status === CalendarAttendance.Attending ? 'border-2 border-primary text-primary' : ''" :class="myAttendance?.status === CalendarAttendance.Attending ? 'border-2 border-primary text-primary' : ''"
@click="setAttendance(CalendarAttendance.Attending)">Going</Button> @click="setAttendance(CalendarAttendance.Attending)">Going</Button>
<Button variant="outline" <Button variant="outline" class="flex-1"
:class="myAttendance?.status === CalendarAttendance.Maybe ? 'border-2 !border-l-2 border-primary text-primary' : ''" :class="myAttendance?.status === CalendarAttendance.Maybe ? 'border-2 !border-l-2 border-primary text-primary' : ''"
@click="setAttendance(CalendarAttendance.Maybe)">Maybe</Button> @click="setAttendance(CalendarAttendance.Maybe)">Maybe</Button>
<Button variant="outline" <Button variant="outline" class="flex-1"
:class="myAttendance?.status === CalendarAttendance.NotAttending ? 'border-2 !border-l-2 border-primary text-primary' : ''" :class="myAttendance?.status === CalendarAttendance.NotAttending ? 'border-2 !border-l-2 border-primary text-primary' : ''"
@click="setAttendance(CalendarAttendance.NotAttending)">Declined</Button> @click="setAttendance(CalendarAttendance.NotAttending)">Declined</Button>
</ButtonGroup> </ButtonGroup>
@@ -259,7 +264,7 @@ defineExpose({ forceReload })
<!-- Description --> <!-- Description -->
<section class="space-y-2 w-full"> <section class="space-y-2 w-full">
<p class="text-lg font-semibold">Description</p> <p class="text-lg font-semibold">Description</p>
<p class="border bg-muted/50 px-3 py-2 rounded-lg min-h-24 my-2 whitespace-pre-line"> <p class="border border-border bg-muted/50 px-3 py-2 rounded-lg min-h-24 my-2 whitespace-pre-line">
{{ activeEvent.description }} {{ activeEvent.description }}
</p> </p>
</section> </section>
@@ -273,8 +278,8 @@ defineExpose({ forceReload })
<p>Declined <span class="ml-1">{{ attendanceStatusSummary.notAttending }}</span></p> <p>Declined <span class="ml-1">{{ attendanceStatusSummary.notAttending }}</span></p>
</div> --> </div> -->
</div> </div>
<div class="flex flex-col border bg-muted/50 rounded-lg min-h-24 my-2"> <div class="flex flex-col border border-border bg-muted/50 rounded-lg min-h-24 my-2">
<div class="flex w-full pt-2 border-b *:w-full *:text-center *:pb-1 *:cursor-pointer"> <div class="flex w-full pt-2 border-b border-border *:w-full *:text-center *:pb-1 *:cursor-pointer">
<label :class="attendanceTab === 'Alpha' ? 'border-b-3 border-foreground' : 'mb-[2px]'" <label :class="attendanceTab === 'Alpha' ? 'border-b-3 border-foreground' : 'mb-[2px]'"
@click="attendanceTab = 'Alpha'">Alpha {{ attendanceCountsByGroup.Alpha }}</label> @click="attendanceTab = 'Alpha'">Alpha {{ attendanceCountsByGroup.Alpha }}</label>
<label :class="attendanceTab === 'Echo' ? 'border-b-3 border-foreground' : 'mb-[2px]'" <label :class="attendanceTab === 'Echo' ? 'border-b-3 border-foreground' : 'mb-[2px]'"
@@ -283,14 +288,14 @@ defineExpose({ forceReload })
@click="attendanceTab = 'Other'">Other {{ attendanceCountsByGroup.Other }}</label> @click="attendanceTab = 'Other'">Other {{ attendanceCountsByGroup.Other }}</label>
</div> </div>
<div class="pb-1 min-h-48"> <div class="pb-1 min-h-48">
<div class="grid grid-cols-2 font-semibold text-muted-foreground border-b py-1 px-3 mb-2"> <div class="grid grid-cols-2 font-semibold text-muted-foreground border-b border-border py-1 px-3 mb-2">
<p>Name</p> <p>Name</p>
<p class="text-right">Status</p> <p class="text-right">Status</p>
</div> </div>
<div v-for="person in attendanceList" :key="person.member_id" <div v-for="person in attendanceList" :key="person.member_id"
class="grid grid-cols-2 py-1 *:px-3 hover:bg-muted"> class="grid grid-cols-3 py-1 *:px-3 hover:bg-muted">
<div> <div class="col-span-2">
<MemberCard :member-id="person.member_id"></MemberCard> <MemberCard :member-id="person.member_id"></MemberCard>
</div> </div>
<p :class="statusColor(person.status)" class="text-right"> <p :class="statusColor(person.status)" class="text-right">
@@ -302,11 +307,14 @@ defineExpose({ forceReload })
</section> </section>
</div> </div>
</div> </div>
<div v-else class="flex justify-center h-full items-center"> <div v-else class="relative flex justify-center items-center h-full">
<Button variant="ghost" size="icon" @click="emit('close')"> <!-- Close button (top-right) -->
<Button variant="ghost" size="icon" class="absolute top-2 right-2" @click="emit('close')">
<X class="size-5" /> <X class="size-5" />
</Button> </Button>
<Spinner class="size-8"></Spinner> <!-- Spinner (centered) -->
<Spinner class="size-8" />
</div> </div>
</template> </template>

View File

@@ -75,15 +75,17 @@ function formatDate(date: Date): string {
}); });
} }
function loaStatus(loa: LOARequest): "Upcoming" | "Active" | "Overdue" | "Closed" { function loaStatus(loa: LOARequest): "Upcoming" | "Active" | "Extended" | "Overdue" | "Closed" {
if (loa.closed) return "Closed"; if (loa.closed) return "Closed";
const now = new Date(); const now = new Date();
const start = new Date(loa.start_date); const start = new Date(loa.start_date);
const end = new Date(loa.end_date); const end = new Date(loa.end_date);
const extension = new Date(loa.extended_till);
if (now < start) return "Upcoming"; if (now < start) return "Upcoming";
if (now >= start && now <= end) return "Active"; if (now >= start && (now <= end)) return "Active";
if (now >= start && (now <= extension)) return "Extended";
if (now > loa.extended_till || end) return "Overdue"; if (now > loa.extended_till || end) return "Overdue";
return "Overdue"; // fallback return "Overdue"; // fallback
@@ -191,6 +193,7 @@ function setPage(pagenum: number) {
<TableCell> <TableCell>
<Badge v-if="loaStatus(post) === 'Upcoming'" class="bg-blue-400">Upcoming</Badge> <Badge v-if="loaStatus(post) === 'Upcoming'" class="bg-blue-400">Upcoming</Badge>
<Badge v-else-if="loaStatus(post) === 'Active'" class="bg-green-500">Active</Badge> <Badge v-else-if="loaStatus(post) === 'Active'" class="bg-green-500">Active</Badge>
<Badge v-else-if="loaStatus(post) === 'Extended'" class="bg-green-500">Extended</Badge>
<Badge v-else-if="loaStatus(post) === 'Overdue'" class="bg-yellow-400">Overdue</Badge> <Badge v-else-if="loaStatus(post) === 'Overdue'" class="bg-yellow-400">Overdue</Badge>
<Badge v-else class="bg-gray-400">Ended</Badge> <Badge v-else class="bg-gray-400">Ended</Badge>
</TableCell> </TableCell>
@@ -232,11 +235,34 @@ function setPage(pagenum: number) {
<TableRow v-if="expanded === post.id" @mouseenter="hoverID = post.id" <TableRow v-if="expanded === post.id" @mouseenter="hoverID = post.id"
@mouseleave="hoverID = null" :class="{ 'bg-muted/50 border-t-0': hoverID === post.id }"> @mouseleave="hoverID = null" :class="{ 'bg-muted/50 border-t-0': hoverID === post.id }">
<TableCell :colspan="8" class="p-0"> <TableCell :colspan="8" class="p-0">
<div class="w-full p-3 mb-6 space-y-3"> <div class="w-full p-4 mb-6 space-y-4">
<div class="flex justify-between items-start gap-4">
<div class="space-y-3 w-full">
<!-- Header --> <!-- Dates -->
<div class="grid grid-cols-3 gap-4 text-sm">
<div>
<p class="text-muted-foreground">Start</p>
<p class="font-medium">
{{ formatDate(post.start_date) }}
</p>
</div>
<div>
<p class="text-muted-foreground">Original end</p>
<p class="font-medium">
{{ formatDate(post.end_date) }}
</p>
</div>
<div class="">
<p class="text-muted-foreground">Extended to</p>
<p class="font-medium text-foreground">
{{post.extended_till ? formatDate(post.extended_till) : 'N/A' }}
</p>
</div>
</div>
<!-- Reason -->
<div class="space-y-2">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h4 class="text-sm font-semibold text-foreground"> <h4 class="text-sm font-semibold text-foreground">
Reason Reason
@@ -244,16 +270,13 @@ function setPage(pagenum: number) {
<Separator class="flex-1" /> <Separator class="flex-1" />
</div> </div>
<!-- Content -->
<div <div
class="rounded-lg border bg-muted/40 px-4 py-3 text-sm leading-relaxed whitespace-pre-wrap text-muted-foreground w-full"> class="rounded-lg border bg-muted/40 px-4 py-3 text-sm leading-relaxed whitespace-pre-wrap text-muted-foreground">
{{ post.reason || 'No reason provided.' }} {{ post.reason || 'No reason provided.' }}
</div> </div>
</div> </div>
</div> </div>
</div>
</TableCell> </TableCell>
</TableRow> </TableRow>

View File

@@ -129,7 +129,11 @@ function setAllToday() {
<div class="w-xl"> <div class="w-xl">
<form v-if="!formSubmitted" id="trainingForm" @submit.prevent="submitForm" <form v-if="!formSubmitted" id="trainingForm" @submit.prevent="submitForm"
class="w-full min-w-0 flex flex-col gap-4"> class="w-full min-w-0 flex flex-col gap-4">
<div>
<FieldLegend class="scroll-m-20 text-2xl font-semibold tracking-tight">
Promotion Form
</FieldLegend>
</div>
<VeeFieldArray name="promotions" v-slot="{ fields, push, remove }"> <VeeFieldArray name="promotions" v-slot="{ fields, push, remove }">
<FieldSet class="w-full min-w-0"> <FieldSet class="w-full min-w-0">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">

View File

@@ -26,6 +26,7 @@ import PopoverTrigger from "@/components/ui/popover/PopoverTrigger.vue";
import PopoverContent from "@/components/ui/popover/PopoverContent.vue"; import PopoverContent from "@/components/ui/popover/PopoverContent.vue";
import Combobox from '../ui/combobox/Combobox.vue' import Combobox from '../ui/combobox/Combobox.vue'
import Tooltip from '../tooltip/Tooltip.vue' import Tooltip from '../tooltip/Tooltip.vue'
import Spinner from '../ui/spinner/Spinner.vue'
const { handleSubmit, resetForm, errors, values, setFieldValue } = useForm({ const { handleSubmit, resetForm, errors, values, setFieldValue } = useForm({
@@ -67,19 +68,24 @@ function toMySQLDateTime(date: Date): string {
.replace("T", " ") + "000"; // becomes → 2025-11-19 00:00:00.000000 .replace("T", " ") + "000"; // becomes → 2025-11-19 00:00:00.000000
} }
const submitting = ref(false);
function onSubmit(vals) { async function onSubmit(vals) {
//catch double submit
if (submitting.value) return;
submitting.value = true;
try { try {
const clean: CourseEventDetails = { const clean: CourseEventDetails = {
...vals, ...vals,
event_date: new Date(vals.event_date), event_date: new Date(vals.event_date),
} }
postTrainingReport(clean).then((newID) => { await postTrainingReport(clean).then((newID) => {
emit("submit", newID); emit("submit", newID);
}); });
} catch (err) { } catch (err) {
console.error("There was an error submitting the training report", err); console.error("There was an error submitting the training report", err);
} finally {
submitting.value = false;
} }
} }
@@ -402,7 +408,12 @@ const filteredMembers = computed(() => {
</FieldGroup> </FieldGroup>
<div class="flex gap-3 justify-end"> <div class="flex gap-3 justify-end">
<Button type="button" variant="outline" @click="resetForm">Reset</Button> <Button type="button" variant="outline" @click="resetForm">Reset</Button>
<Button type="submit" form="trainingForm">Submit</Button> <Button type="submit" form="trainingForm" :disabled="submitting" class="w-35">
<span class="flex items-center gap-2" v-if="submitting">
<Spinner></Spinner> Submitting
</span>
<span v-else>Submit</span>
</Button>
</div> </div>
</form> </form>
</template> </template>

View File

@@ -16,7 +16,7 @@ export const buttonVariants = cva(
secondary: secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost: ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", "hover:bg-accent active:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
success: success:
"bg-success text-success-foreground shadow-xs hover:bg-success/90", "bg-success text-success-foreground shadow-xs hover:bg-success/90",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",

View File

@@ -155,7 +155,7 @@ onMounted(() => {
<div> <div>
<CreateCalendarEvent ref="dialogRef" @reload="loadEvents(); eventViewRef.forceReload();"></CreateCalendarEvent> <CreateCalendarEvent ref="dialogRef" @reload="loadEvents(); eventViewRef.forceReload();"></CreateCalendarEvent>
<div class="flex"> <div class="flex">
<div class="flex-1 min-h-0 mt-5"> <div class="flex-1 min-h-0 mt-5" :class="{ 'hidden md:block': panelOpen }">
<div class="h-[80vh] min-h-0"> <div class="h-[80vh] min-h-0">
<!-- calendar header --> <!-- calendar header -->
<div class="flex items-center justify-between mx-5"> <div class="flex items-center justify-between mx-5">
@@ -208,10 +208,10 @@ onMounted(() => {
</div> </div>
</div> </div>
<aside v-if="panelOpen" <aside v-if="panelOpen"
class="3xl:w-lg 2xl:w-md border-l bg-card text-foreground flex flex-col overflow-auto scrollbar-themed" class="w-screen 3xl:w-lg 2xl:w-md lg:border-l bg-card text-foreground flex flex-col overflow-auto scrollbar-themed"
:style="{ height: 'calc(100vh - 61px)', position: 'sticky', top: '64px' }"> :style="{ height: 'calc(100vh - 61px)', position: 'sticky', top: '64px' }">
<ViewCalendarEvent ref="eventViewRef" :key="currentEventID" @close="() => { router.push('/calendar'); }" <ViewCalendarEvent ref="eventViewRef" :key="currentEventID" @close="() => { router.push('/calendar'); }"
@reload="loadEvents()" @edit="(val) => { dialogRef.openDialog(null, 'edit', val) }"> @reload="loadEvents()" @load="calendarRef.getApi().updateSize()" @edit="(val) => { dialogRef.openDialog(null, 'edit', val) }">
</ViewCalendarEvent> </ViewCalendarEvent>
</aside> </aside>
</div> </div>

View File

@@ -1,48 +1,29 @@
<script setup>
import { ref } from 'vue'
import { Plus, PlusIcon, X } from 'lucide-vue-next'
import PromotionForm from '@/components/promotions/promotionForm.vue'
import PromotionList from '@/components/promotions/promotionList.vue'
import DialogContent from '@/components/ui/dialog/DialogContent.vue'
import Dialog from '@/components/ui/dialog/Dialog.vue'
import DialogTrigger from '@/components/ui/dialog/DialogTrigger.vue'
import DialogHeader from '@/components/ui/dialog/DialogHeader.vue'
import DialogTitle from '@/components/ui/dialog/DialogTitle.vue'
import Button from '@/components/ui/button/Button.vue'
const isFormOpen = ref(false)
const listRef = ref(null)
const isMobileFormOpen = ref(false);
</script>
<template> <template>
<div class="mx-auto mt-6 lg:mt-10 px-4 lg:px-8"> <div class="mx-auto mt-6 lg:mt-10 px-4 lg:px-8">
<div class="flex flex-col items-center justify-between mb-6 lg:hidden"> <div class="flex items-center justify-between mb-6 lg:hidden">
<div v-if="isMobileFormOpen">
<div class="mb-4 flex justify-between items-center">
<p class="scroll-m-20 text-2xl font-semibold tracking-tight">
Promotion Form
</p>
<Button variant="ghost" size="icon" @click="isMobileFormOpen = false">
<X v-if="isMobileFormOpen" class="size-6"></X>
</Button>
</div>
<PromotionForm @submitted="listRef?.refresh" />
</div>
<div v-else>
<div class="flex justify-between w-full">
<h1 class="text-2xl font-semibold tracking-tight">Promotion History</h1> <h1 class="text-2xl font-semibold tracking-tight">Promotion History</h1>
<Button @click="isMobileFormOpen = true">
<PlusIcon />Submit <Dialog v-model:open="isFormOpen">
<DialogTrigger as-child>
<Button size="sm" class="gap-2">
<Plus class="size-4" />
Promote
</Button> </Button>
</DialogTrigger>
<DialogContent class="w-full h-full max-w-none m-0 rounded-none flex flex-col">
<DialogHeader class="flex-row items-center justify-between border-b pb-4">
<DialogTitle>New Promotion</DialogTitle>
</DialogHeader>
<div class="flex-1 overflow-y-auto pt-6">
<PromotionForm @submitted="handleMobileSubmit" />
</div> </div>
<PromotionList></PromotionList> </DialogContent>
</div> </Dialog>
</div> </div>
<div class="hidden lg:flex flex-row lg:max-h-[70vh] gap-8"> <div class="flex flex-col lg:flex-row lg:max-h-[70vh] gap-8">
<div class="flex-1 lg:border-r lg:pr-8 w-full lg:min-w-2xl"> <div class="flex-1 lg:border-r lg:pr-8 w-full lg:min-w-2xl">
<p class="hidden lg:block scroll-m-20 text-2xl font-semibold tracking-tight mb-3"> <p class="hidden lg:block scroll-m-20 text-2xl font-semibold tracking-tight mb-3">
Promotion History Promotion History
@@ -61,3 +42,24 @@ const isMobileFormOpen = ref(false);
</div> </div>
</div> </div>
</template> </template>
<script setup>
import { ref } from 'vue'
import { Plus } from 'lucide-vue-next'
import PromotionForm from '@/components/promotions/promotionForm.vue'
import PromotionList from '@/components/promotions/promotionList.vue'
import DialogContent from '@/components/ui/dialog/DialogContent.vue'
import Dialog from '@/components/ui/dialog/Dialog.vue'
import DialogTrigger from '@/components/ui/dialog/DialogTrigger.vue'
import DialogHeader from '@/components/ui/dialog/DialogHeader.vue'
import DialogTitle from '@/components/ui/dialog/DialogTitle.vue'
import Button from '@/components/ui/button/Button.vue'
const isFormOpen = ref(false)
const listRef = ref(null)
const handleMobileSubmit = () => {
isFormOpen.value = false // Close the "Whole page" modal
listRef.value?.refresh() // Refresh the list behind it
}
</script>