2 Commits

Author SHA1 Message Date
ea52be83ef Create dossier
Some checks failed
Continuous Deployment / Update Deployment (push) Failing after 1m29s
2025-12-06 15:40:51 -06:00
9c903c9ad9 Create dossier
Some checks failed
Continuous Deployment / Update Deployment (push) Failing after 1m19s
2025-12-05 17:59:04 -06:00
16 changed files with 256 additions and 343 deletions

View File

@@ -1,8 +1,6 @@
name: Continuous Deployment name: Continuous Deployment
on: on:
push: push:
tags:
- '*'
jobs: jobs:
Deploy: Deploy:
@@ -10,29 +8,20 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
volumes: volumes:
- /var/www/html/milsim-site-v4:/var/www/html/milsim-site-v4:z - /var/www/html/milsim-site-v4:/var/www/html/milsim-site-v4:rw
steps: steps:
- name: Setup Local Environment - name: Setup Local Environment
run: | run: |
groupadd -g 989 nginx || true groupadd -g 989 nginx || true
useradd nginx -u 990 -g nginx -m || true useradd nginx -u 990 -g nginx -m || true
- name: Update Node Environment - name: Verify Node Environment
uses: actions/setup-node@v6
with:
node-version: 20.19
- name: Verify Local Environment
run: | run: |
which npm
npm -v npm -v
which node
node -v node -v
which sed
sed --version
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
ref: 'main' ref: 'main'
@@ -43,53 +32,31 @@ jobs:
cp /workspace/17th-Ranger-Battalion-ORG/milsim-site-v4/.git/config .git/config cp /workspace/17th-Ranger-Battalion-ORG/milsim-site-v4/.git/config .git/config
chown nginx:nginx .git/config chown nginx:nginx .git/config
- name: Update Application Code - name: Fix File Permissions
run: |
cd /var/www/html/milsim-site-v4
version=`git log -1 --format=%H`
echo "Current Revision: $version"
echo "Updating to: ${{ github.sha }}
sudo -u nginx git reset --hard
sudo -u nginx git pull origin main
sudo -u nginx git pull origin main
new_version=`git log -1 --format=%H`
echo "Sucessfully updated to: $new_version
- name: Update Shared Dependencies and Fix Permissions
run: |
cd /var/www/html/milsim-site-v4/shared
npm install
chown -R nginx:nginx .
- name: Update UI Dependencies and Fix Permissions
run: |
cd /var/www/html/milsim-site-v4/ui
npm install
chown -R nginx:nginx .
- name: Update API Dependencies and Fix Permissions
run: |
cd /var/www/html/milsim-site-v4/api
npm install
chown -R nginx:nginx .
- name: Build UI / Update Version / Fix Permissions
run: |
cd /var/www/html/milsim-site-v4/ui
npm run build
version=`git describe --abbrev=0 --tags`
sed -i "s/VITE_APPLICATION_VERSION=.*/VITE_APPLICATION_VERSION=$version/" .env
chown -R nginx:nginx .
- name: Build API / Update Version / Fix Permissions
run: |
cd /var/www/html/milsim-site-v4/api
npm run build
version=`git describe --abbrev=0 --tags`
sed -i "s/APPLICATION_VERSION=.*/APPLICATION_VERSION=$version/" .env
chown -R nginx:nginx .
- name: Reset File Permissions
run: | run: |
sudo chown -R nginx:nginx /var/www/html/milsim-site-v4 sudo chown -R nginx:nginx /var/www/html/milsim-site-v4
sudo chmod -R u+w /var/www/html/milsim-site-v4 sudo chmod -R u+w /var/www/html/milsim-site-v4
- name: Update Application Code
run: |
sudo -u nginx bash -c "cd /var/www/html/milsim-site-v4 && git reset --hard && git pull origin main"
- name: Update Shared Dependencies
run: |
sudo -u nginx bash -c "cd /var/www/html/milsim-site-v4/shared && npm install"
- name: Update UI Dependencies
run: |
sudo -u nginx bash -c "cd /var/www/html/milsim-site-v4/ui && npm install"
- name: Update API Dependencies
run: |
sudo -u nginx bash -c "cd /var/www/html/milsim-site-v4/api && npm install"
- name: Build UI
run: |
sudo -u nginx bash -c "cd /var/www/html/milsim-site-v4/ui && npm run build"
- name: Build API
run: |
sudo -u nginx bash -c "cd /var/www/html/milsim-site-v4/api && npm run build"

View File

@@ -1,89 +0,0 @@
name: Continuous Integration
on:
push:
branches:
- main
jobs:
Deploy:
name: Update Development
runs-on: ubuntu-latest
container:
volumes:
- /var/www/html/milsim-site-v4-dev:/var/www/html/milsim-site-v4:z
steps:
- name: Setup Local Environment
run: |
groupadd -g 989 nginx || true
useradd nginx -u 990 -g nginx -m || true
- name: Update Node Environment
uses: actions/setup-node@v6
with:
node-version: 20.19
- name: Verify Local Environment
run: |
which npm
npm -v
which node
node -v
which sed
sed --version
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
ref: 'main'
- name: Token Copy
run: |
cd /var/www/html/milsim-site-v4
cp /workspace/17th-Ranger-Battalion-ORG/milsim-site-v4/.git/config .git/config
chown nginx:nginx .git/config
- name: Update Application Code
run: |
cd /var/www/html/milsim-site-v4
sudo -u nginx git reset --hard
sudo -u nginx git pull origin main
- name: Update Shared Dependencies and Fix Permissions
run: |
cd /var/www/html/milsim-site-v4/shared
npm install
chown -R nginx:nginx .
- name: Update UI Dependencies and Fix Permissions
run: |
cd /var/www/html/milsim-site-v4/ui
npm install
chown -R nginx:nginx .
- name: Update API Dependencies and Fix Permissions
run: |
cd /var/www/html/milsim-site-v4/api
npm install
chown -R nginx:nginx .
- name: Build UI / Update Version / Fix Permissions
run: |
cd /var/www/html/milsim-site-v4/ui
npm run build
version=`git rev-parse --short=10 HEAD`
sed -i "s/VITE_APPLICATION_VERSION=.*/VITE_APPLICATION_VERSION=$version/" .env
chown -R nginx:nginx .
- name: Build API / Update Version / Fix Permissions
run: |
cd /var/www/html/milsim-site-v4/api
npm run build
version=`git rev-parse --short=10 HEAD`
sed -i "s/APPLICATION_VERSION=.*/APPLICATION_VERSION=$version/" .env
chown -R nginx:nginx .
- name: Reset File Permissions
run: |
sudo chown -R nginx:nginx /var/www/html/milsim-site-v4
sudo chmod -R u+w /var/www/html/milsim-site-v4

View File

@@ -19,8 +19,6 @@ AUTH_END_SESSION_URI=
SERVER_PORT=3000 SERVER_PORT=3000
CLIENT_URL= # This is whatever URL the client web app is served on CLIENT_URL= # This is whatever URL the client web app is served on
CLIENT_DOMAIN= #whatever.com CLIENT_DOMAIN= #whatever.com
APPLICATION_VERSION= # Should match release tag
APPLICATION_ENVIRONMENT= # dev / prod
# Glitchtip # Glitchtip
GLITCHTIP_DSN= GLITCHTIP_DSN=

View File

@@ -20,14 +20,11 @@ const port = process.env.SERVER_PORT;
//glitchtip setup //glitchtip setup
const sentry = require('@sentry/node'); const sentry = require('@sentry/node');
if (process.env.DISABLE_GLITCHTIP === "true") { if (!process.env.DISABLE_GLITCHTIP) {
console.log("Glitchtip disabled") console.log("Glitchtip disabled AAAAAA")
} else { } else {
let dsn = process.env.GLITCHTIP_DSN; let dsn = process.env.GLITCHTIP_DSN;
let release = process.env.APPLICATION_VERSION; sentry.init({ dsn: dsn });
let environment = process.env.APPLICATION_ENVIRONMENT;
console.log(release, environment)
sentry.init({ dsn: dsn, release: release, environment: environment });
console.log("Glitchtip initialized"); console.log("Glitchtip initialized");
} }
@@ -61,7 +58,6 @@ const { roles, memberRoles } = require('./routes/roles');
const { courseRouter, eventRouter } = require('./routes/course'); const { courseRouter, eventRouter } = require('./routes/course');
const { calendarRouter } = require('./routes/calendar') const { calendarRouter } = require('./routes/calendar')
const morgan = require('morgan'); const morgan = require('morgan');
const { env } = require('process');
app.use('/application', applicationsRouter); app.use('/application', applicationsRouter);
app.use('/ranks', ranks); app.use('/ranks', ranks);

View File

@@ -21,13 +21,12 @@ passport.use(new OpenIDConnectStrategy({
scope: ['openid', 'profile'] scope: ['openid', 'profile']
}, async function verify(issuer, sub, profile, jwtClaims, accessToken, refreshToken, params, cb) { }, async function verify(issuer, sub, profile, jwtClaims, accessToken, refreshToken, params, cb) {
console.log('--- OIDC verify() called ---'); // console.log('--- OIDC verify() called ---');
console.log('issuer:', issuer); // console.log('issuer:', issuer);
console.log('sub:', sub); // console.log('sub:', sub);
// console.log('profile:', JSON.stringify(profile, null, 2)); // console.log('profile:', JSON.stringify(profile, null, 2));
console.log('profile:', profile); // console.log('id_token claims:', JSON.stringify(jwtClaims, null, 2));
console.log('id_token claims:', JSON.stringify(jwtClaims, null, 2)); // console.log('preferred_username:', jwtClaims?.preferred_username);
console.log('preferred_username:', jwtClaims?.preferred_username);
const con = await pool.getConnection(); const con = await pool.getConnection();
try { try {

View File

@@ -123,9 +123,15 @@ export async function setAttendanceStatus(memberID: number, eventID: number, sta
} }
export async function getEventAttendance(eventID: number): Promise<CalendarSignup[]> { export async function getEventAttendance(eventID: number): Promise<CalendarSignup[]> {
const sql = `
SELECT
s.member_id,
s.status,
m.name AS member_name
FROM calendar_events_signups s
LEFT JOIN members m ON s.member_id = m.id
WHERE s.event_id = ?
`;
const sql = "CALL `sp_GetCalendarEventSignups`(?)" return await pool.query(sql, [eventID]);
const res = await pool.query(sql, [eventID]);
console.log(res[0]);
return res[0];
} }

View File

@@ -5,7 +5,6 @@ module.exports = {
script: 'built/api/src/index.js', script: 'built/api/src/index.js',
watch: ['.env', 'built'], watch: ['.env', 'built'],
ignore_watch: ['.gitignore', '\.json', 'src', '\.db', 'node_modules'], ignore_watch: ['.gitignore', '\.json', 'src', '\.db', 'node_modules'],
appendEnvToName: true,
watch_options: { watch_options: {
usePolling: true, usePolling: true,
interval: 10000 interval: 10000

View File

@@ -26,7 +26,6 @@ export interface CalendarSignup {
eventID: number; eventID: number;
status: CalendarAttendance; status: CalendarAttendance;
member_name?: string; member_name?: string;
member_unit?: string;
} }
export interface CalendarEventShort { export interface CalendarEventShort {

View File

@@ -1,8 +1,6 @@
# SITE SETTINGS # SITE SETTINGS
VITE_APIHOST= VITE_APIHOST=
VITE_ENVIRONMENT= # dev / prod VITE_ENVIRONMENT= # dev / prod
VITE_APPLICATION_VERSION= # Should match release tag
# Glitchtip # Glitchtip
VITE_GLITCHTIP_DSN= VITE_GLITCHTIP_DSN=

View File

@@ -156,12 +156,6 @@ function blurAfter() {
<RouterLink to="/join" @click="blurAfter">Join</RouterLink> <RouterLink to="/join" @click="blurAfter">Join</RouterLink>
</NavigationMenuLink> </NavigationMenuLink>
</NavigationMenuItem> </NavigationMenuItem>
<!-- Calendar -->
<NavigationMenuItem>
<NavigationMenuLink as-child :class="navigationMenuTriggerStyle()">
<RouterLink to="/calendar" @click="blurAfter">Calendar</RouterLink>
</NavigationMenuLink>
</NavigationMenuItem>
</NavigationMenuList> </NavigationMenuList>
</NavigationMenu> </NavigationMenu>
</div> </div>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { CalendarEvent, CalendarSignup } from '@shared/types/calendar' import type { CalendarEvent, CalendarSignup } from '@shared/types/calendar'
import { CircleAlert, Clock4, EllipsisVertical, MapPin, User, X } from 'lucide-vue-next'; import { CircleAlert, Clock, EllipsisVertical, MapPin, User, X } from 'lucide-vue-next';
import { computed, ref, watch } from 'vue'; import { computed, onMounted, ref, watch } from 'vue';
import ButtonGroup from '../ui/button-group/ButtonGroup.vue'; import ButtonGroup from '../ui/button-group/ButtonGroup.vue';
import Button from '../ui/button/Button.vue'; import Button from '../ui/button/Button.vue';
import { CalendarAttendance, getCalendarEvent, setCalendarEventAttendance, setCancelCalendarEvent } from '@/api/calendar'; import { CalendarAttendance, getCalendarEvent, setCalendarEventAttendance, setCancelCalendarEvent } from '@/api/calendar';
@@ -11,13 +11,24 @@ import DropdownMenu from '../ui/dropdown-menu/DropdownMenu.vue';
import DropdownMenuTrigger from '../ui/dropdown-menu/DropdownMenuTrigger.vue'; import DropdownMenuTrigger from '../ui/dropdown-menu/DropdownMenuTrigger.vue';
import DropdownMenuContent from '../ui/dropdown-menu/DropdownMenuContent.vue'; import DropdownMenuContent from '../ui/dropdown-menu/DropdownMenuContent.vue';
import DropdownMenuItem from '../ui/dropdown-menu/DropdownMenuItem.vue'; import DropdownMenuItem from '../ui/dropdown-menu/DropdownMenuItem.vue';
import { Calendar } from 'lucide-vue-next';
const route = useRoute(); const route = useRoute();
// const eventID = computed(() => {
// const id = route.params.id;
// if (typeof id === 'string') return id;
// return undefined;
// });
const loaded = ref<boolean>(false); const loaded = ref<boolean>(false);
const activeEvent = ref<CalendarEvent | null>(null); const activeEvent = ref<CalendarEvent | null>(null);
// onMounted(async () => {
// let eventID = route.params.id;
// console.log(eventID);
// activeEvent.value = await getCalendarEvent(Number(eventID));
// loaded.value = true;
// });
watch( watch(
() => route.params.id, () => route.params.id,
async (id) => { async (id) => {
@@ -34,27 +45,23 @@ const emit = defineEmits<{
(e: 'edit', event: CalendarEvent): void (e: 'edit', event: CalendarEvent): void
}>() }>()
const dateFmt = new Intl.DateTimeFormat(undefined, { // const activeEvent = computed(() => props.event)
weekday: 'long', month: 'short', day: 'numeric',
})
const timeFmt = new Intl.DateTimeFormat(undefined, { const startFmt = new Intl.DateTimeFormat(undefined, {
weekday: 'short', year: 'numeric', month: 'short', day: 'numeric',
hour: 'numeric', minute: '2-digit'
})
const endFmt = new Intl.DateTimeFormat(undefined, {
hour: 'numeric', minute: '2-digit' hour: 'numeric', minute: '2-digit'
}) })
const dateText = computed(() => { const whenText = computed(() => {
let start = dateFmt.format(new Date(activeEvent.value.start)); if (!activeEvent.value?.start) return ''
let end = dateFmt.format(new Date(activeEvent.value.end)); const s = new Date(activeEvent.value.start)
if (start === end) const e = activeEvent.value?.end ? new Date(activeEvent.value.end) : null
return start; return e
else ? `${startFmt.format(s)} ${endFmt.format(e)}`
return `${start} - ${end}`; : `${startFmt.format(s)}`
})
const timeText = computed(() => {
let startTime = timeFmt.format(new Date(activeEvent.value.start))
let endTime = timeFmt.format(new Date(activeEvent.value.end))
return [startTime, endTime]
}) })
const attending = computed<CalendarSignup[]>(() => { return activeEvent.value.eventSignups.filter((s) => s.status == CalendarAttendance.Attending) }) const attending = computed<CalendarSignup[]>(() => { return activeEvent.value.eventSignups.filter((s) => s.status == CalendarAttendance.Attending) })
@@ -64,7 +71,6 @@ const viewedState = ref<CalendarAttendance>(CalendarAttendance.Attending);
let user = useUserStore(); let user = useUserStore();
const myAttendance = computed<CalendarSignup | null>(() => { const myAttendance = computed<CalendarSignup | null>(() => {
if (!user.isLoggedIn) return null;
return activeEvent.value.eventSignups.find( return activeEvent.value.eventSignups.find(
(s) => s.member_id === user.user.id (s) => s.member_id === user.user.id
) || null; ) || null;
@@ -77,7 +83,6 @@ async function setAttendance(state: CalendarAttendance) {
} }
const canEditEvent = computed(() => { const canEditEvent = computed(() => {
if (!user.isLoggedIn) return false;
if (user.user.id == activeEvent.value.creator_id) if (user.user.id == activeEvent.value.creator_id)
return true; return true;
}); });
@@ -92,94 +97,17 @@ async function forceReload() {
activeEvent.value = await getCalendarEvent(activeEvent.value.id); activeEvent.value = await getCalendarEvent(activeEvent.value.id);
} }
const isPast = computed(() => { defineExpose({forceReload})
const end = new Date(activeEvent.value.end)
// is current date later than end date
return new Date() < end;
})
const attendanceTab = ref<"Alpha" | "Echo" | "Other">("Alpha");
const attendanceList = computed<CalendarSignup[]>(() => {
let out: CalendarSignup[] = [];
if (attendanceTab.value === 'Alpha') {
out = activeEvent.value.eventSignups?.filter((s) => s.member_unit === 'Alpha Company');
} else if (attendanceTab.value === 'Echo') {
out = activeEvent.value.eventSignups?.filter((s) => s.member_unit === 'Echo Company')
} else {
out = activeEvent.value.eventSignups?.filter((s) => s.member_unit != 'Alpha Company' && s.member_unit != 'Echo Company')
}
const statusOrder: Record<CalendarAttendance, number> = {
[CalendarAttendance.Attending]: 1,
[CalendarAttendance.Maybe]: 2,
[CalendarAttendance.NotAttending]: 3,
};
out.sort((a, b) => statusOrder[a.status] - statusOrder[b.status]);
return out;
})
const attendanceCountsByGroup = computed(() => {
const signups = activeEvent.value.eventSignups ?? [];
return {
Alpha: signups.filter(s => s.member_unit === "Alpha Company").length,
Echo: signups.filter(s => s.member_unit === "Echo Company").length,
Other: signups.filter(s =>
s.member_unit !== "Alpha Company" &&
s.member_unit !== "Echo Company"
).length,
};
});
const attendanceStatusSummary = computed(() => {
const signups = activeEvent.value.eventSignups ?? [];
return {
attending: signups.filter(s => s.status === CalendarAttendance.Attending).length,
maybe: signups.filter(s => s.status === CalendarAttendance.Maybe).length,
notAttending: signups.filter(s => s.status === CalendarAttendance.NotAttending).length,
};
});
const statusColor = (status: CalendarAttendance) => {
switch (status) {
case CalendarAttendance.Attending:
return "text-success";
case CalendarAttendance.Maybe:
return "text-yellow-600";
case CalendarAttendance.NotAttending:
return "text-destructive";
default:
return "";
}
};
const displayStatus = (status: CalendarAttendance) => {
switch (status) {
case CalendarAttendance.Attending:
return "Attending";
case CalendarAttendance.Maybe:
return "Maybe";
case CalendarAttendance.NotAttending:
return "Declined";
default:
return status;
}
};
defineExpose({ forceReload })
</script> </script>
<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 h-14"> <div class="flex items-center justify-between gap-3 border-b px-4 py-3">
<h2 class="text-lg font-semibold break-all"> <h2 class="text-lg font-semibold break-all">
{{ activeEvent?.name || 'Event' }} {{ activeEvent?.name || 'Event' }}
</h2> </h2>
<div class="flex gap-4 items-center"> <div class="flex gap-4">
<DropdownMenu v-if="canEditEvent"> <DropdownMenu v-if="canEditEvent">
<DropdownMenuTrigger> <DropdownMenuTrigger>
<button <button
@@ -191,7 +119,8 @@ defineExpose({ forceReload })
<DropdownMenuItem @click="emit('edit', activeEvent)"> <DropdownMenuItem @click="emit('edit', activeEvent)">
Edit Edit
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem v-if="activeEvent.cancelled" @click="setCancel(false)"> <DropdownMenuItem v-if="activeEvent.cancelled"
@click="setCancel(false)">
Un-Cancel Un-Cancel
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem v-else @click="setCancel(true)"> <DropdownMenuItem v-else @click="setCancel(true)">
@@ -213,7 +142,7 @@ defineExpose({ forceReload })
<CircleAlert></CircleAlert> This event has been cancelled <CircleAlert></CircleAlert> This event has been cancelled
</div> </div>
</section> </section>
<section v-if="isPast && user.isLoggedIn" class="w-full"> <section class="w-full">
<ButtonGroup class="flex w-full"> <ButtonGroup class="flex w-full">
<Button variant="outline" <Button variant="outline"
:class="myAttendance?.status === CalendarAttendance.Attending ? 'border-2 border-primary text-primary' : ''" :class="myAttendance?.status === CalendarAttendance.Attending ? 'border-2 border-primary text-primary' : ''"
@@ -226,22 +155,25 @@ defineExpose({ forceReload })
@click="setAttendance(CalendarAttendance.NotAttending)">Declined</Button> @click="setAttendance(CalendarAttendance.NotAttending)">Declined</Button>
</ButtonGroup> </ButtonGroup>
</section> </section>
<!-- Meta --> <!-- When -->
<section class="space-y-3 w-full"> <section v-if="whenText" class="space-y-2 w-full">
<p class="text-lg font-semibold">Details</p> <div class="inline-flex items-center gap-2 rounded-md border px-2.5 py-1.5 text-sm">
<div class="text-foreground/80 flex gap-3 items-center"> <Clock class="size-4 opacity-80" />
<Calendar :size="20"></Calendar> {{ dateText }} <span class="font-medium">{{ whenText }}</span>
</div>
<div class="text-foreground/80 flex gap-3 items-center">
<Clock4 :size="20"></Clock4> {{ timeText[0] }} - {{ timeText[1] }}
</div>
<div class="text-foreground/80 flex gap-3 items-center">
<MapPin :size="20"></MapPin> {{ activeEvent.location || "Unknown" }}
</div>
<div class="text-foreground/80 flex gap-3 items-center">
<User :size="20"></User> {{ activeEvent.creator_name || "Unknown User" }}
</div> </div>
</section> </section>
<!-- Quick meta chips -->
<section class="flex flex-wrap gap-2 w-full">
<span class="inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs">
<MapPin class="size-3.5 opacity-80" />
<span class="font-medium">{{ activeEvent.location || "Unknown" }}</span>
</span>
<span class="inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs">
<User class="size-3.5 opacity-80" />
<span class="font-medium">Created by: {{ activeEvent.creator_name || "Unknown User"
}}</span>
</span>
</section>
<!-- 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>
@@ -249,41 +181,46 @@ defineExpose({ forceReload })
{{ activeEvent.description }} {{ activeEvent.description }}
</p> </p>
</section> </section>
<!-- attendance --> <!-- Attendance -->
<section class="space-y-2 w-full"> <section class="space-y-2 w-full">
<div class="flex items-center gap-5">
<p class="text-lg font-semibold">Attendance</p> <p class="text-lg font-semibold">Attendance</p>
<!-- <div class="text-muted-foreground flex gap-6">
<p>Going <span class="ml-1">{{ attendanceStatusSummary.attending }}</span></p>
<p>Maybe <span class="ml-1">{{ attendanceStatusSummary.maybe }}</span></p>
<p>Declined <span class="ml-1">{{ attendanceStatusSummary.notAttending }}</span></p>
</div> -->
</div>
<div class="flex flex-col border bg-muted/50 rounded-lg min-h-24 my-2"> <div class="flex flex-col 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 *:w-full *:text-center *:pb-1 *:cursor-pointer">
<label :class="attendanceTab === 'Alpha' ? 'border-b-3 border-foreground' : 'mb-[2px]'" <label
@click="attendanceTab = 'Alpha'">Alpha {{ attendanceCountsByGroup.Alpha }}</label> :class="viewedState === CalendarAttendance.Attending ? 'border-b-3 border-foreground' : 'mb-[2px]'"
<label :class="attendanceTab === 'Echo' ? 'border-b-3 border-foreground' : 'mb-[2px]'" @click="viewedState = CalendarAttendance.Attending">Going {{ attending.length }}</label>
@click="attendanceTab = 'Echo'">Echo {{ attendanceCountsByGroup.Echo }}</label> <label
<label :class="attendanceTab === 'Other' ? 'border-b-3 border-foreground' : 'mb-[2px]'" :class="viewedState === CalendarAttendance.Maybe ? 'border-b-3 border-foreground' : 'mb-[2px]'"
@click="attendanceTab = 'Other'">Other {{ attendanceCountsByGroup.Other }}</label> @click="viewedState = CalendarAttendance.Maybe">Maybe {{ maybe.length }}</label>
<label
:class="viewedState === CalendarAttendance.NotAttending ? 'border-b-3 border-foreground' : 'mb-[2px]'"
@click="viewedState = CalendarAttendance.NotAttending">Declined {{ declined.length
}}</label>
</div> </div>
<div class="pb-1 min-h-48"> <div class="px-5 py-4 min-h-28">
<div class="grid grid-cols-2 font-semibold text-muted-foreground border-b py-1 px-3 mb-2"> <div v-if="viewedState === CalendarAttendance.Attending" v-for="person in attending">
<p>Name</p> <p>{{ person.member_name }}</p>
<p class="text-right">Status</p> </div>
</div> <div v-if="viewedState === CalendarAttendance.Maybe" v-for="person in maybe">
<p>{{ person.member_name }}</p>
<div v-for="person in attendanceList" :key="person.member_id" </div>
class="grid grid-cols-2 py-1 *:px-3 hover:bg-muted"> <div v-if="viewedState === CalendarAttendance.NotAttending" v-for="person in declined">
<p>{{ person.member_name }}</p> <p>{{ person.member_name }}</p>
<p :class="statusColor(person.status)" class="text-right">
{{ displayStatus(person.status) }}
</p>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
</div> </div>
<!-- Footer (optional actions) -->
<!-- <div class="border-t px-4 py-3 flex items-center justify-end gap-2">
<button class="rounded-md border px-3 py-1.5 text-sm hover:bg-muted/40 transition">
Edit
</button>
<button
class="rounded-md bg-primary text-primary-foreground px-3 py-1.5 text-sm hover:opacity-90 transition">
Open details
</button>
</div> -->
</div> </div>
</template> </template>

View File

@@ -20,12 +20,10 @@ const app = createApp(App)
app.use(createPinia()) app.use(createPinia())
app.use(router) app.use(router)
if (import.meta.env.VITE_DISABLE_GLITCHTIP === "true") { if (!!import.meta.env.VITE_DISABLE_GLITCHTIP) {
console.log("Glitchtip disabled");
} else {
let dsn = import.meta.env.VITE_GLITCHTIP_DSN; let dsn = import.meta.env.VITE_GLITCHTIP_DSN;
let environment = import.meta.env.VITE_ENVIRONMENT; let environment = import.meta.env.VITE_ENVIRONMENT;
let release = import.meta.env.VITE_APPLICATION_VERSION;
Sentry.init({ Sentry.init({
app, app,
dsn: dsn, dsn: dsn,
@@ -34,7 +32,7 @@ if (import.meta.env.VITE_DISABLE_GLITCHTIP === "true") {
], ],
tracesSampleRate: 0.01, tracesSampleRate: 0.01,
environment: environment, environment: environment,
release: release release: 'release tag'
}); });
} }

View File

@@ -3,14 +3,15 @@ import { ref, watch, nextTick, computed, onMounted } from 'vue'
import FullCalendar from '@fullcalendar/vue3' import FullCalendar from '@fullcalendar/vue3'
import dayGridPlugin from '@fullcalendar/daygrid' import dayGridPlugin from '@fullcalendar/daygrid'
import interactionPlugin from '@fullcalendar/interaction' import interactionPlugin from '@fullcalendar/interaction'
import { ChevronLeft, ChevronRight, Plus } from 'lucide-vue-next' import { X, Clock, MapPin, User, ListTodo, ChevronLeft, ChevronRight, Plus } from 'lucide-vue-next'
import CreateCalendarEvent from '@/components/calendar/CreateCalendarEvent.vue' import CreateCalendarEvent from '@/components/calendar/CreateCalendarEvent.vue'
import { CalendarEvent } from '@shared/types/calendar' import { getCalendarEvent, getMonthCalendarEvents } from '@/api/calendar'
import { CalendarEvent, CalendarEventShort } from '@shared/types/calendar'
import { Calendar } from '@fullcalendar/core'
import ViewCalendarEvent from '@/components/calendar/ViewCalendarEvent.vue' import ViewCalendarEvent from '@/components/calendar/ViewCalendarEvent.vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { useCalendarEvents } from '@/composables/useCalendarEvents' import { useCalendarEvents } from '@/composables/useCalendarEvents'
import { useCalendarNavigation } from '@/composables/useCalendarNavigation' import { useCalendarNavigation } from '@/composables/useCalendarNavigation'
import { useUserStore } from '@/stores/user'
const monthLabels = [ const monthLabels = [
'January', 'February', 'March', 'April', 'May', 'June', 'January', 'February', 'March', 'April', 'May', 'June',
@@ -23,14 +24,13 @@ function api() {
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const userStore = useUserStore();
function buildFullDate(month: number, year: number): Date { function buildFullDate(month: number, year: number): Date {
return new Date(year, month, 1); //default to first of month return new Date(year, month, 1); //default to first of month
} }
const { selectedMonth, selectedYear, years, goPrev, goNext, goToday, onDatesSet, goToSelectedDate } = useCalendarNavigation(api) const { selectedMonth, selectedYear, years, goPrev, goNext, goToday, onDatesSet, goToSelectedDate } = useCalendarNavigation(api)
const { events, loadEvents } = useCalendarEvents(selectedMonth, selectedYear); const { events, loadEvents} = useCalendarEvents(selectedMonth, selectedYear);
const panelOpen = ref(false) const panelOpen = ref(false)
const activeEvent = ref<CalendarEvent | null>(null) const activeEvent = ref<CalendarEvent | null>(null)
@@ -48,7 +48,6 @@ const dialogRef = ref<any>(null)
// NEW: handle day/time slot clicks to start creating an event // NEW: handle day/time slot clicks to start creating an event
function onDateClick(arg: { dateStr: string }) { function onDateClick(arg: { dateStr: string }) {
if (!userStore.isLoggedIn) return;
dialogRef.value?.openDialog(arg.dateStr); dialogRef.value?.openDialog(arg.dateStr);
// For now, just open the panel with a draft payload. // For now, just open the panel with a draft payload.
// activeEvent.value = { // activeEvent.value = {
@@ -203,7 +202,7 @@ onMounted(() => {
@click="goToday"> @click="goToday">
Today Today
</button> </button>
<button v-if="userStore.isLoggedIn" <button
class="cursor-pointer ml-1 inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:opacity-90" class="cursor-pointer ml-1 inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:opacity-90"
@click="onCreateEvent"> @click="onCreateEvent">
<Plus class="h-4 w-4" /> <Plus class="h-4 w-4" />
@@ -217,8 +216,7 @@ onMounted(() => {
<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="3xl:w-lg 2xl:w-md 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" @close="() => { router.push('/calendar'); }" <ViewCalendarEvent ref="eventViewRef" @close="() => { router.push('/calendar'); }" @reload="loadEvents()" @edit="(val) => {dialogRef.openDialog(null, 'edit', val)}">
@reload="loadEvents()" @edit="(val) => { dialogRef.openDialog(null, 'edit', val) }">
</ViewCalendarEvent> </ViewCalendarEvent>
</aside> </aside>
</div> </div>

110
ui/src/pages/Dossier.vue Normal file
View File

@@ -0,0 +1,110 @@
<script setup lang="ts">
</script>
<template>
<div class="px-10 py-6 max-w-7xl mx-auto w-full">
<!-- Header -->
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-semibold tracking-tight">Member Deployments</h1>
<div class="text-muted-foreground">Unit / Dossier / Deployments</div>
</div>
<!-- Summary Cards -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div class="p-5 rounded-xl border bg-card shadow-sm">
<p class="text-muted-foreground text-sm">Total Deployments</p>
<p class="text-3xl font-bold mt-2">123</p>
</div>
<div class="p-5 rounded-xl border bg-card shadow-sm">
<p class="text-muted-foreground text-sm">Total Hours</p>
<p class="text-3xl font-bold mt-2">456h</p>
</div>
<div class="p-5 rounded-xl border bg-card shadow-sm">
<p class="text-muted-foreground text-sm">Avg. Attendance</p>
<p class="text-3xl font-bold mt-2">87%</p>
</div>
</div>
<!-- Filters & Search -->
<div class="flex justify-between items-end mb-4 flex-wrap gap-4">
<div class="flex gap-4 flex-wrap">
<div>
<label class="block text-sm text-muted-foreground mb-1">Operation Type</label>
<select class="border rounded-md px-3 py-2 w-48 bg-background">
<option>All</option>
<option>Deployment</option>
<option>Training</option>
</select>
</div>
<div>
<label class="block text-sm text-muted-foreground mb-1">Sort By</label>
<select class="border rounded-md px-3 py-2 w-48 bg-background">
<option>Date (Newest)</option>
<option>Date (Oldest)</option>
<option>Longest Duration</option>
</select>
</div>
</div>
<div>
<label class="block text-sm text-muted-foreground mb-1">Search</label>
<input type="text" placeholder="Search deployments..." class="border rounded-md px-3 py-2 w-56 bg-background" />
</div>
</div>
<!-- Deployment List -->
<div class="rounded-xl border divide-y bg-card shadow-sm">
<!-- Row -->
<div class="p-5 hover:bg-accent cursor-pointer flex justify-between items-center">
<div>
<p class="font-semibold text-lg">Operation Dawn Strike</p>
<div class="text-sm text-muted-foreground flex gap-6 mt-1">
<span>Date: 2024-08-14</span>
<span>Duration: 3.4h</span>
<span>Role: Rifleman</span>
</div>
</div>
<div class="text-right">
<p class="font-medium">Status: <span class="text-green-500 font-semibold">Completed</span></p>
</div>
</div>
<div class="p-5 hover:bg-accent cursor-pointer flex justify-between items-center">
<div>
<p class="font-semibold text-lg">Operation Iron Resolve</p>
<div class="text-sm text-muted-foreground flex gap-6 mt-1">
<span>Date: 2024-08-02</span>
<span>Duration: 2.1h</span>
<span>Role: Machine Gunner</span>
</div>
</div>
<div class="text-right">
<p class="font-medium">Status: <span class="text-yellow-500 font-semibold">Partial</span></p>
</div>
</div>
<div class="p-5 hover:bg-accent cursor-pointer flex justify-between items-center">
<div>
<p class="font-semibold text-lg">Operation Midnight Gale</p>
<div class="text-sm text-muted-foreground flex gap-6 mt-1">
<span>Date: 2024-07-22</span>
<span>Duration: 4.8h</span>
<span>Role: Squad Leader</span>
</div>
</div>
<div class="text-right">
<p class="font-medium">Status: <span class="text-red-500 font-semibold">NoShow</span></p>
</div>
</div>
</div>
</div>
</template>

View File

@@ -171,9 +171,9 @@ const finalPanel = ref<'app' | 'message'>('message');
<li>When prompted, choose <em>“Yes”</em> to download all associated mods.</li> <li>When prompted, choose <em>“Yes”</em> to download all associated mods.</li>
</ul> </ul>
<p> <p>
<a href="https://docs.iceberg-gaming.com/books/member-guides/page/new-member-setup-onboarding" <a href="https://www.guilded.gg/Iceberg-gaming/groups/v3j2vAP3/channels/6979335e-60f7-4ab9-9590-66df69367d1e/docs/2013948655"
class="text-primary underline" target="_blank"> class="text-primary underline" target="_blank">
Click here for the full installation guide (Requires Sign-in) Click here for the full installation guide
</a> </a>
</p> </p>
<!-- CONTACT SECTION --> <!-- CONTACT SECTION -->
@@ -211,7 +211,7 @@ const finalPanel = ref<'app' | 'message'>('message');
our forums and introduce yourself. our forums and introduce yourself.
</p> </p>
<p> <p>
If you have any questions, feel free to reach out on TeamSpeak or Discord If you have any questions, feel free to reach out on TeamSpeak, Discord, or Guilded,
someone someone
will always be around to help. will always be around to help.
</p> </p>

View File

@@ -16,13 +16,16 @@ const router = createRouter({
{ path: '/loa', component: () => import('@/pages/SubmitLOA.vue'), meta: { requiresAuth: true, memberOnly: true } }, { path: '/loa', component: () => import('@/pages/SubmitLOA.vue'), meta: { requiresAuth: true, memberOnly: true } },
{ path: '/transfer', component: () => import('@/pages/Transfer.vue'), meta: { requiresAuth: true, memberOnly: true } }, { path: '/transfer', component: () => import('@/pages/Transfer.vue'), meta: { requiresAuth: true, memberOnly: true } },
{ path: '/calendar', component: () => import('@/pages/Calendar.vue'), meta: { }, }, { path: '/calendar', component: () => import('@/pages/Calendar.vue'), meta: { requiresAuth: true, memberOnly: true }, },
{ path: '/calendar/event/:id', component: () => import('@/pages/Calendar.vue'), meta: { }, }, { path: '/calendar/event/:id', component: () => import('@/pages/Calendar.vue'), meta: { requiresAuth: true, memberOnly: true }, },
{ path: '/trainingReport', component: () => import('@/pages/TrainingReport.vue'), meta: { requiresAuth: true, memberOnly: true } }, { path: '/trainingReport', component: () => import('@/pages/TrainingReport.vue'), meta: { requiresAuth: true, memberOnly: true } },
{ path: '/trainingReport/new', component: () => import('@/pages/TrainingReport.vue'), meta: { requiresAuth: true, memberOnly: true } }, { path: '/trainingReport/new', component: () => import('@/pages/TrainingReport.vue'), meta: { requiresAuth: true, memberOnly: true } },
{ path: '/trainingReport/:id', component: () => import('@/pages/TrainingReport.vue'), meta: { requiresAuth: true, memberOnly: true } }, { path: '/trainingReport/:id', component: () => import('@/pages/TrainingReport.vue'), meta: { requiresAuth: true, memberOnly: true } },
// Personnel File
{ path: '/dossier', component: () => import('@/pages/Dossier.vue'), meta: { requiresAuth: false, memberOnly: false } },
// ADMIN / STAFF ROUTES // ADMIN / STAFF ROUTES
{ {
path: '/administration', path: '/administration',