Some checks failed
Continuous Deployment / Update Deployment (push) Failing after 1m22s
282 lines
12 KiB
Vue
282 lines
12 KiB
Vue
<script setup lang="ts">
|
|
import type { CalendarEvent, CalendarSignup } from '@shared/types/calendar'
|
|
import { CircleAlert, Clock, Clock1, EllipsisVertical, MapPin, User, X } from 'lucide-vue-next';
|
|
import { computed, onMounted, ref, watch } from 'vue';
|
|
import ButtonGroup from '../ui/button-group/ButtonGroup.vue';
|
|
import Button from '../ui/button/Button.vue';
|
|
import { CalendarAttendance, getCalendarEvent, setCalendarEventAttendance, setCancelCalendarEvent } from '@/api/calendar';
|
|
import { useUserStore } from '@/stores/user';
|
|
import { useRoute } from 'vue-router';
|
|
import DropdownMenu from '../ui/dropdown-menu/DropdownMenu.vue';
|
|
import DropdownMenuTrigger from '../ui/dropdown-menu/DropdownMenuTrigger.vue';
|
|
import DropdownMenuContent from '../ui/dropdown-menu/DropdownMenuContent.vue';
|
|
import DropdownMenuItem from '../ui/dropdown-menu/DropdownMenuItem.vue';
|
|
import { Calendar } from 'lucide-vue-next';
|
|
|
|
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 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(
|
|
() => route.params.id,
|
|
async (id) => {
|
|
if (!id) return;
|
|
activeEvent.value = await getCalendarEvent(Number(id));
|
|
loaded.value = true;
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'close'): void
|
|
(e: 'reload'): void
|
|
(e: 'edit', event: CalendarEvent): void
|
|
}>()
|
|
|
|
const dateFmt = new Intl.DateTimeFormat(undefined, {
|
|
weekday: 'long', month: 'short', day: 'numeric',
|
|
})
|
|
|
|
const timeFmt = new Intl.DateTimeFormat(undefined, {
|
|
hour: 'numeric', minute: '2-digit'
|
|
})
|
|
|
|
const dateText = computed(() => {
|
|
let start = dateFmt.format(new Date(activeEvent.value.start));
|
|
let end = dateFmt.format(new Date(activeEvent.value.end));
|
|
if (start === end)
|
|
return start;
|
|
else
|
|
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 maybe = computed<CalendarSignup[]>(() => { return activeEvent.value.eventSignups.filter((s) => s.status == CalendarAttendance.Maybe) })
|
|
const declined = computed<CalendarSignup[]>(() => { return activeEvent.value.eventSignups.filter((s) => s.status == CalendarAttendance.NotAttending) })
|
|
const viewedState = ref<CalendarAttendance>(CalendarAttendance.Attending);
|
|
|
|
let user = useUserStore();
|
|
const myAttendance = computed<CalendarSignup | null>(() => {
|
|
return activeEvent.value.eventSignups.find(
|
|
(s) => s.member_id === user.user.id
|
|
) || null;
|
|
});
|
|
|
|
async function setAttendance(state: CalendarAttendance) {
|
|
await setCalendarEventAttendance(activeEvent.value.id, state);
|
|
//refresh event data
|
|
activeEvent.value = await getCalendarEvent(activeEvent.value.id);
|
|
}
|
|
|
|
const canEditEvent = computed(() => {
|
|
if (user.user.id == activeEvent.value.creator_id)
|
|
return true;
|
|
});
|
|
|
|
async function setCancel(isCancelled: boolean) {
|
|
await setCancelCalendarEvent(activeEvent.value.id, isCancelled);
|
|
emit("reload");
|
|
activeEvent.value = await getCalendarEvent(activeEvent.value.id);
|
|
}
|
|
|
|
async function forceReload() {
|
|
activeEvent.value = await getCalendarEvent(activeEvent.value.id);
|
|
}
|
|
|
|
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 attendanceCounts = 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 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>
|
|
|
|
<template>
|
|
<div v-if="loaded">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between gap-3 border-b px-4 py-3">
|
|
<h2 class="text-lg font-semibold break-all">
|
|
{{ activeEvent?.name || 'Event' }}
|
|
</h2>
|
|
<div class="flex gap-4">
|
|
<DropdownMenu v-if="canEditEvent">
|
|
<DropdownMenuTrigger>
|
|
<button
|
|
class="inline-flex flex-none size-8 items-center justify-center cursor-pointer rounded-md hover:bg-muted/40 transition">
|
|
<EllipsisVertical class="size-6" />
|
|
</button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent>
|
|
<DropdownMenuItem @click="emit('edit', activeEvent)">
|
|
Edit
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem v-if="activeEvent.cancelled" @click="setCancel(false)">
|
|
Un-Cancel
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem v-else @click="setCancel(true)">
|
|
Cancel
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
<button
|
|
class="inline-flex flex-none size-8 items-center justify-center rounded-md border hover:bg-muted/40 transition cursor-pointer"
|
|
aria-label="Close" @click="emit('close')">
|
|
<X class="size-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<!-- Body -->
|
|
<div class="flex-1 flex flex-col items-center min-h-0 overflow-y-auto px-4 py-4 space-y-6 w-full">
|
|
<section v-if="activeEvent.cancelled == true" class="w-full">
|
|
<div class="flex p-2 rounded-md w-full bg-destructive gap-3">
|
|
<CircleAlert></CircleAlert> This event has been cancelled
|
|
</div>
|
|
</section>
|
|
<section v-if="isPast" class="w-full">
|
|
<ButtonGroup class="flex w-full">
|
|
<Button variant="outline"
|
|
:class="myAttendance?.status === CalendarAttendance.Attending ? 'border-2 border-primary text-primary' : ''"
|
|
@click="setAttendance(CalendarAttendance.Attending)">Going</Button>
|
|
<Button variant="outline"
|
|
:class="myAttendance?.status === CalendarAttendance.Maybe ? 'border-2 !border-l-2 border-primary text-primary' : ''"
|
|
@click="setAttendance(CalendarAttendance.Maybe)">Maybe</Button>
|
|
<Button variant="outline"
|
|
:class="myAttendance?.status === CalendarAttendance.NotAttending ? 'border-2 !border-l-2 border-primary text-primary' : ''"
|
|
@click="setAttendance(CalendarAttendance.NotAttending)">Declined</Button>
|
|
</ButtonGroup>
|
|
</section>
|
|
<!-- Meta -->
|
|
<section class="space-y-3 w-full">
|
|
<p class="text-lg font-semibold">Details</p>
|
|
<div class="text-foreground/80 flex gap-3 items-center">
|
|
<Calendar :size="20"></Calendar> {{ dateText }}
|
|
</div>
|
|
<div class="text-foreground/80 flex gap-3 items-center">
|
|
<Clock1 :size="20"></Clock1> {{ 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>
|
|
</section>
|
|
<!-- Description -->
|
|
<section class="space-y-2 w-full">
|
|
<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">
|
|
{{ activeEvent.description }}
|
|
</p>
|
|
</section>
|
|
<!-- attendance -->
|
|
<section class="space-y-2 w-full">
|
|
<p class="text-lg font-semibold">Attendance</p>
|
|
<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">
|
|
<label :class="attendanceTab === 'Alpha' ? 'border-b-3 border-foreground' : 'mb-[2px]'"
|
|
@click="attendanceTab = 'Alpha'">Alpha {{ attendanceCounts.Alpha }}</label>
|
|
<label :class="attendanceTab === 'Echo' ? 'border-b-3 border-foreground' : 'mb-[2px]'"
|
|
@click="attendanceTab = 'Echo'">Echo {{ attendanceCounts.Echo }}</label>
|
|
<label :class="attendanceTab === 'Other' ? 'border-b-3 border-foreground' : 'mb-[2px]'"
|
|
@click="attendanceTab = 'Other'">Other {{ attendanceCounts.Other }}</label>
|
|
</div>
|
|
<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">
|
|
<p>Name</p>
|
|
<p class="text-right">Status</p>
|
|
</div>
|
|
|
|
<div v-for="person in attendanceList" :key="person.member_id"
|
|
class="grid grid-cols-2 py-1 *:px-3 hover:bg-muted">
|
|
<p>{{ person.member_name }}</p>
|
|
<p :class="statusColor(person.status)" class="text-right">
|
|
{{ displayStatus(person.status) }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
</template> |