Compare commits
14 Commits
Mobile-Enh
...
1.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| b8c6590159 | |||
| 52bea200c8 | |||
| 7fff220053 | |||
| afbb771061 | |||
| cdf8f57eb5 | |||
| 3ff28de269 | |||
| f26a334487 | |||
| c14475258d | |||
| dd21d12dd5 | |||
| a4f762e793 | |||
| 5fdb0b45f0 | |||
| f58d0114eb | |||
| f4abc51198 | |||
| 7d5e9c33bf |
@@ -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));
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user