34 Commits

Author SHA1 Message Date
dcd1f522ad Merge branch 'main' into external-links-and-app-messages 2025-12-10 20:21:24 -06:00
22c909cfa5 increase logging in deploy script
All checks were successful
Continuous Integration / Update Development (push) Successful in 2m24s
for some reason it seems to not be fully updating the local repo, so increase logging and double pull
2025-12-10 20:20:06 -06:00
50b9924627 deployment versioning
Some checks failed
Continuous Integration / Update Development (push) Successful in 1m58s
Continuous Deployment / Update Deployment (push) Failing after 2m12s
port development versioning to deployment, but using tags instead of short sha
2025-12-10 11:50:07 -06:00
d44d490905 version parsing fix
All checks were successful
Continuous Integration / Update Development (push) Successful in 2m13s
attempt parse fix because i forgot this isn't running on windows
2025-12-10 11:43:40 -06:00
114857cceb debug workflow
Some checks failed
Continuous Integration / Update Development (push) Failing after 1m49s
is sed really not included?
2025-12-10 11:35:58 -06:00
0245cb38f4 enable development app versioning
Some checks failed
Continuous Integration / Update Development (push) Failing after 2m16s
parse git commit short sha and use as app version in development
2025-12-10 11:30:13 -06:00
4e443cf46d Fixed glitchtip enable/disable to behave as expected and added support for ENV versioning
All checks were successful
Continuous Integration / Update Development (push) Successful in 2m27s
2025-12-10 00:18:09 -05:00
be53fce4a5 Merge pull request 'Calendar-Improvements' (#57) from Calendar-Improvements into main
All checks were successful
Continuous Integration / Update Development (push) Successful in 2m23s
Reviewed-on: #57
2025-12-09 21:21:24 -06:00
2ac66877eb Merge branch 'main' into Calendar-Improvements 2025-12-09 21:21:03 -06:00
760965de19 Add .gitea/workflows/ci-deploy.yaml
Some checks failed
Continuous Integration / Update Development (push) Failing after 1m28s
create CI script based on CD script to run on every push to main and deploy on development site
2025-12-09 21:11:03 -06:00
37f204137b Update .gitea/workflows/cd-deploy.yaml
fix run, apparently on push triggers are not cumulatively conditional
2025-12-09 21:06:45 -06:00
58431891ba Update ecosystem.config.js
All checks were successful
Continuous Deployment / Update Deployment (push) Successful in 2m15s
allow automatic naming of started process based on environment
2025-12-09 20:55:41 -06:00
a95bf3318d Update .gitea/workflows/cd-deploy.yaml
All checks were successful
Continuous Deployment / Update Deployment (push) Successful in 2m5s
update the CD script to only run on tags signifying a usable release
2025-12-09 20:53:33 -06:00
6d544225d9 Update .gitea/workflows/cd-deploy.yaml
All checks were successful
Continuous Deployment / Update Deployment (push) Successful in 3m20s
fix syntax
2025-12-09 20:31:38 -06:00
ec854c6800 Update .gitea/workflows/cd-deploy.yaml
Some checks failed
Continuous Deployment / Update Deployment (push) Failing after 1m17s
update node version to 20 from built in 18
2025-12-09 20:26:13 -06:00
8089ee4268 Update .gitea/workflows/cd-deploy.yaml
Some checks failed
Continuous Deployment / Update Deployment (push) Failing after 1m48s
stupidly work out permissions error due to sudo not copying environment variables properly
2025-12-09 20:22:08 -06:00
468fd30514 Update .gitea/workflows/cd-deploy.yaml
Some checks failed
Continuous Deployment / Update Deployment (push) Failing after 1m27s
attempt to preserve existing environment to fix node issues
2025-12-09 14:55:40 -06:00
1f52a2c4f7 Update .gitea/workflows/cd-deploy.yaml
Some checks failed
Continuous Deployment / Update Deployment (push) Failing after 1m27s
forcefully override npm path for execution
2025-12-09 14:48:19 -06:00
dccdaddd20 Update .gitea/workflows/cd-deploy.yaml
Some checks failed
Continuous Deployment / Update Deployment (push) Failing after 1m28s
another deployment attempt
2025-12-09 14:45:24 -06:00
1a80bfc543 Update .gitea/workflows/cd-deploy.yaml
Some checks failed
Continuous Deployment / Update Deployment (push) Failing after 1m33s
continue to debug workflow
2025-12-09 14:37:16 -06:00
2185ffc746 Update .gitea/workflows/cd-deploy.yaml
Some checks failed
Continuous Deployment / Update Deployment (push) Failing after 1m32s
debug workflow action
2025-12-09 14:27:42 -06:00
9720ee6ada Updated GUILDED references and links.
Some checks failed
Continuous Deployment / Update Deployment (push) Failing after 1m22s
2025-12-08 17:55:33 -06:00
c04a2b06cb Fixed glitchtip disabled inversion on API
Some checks failed
Continuous Deployment / Update Deployment (push) Failing after 1m22s
2025-12-08 14:51:02 -05:00
8c1b132f8d Merge branch 'Calendar-Improvements' of https://gitea.iceberg-gaming.com/17th-Ranger-Battalion-ORG/milsim-site-v4 into Calendar-Improvements
Some checks failed
Continuous Deployment / Update Deployment (push) Failing after 1m17s
2025-12-05 16:47:27 -05:00
4e69b419f0 Merge branch 'main' into Calendar-Improvements
Some checks failed
Continuous Deployment / Update Deployment (push) Failing after 1m18s
2025-12-05 15:43:49 -06:00
a233b15ad4 Switched clock icon to something more readable at that size 2025-12-05 16:34:00 -05:00
94b5bfcff7 Added support for public read only calendar #53
Some checks failed
Continuous Deployment / Update Deployment (push) Failing after 1m18s
2025-12-05 16:28:42 -05:00
09d20fd18f prevented header wiggle when editable value changes
Some checks failed
Continuous Deployment / Update Deployment (push) Failing after 1m18s
2025-12-05 16:16:51 -05:00
89280dd3a7 Added (hidden) attendees summary
Some checks failed
Continuous Deployment / Update Deployment (push) Failing after 1m18s
2025-12-05 16:12:25 -05:00
575455a0fc overhauled attendance visualization to be more useful for planning
Some checks failed
Continuous Deployment / Update Deployment (push) Failing after 1m22s
2025-12-05 16:04:32 -05:00
ccb132b9b0 Improvements to event details UI to address #41
Some checks failed
Continuous Deployment / Update Deployment (push) Failing after 1m30s
2025-12-05 12:53:32 -05:00
98138f51f4 Fixed glitchtip disabled state inverted (whoops)
Some checks failed
Continuous Deployment / Update Deployment (push) Failing after 1m12s
2025-12-04 23:42:28 -05:00
e550f862bc event details area initial redesign 2025-12-04 23:41:55 -05:00
6d6789c4a6 Disabled attendance buttons if event end time is past current time to address #55
Some checks failed
Continuous Deployment / Update Deployment (push) Failing after 1m13s
2025-12-04 22:55:57 -05:00
16 changed files with 332 additions and 245 deletions

View File

@@ -1,6 +1,8 @@
name: Continuous Deployment name: Continuous Deployment
on: on:
push: push:
tags:
- '*'
jobs: jobs:
Deploy: Deploy:
@@ -8,20 +10,29 @@ 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:rw - /var/www/html/milsim-site-v4:/var/www/html/milsim-site-v4:z
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: Verify Node Environment - name: Update 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@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
ref: 'main' ref: 'main'
@@ -32,31 +43,53 @@ 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: Fix 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
- name: Update Application Code - name: Update Application Code
run: | run: |
sudo -u nginx bash -c "cd /var/www/html/milsim-site-v4 && git reset --hard && git pull origin main" 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 - name: Update Shared Dependencies and Fix Permissions
run: | run: |
sudo -u nginx bash -c "cd /var/www/html/milsim-site-v4/shared && npm install" cd /var/www/html/milsim-site-v4/shared
npm install
chown -R nginx:nginx .
- name: Update UI Dependencies - name: Update UI Dependencies and Fix Permissions
run: | run: |
sudo -u nginx bash -c "cd /var/www/html/milsim-site-v4/ui && npm install" cd /var/www/html/milsim-site-v4/ui
npm install
chown -R nginx:nginx .
- name: Update API Dependencies - name: Update API Dependencies and Fix Permissions
run: | run: |
sudo -u nginx bash -c "cd /var/www/html/milsim-site-v4/api && npm install" cd /var/www/html/milsim-site-v4/api
npm install
chown -R nginx:nginx .
- name: Build UI - name: Build UI / Update Version / Fix Permissions
run: | run: |
sudo -u nginx bash -c "cd /var/www/html/milsim-site-v4/ui && npm run build" 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 - name: Build API / Update Version / Fix Permissions
run: | run: |
sudo -u nginx bash -c "cd /var/www/html/milsim-site-v4/api && npm run build" 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: |
sudo chown -R nginx:nginx /var/www/html/milsim-site-v4
sudo chmod -R u+w /var/www/html/milsim-site-v4

View File

@@ -0,0 +1,89 @@
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,6 +19,8 @@ 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,11 +20,14 @@ 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) { if (process.env.DISABLE_GLITCHTIP === "true") {
console.log("Glitchtip disabled AAAAAA") console.log("Glitchtip disabled")
} else { } else {
let dsn = process.env.GLITCHTIP_DSN; let dsn = process.env.GLITCHTIP_DSN;
sentry.init({ dsn: dsn }); let release = process.env.APPLICATION_VERSION;
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");
} }
@@ -58,6 +61,7 @@ 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,12 +21,13 @@ 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('id_token claims:', JSON.stringify(jwtClaims, null, 2)); console.log('profile:', profile);
// console.log('preferred_username:', jwtClaims?.preferred_username); console.log('id_token claims:', JSON.stringify(jwtClaims, null, 2));
console.log('preferred_username:', jwtClaims?.preferred_username);
const con = await pool.getConnection(); const con = await pool.getConnection();
try { try {

View File

@@ -123,15 +123,9 @@ 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 = ?
`;
return await pool.query(sql, [eventID]); const sql = "CALL `sp_GetCalendarEventSignups`(?)"
const res = await pool.query(sql, [eventID]);
console.log(res[0]);
return res[0];
} }

View File

@@ -5,6 +5,7 @@ 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,6 +26,7 @@ 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,6 +1,8 @@
# 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,6 +156,12 @@ 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, Clock, EllipsisVertical, MapPin, User, X } from 'lucide-vue-next'; import { CircleAlert, Clock4, EllipsisVertical, MapPin, User, X } from 'lucide-vue-next';
import { computed, onMounted, ref, watch } from 'vue'; import { computed, 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,24 +11,13 @@ 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) => {
@@ -45,23 +34,27 @@ const emit = defineEmits<{
(e: 'edit', event: CalendarEvent): void (e: 'edit', event: CalendarEvent): void
}>() }>()
// const activeEvent = computed(() => props.event) const dateFmt = new Intl.DateTimeFormat(undefined, {
weekday: 'long', month: 'short', day: 'numeric',
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, {
const timeFmt = new Intl.DateTimeFormat(undefined, {
hour: 'numeric', minute: '2-digit' hour: 'numeric', minute: '2-digit'
}) })
const whenText = computed(() => { const dateText = computed(() => {
if (!activeEvent.value?.start) return '' let start = dateFmt.format(new Date(activeEvent.value.start));
const s = new Date(activeEvent.value.start) let end = dateFmt.format(new Date(activeEvent.value.end));
const e = activeEvent.value?.end ? new Date(activeEvent.value.end) : null if (start === end)
return e return start;
? `${startFmt.format(s)} ${endFmt.format(e)}` else
: `${startFmt.format(s)}` return `${start} - ${end}`;
})
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) })
@@ -71,6 +64,7 @@ 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;
@@ -83,6 +77,7 @@ 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;
}); });
@@ -97,17 +92,94 @@ async function forceReload() {
activeEvent.value = await getCalendarEvent(activeEvent.value.id); activeEvent.value = await getCalendarEvent(activeEvent.value.id);
} }
defineExpose({forceReload}) const isPast = computed(() => {
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"> <div class="flex items-center justify-between gap-3 border-b px-4 py-3 h-14">
<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"> <div class="flex gap-4 items-center">
<DropdownMenu v-if="canEditEvent"> <DropdownMenu v-if="canEditEvent">
<DropdownMenuTrigger> <DropdownMenuTrigger>
<button <button
@@ -119,8 +191,7 @@ defineExpose({forceReload})
<DropdownMenuItem @click="emit('edit', activeEvent)"> <DropdownMenuItem @click="emit('edit', activeEvent)">
Edit Edit
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem v-if="activeEvent.cancelled" <DropdownMenuItem v-if="activeEvent.cancelled" @click="setCancel(false)">
@click="setCancel(false)">
Un-Cancel Un-Cancel
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem v-else @click="setCancel(true)"> <DropdownMenuItem v-else @click="setCancel(true)">
@@ -142,7 +213,7 @@ defineExpose({forceReload})
<CircleAlert></CircleAlert> This event has been cancelled <CircleAlert></CircleAlert> This event has been cancelled
</div> </div>
</section> </section>
<section class="w-full"> <section v-if="isPast && user.isLoggedIn" 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' : ''"
@@ -155,24 +226,21 @@ defineExpose({forceReload})
@click="setAttendance(CalendarAttendance.NotAttending)">Declined</Button> @click="setAttendance(CalendarAttendance.NotAttending)">Declined</Button>
</ButtonGroup> </ButtonGroup>
</section> </section>
<!-- When --> <!-- Meta -->
<section v-if="whenText" class="space-y-2 w-full"> <section class="space-y-3 w-full">
<div class="inline-flex items-center gap-2 rounded-md border px-2.5 py-1.5 text-sm"> <p class="text-lg font-semibold">Details</p>
<Clock class="size-4 opacity-80" /> <div class="text-foreground/80 flex gap-3 items-center">
<span class="font-medium">{{ whenText }}</span> <Calendar :size="20"></Calendar> {{ dateText }}
</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>
<!-- 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> </section>
<!-- Description --> <!-- Description -->
<section class="space-y-2 w-full"> <section class="space-y-2 w-full">
@@ -181,46 +249,41 @@ 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">
<p class="text-lg font-semibold">Attendance</p> <div class="flex items-center gap-5">
<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 <label :class="attendanceTab === 'Alpha' ? 'border-b-3 border-foreground' : 'mb-[2px]'"
:class="viewedState === CalendarAttendance.Attending ? 'border-b-3 border-foreground' : 'mb-[2px]'" @click="attendanceTab = 'Alpha'">Alpha {{ attendanceCountsByGroup.Alpha }}</label>
@click="viewedState = CalendarAttendance.Attending">Going {{ attending.length }}</label> <label :class="attendanceTab === 'Echo' ? 'border-b-3 border-foreground' : 'mb-[2px]'"
<label @click="attendanceTab = 'Echo'">Echo {{ attendanceCountsByGroup.Echo }}</label>
:class="viewedState === CalendarAttendance.Maybe ? 'border-b-3 border-foreground' : 'mb-[2px]'" <label :class="attendanceTab === 'Other' ? 'border-b-3 border-foreground' : 'mb-[2px]'"
@click="viewedState = CalendarAttendance.Maybe">Maybe {{ maybe.length }}</label> @click="attendanceTab = 'Other'">Other {{ attendanceCountsByGroup.Other }}</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="px-5 py-4 min-h-28"> <div class="pb-1 min-h-48">
<div v-if="viewedState === CalendarAttendance.Attending" v-for="person in attending"> <div class="grid grid-cols-2 font-semibold text-muted-foreground border-b py-1 px-3 mb-2">
<p>{{ person.member_name }}</p> <p>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,10 +20,12 @@ const app = createApp(App)
app.use(createPinia()) app.use(createPinia())
app.use(router) app.use(router)
if (!!import.meta.env.VITE_DISABLE_GLITCHTIP) { if (import.meta.env.VITE_DISABLE_GLITCHTIP === "true") {
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,
@@ -32,7 +34,7 @@ if (!!import.meta.env.VITE_DISABLE_GLITCHTIP) {
], ],
tracesSampleRate: 0.01, tracesSampleRate: 0.01,
environment: environment, environment: environment,
release: 'release tag' release: release
}); });
} }

View File

@@ -3,15 +3,14 @@ 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 { X, Clock, MapPin, User, ListTodo, ChevronLeft, ChevronRight, Plus } from 'lucide-vue-next' import { ChevronLeft, ChevronRight, Plus } from 'lucide-vue-next'
import CreateCalendarEvent from '@/components/calendar/CreateCalendarEvent.vue' import CreateCalendarEvent from '@/components/calendar/CreateCalendarEvent.vue'
import { getCalendarEvent, getMonthCalendarEvents } from '@/api/calendar' import { CalendarEvent } from '@shared/types/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',
@@ -24,13 +23,14 @@ 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,6 +48,7 @@ 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 = {
@@ -202,7 +203,7 @@ onMounted(() => {
@click="goToday"> @click="goToday">
Today Today
</button> </button>
<button <button v-if="userStore.isLoggedIn"
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" />
@@ -216,7 +217,8 @@ 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'); }" @reload="loadEvents()" @edit="(val) => {dialogRef.openDialog(null, 'edit', val)}"> <ViewCalendarEvent ref="eventViewRef" @close="() => { router.push('/calendar'); }"
@reload="loadEvents()" @edit="(val) => { dialogRef.openDialog(null, 'edit', val) }">
</ViewCalendarEvent> </ViewCalendarEvent>
</aside> </aside>
</div> </div>

View File

@@ -1,110 +0,0 @@
<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://www.guilded.gg/Iceberg-gaming/groups/v3j2vAP3/channels/6979335e-60f7-4ab9-9590-66df69367d1e/docs/2013948655" <a href="https://docs.iceberg-gaming.com/books/member-guides/page/new-member-setup-onboarding"
class="text-primary underline" target="_blank"> class="text-primary underline" target="_blank">
Click here for the full installation guide Click here for the full installation guide (Requires Sign-in)
</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, Discord, or Guilded, If you have any questions, feel free to reach out on TeamSpeak or Discord
someone someone
will always be around to help. will always be around to help.
</p> </p>

View File

@@ -16,16 +16,13 @@ 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: { requiresAuth: true, memberOnly: true }, }, { path: '/calendar', component: () => import('@/pages/Calendar.vue'), meta: { }, },
{ path: '/calendar/event/:id', component: () => import('@/pages/Calendar.vue'), meta: { requiresAuth: true, memberOnly: true }, }, { path: '/calendar/event/:id', component: () => import('@/pages/Calendar.vue'), meta: { }, },
{ 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',