Merge commit 'a6d54e0b73b0e388a96008400977db2715f214ce' into Documentation
This commit is contained in:
@@ -2,14 +2,16 @@
|
||||
import ApplicationChat from '@/components/application/ApplicationChat.vue';
|
||||
import ApplicationForm from '@/components/application/ApplicationForm.vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { ApplicationData, approveApplication, denyApplication, loadApplication, postApplication, postChatMessage, ApplicationStatus } from '@/api/application';
|
||||
import { approveApplication, denyApplication, loadApplication, postApplication, postChatMessage, getMyApplication, postAdminChatMessage } from '@/api/application';
|
||||
import { useRoute } from 'vue-router';
|
||||
import Button from '@/components/ui/button/Button.vue';
|
||||
import { CheckIcon, XIcon } from 'lucide-vue-next';
|
||||
import Unauthorized from './Unauthorized.vue';
|
||||
import { ApplicationData, ApplicationFull, ApplicationStatus, CommentRow } from '@shared/types/application';
|
||||
|
||||
const appData = ref<ApplicationData>(null);
|
||||
const appID = ref<number | null>(null);
|
||||
const chatData = ref<object[]>([])
|
||||
const chatData = ref<CommentRow[]>([])
|
||||
const readOnly = ref<boolean>(false);
|
||||
const newApp = ref<boolean>(null);
|
||||
const status = ref<ApplicationStatus>(null);
|
||||
@@ -19,13 +21,12 @@ const loading = ref<boolean>(true);
|
||||
const member_name = ref<string>();
|
||||
|
||||
const props = defineProps<{
|
||||
mode?: "create" | "view-self" | "view-recruiter"
|
||||
mode?: "create" | "view-self" | "view-recruiter" | "view-self-id"
|
||||
}>()
|
||||
|
||||
const finalMode = ref<"create" | "view-self" | "view-recruiter">("create");
|
||||
const finalMode = ref<"create" | "view-self" | "view-recruiter" | "view-self-id">("create");
|
||||
|
||||
async function loadByID(id: number | string) {
|
||||
const raw = await loadApplication(id);
|
||||
function loadData(raw: ApplicationFull) {
|
||||
|
||||
const data = raw.application;
|
||||
|
||||
@@ -40,20 +41,20 @@ async function loadByID(id: number | string) {
|
||||
readOnly.value = true;
|
||||
}
|
||||
|
||||
const router = useRoute();
|
||||
const route = useRoute();
|
||||
const unauthorized = ref(false);
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
//recruiter mode
|
||||
if (props.mode === 'view-recruiter') {
|
||||
finalMode.value = 'view-recruiter';
|
||||
await loadByID(Number(router.params.id));
|
||||
loadData(await loadApplication(Number(route.params.id), true))
|
||||
}
|
||||
|
||||
//viewer mode
|
||||
if (props.mode === 'view-self') {
|
||||
finalMode.value = 'view-self';
|
||||
await loadByID('me');
|
||||
loadData(await loadApplication("me"))
|
||||
}
|
||||
|
||||
//creator mode
|
||||
@@ -64,40 +65,33 @@ onMounted(async () => {
|
||||
newApp.value = true;
|
||||
}
|
||||
|
||||
if (props.mode === 'view-self-id') {
|
||||
finalMode.value = 'view-self-id';
|
||||
try {
|
||||
let raw = await getMyApplication(Number(route.params.id))
|
||||
loadData(raw);
|
||||
unauthorized.value = false;
|
||||
|
||||
} catch (error) {
|
||||
if (error.message === "Unauthorized") {
|
||||
unauthorized.value = true;
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
|
||||
// try {
|
||||
// //get app ID from URL param
|
||||
// if (appIDRaw === undefined) {
|
||||
// //new app
|
||||
// appData.value = null
|
||||
// readOnly.value = false;
|
||||
// newApp.value = true;
|
||||
// } else {
|
||||
// //load app
|
||||
// const raw = await loadApplication(appIDRaw.toString());
|
||||
|
||||
// const data = raw.application;
|
||||
|
||||
// appID.value = data.id;
|
||||
// appData.value = data.app_data;
|
||||
// chatData.value = raw.comments;
|
||||
// status.value = data.app_status;
|
||||
// decisionDate.value = new Date(data.decision_at);
|
||||
// submitDate.value = data.submitted_at ? new Date(data.submitted_at) : null;
|
||||
// member_name.value = data.member_name;
|
||||
// newApp.value = false;
|
||||
// readOnly.value = true;
|
||||
// }
|
||||
// } catch (e) {
|
||||
// console.error(e);
|
||||
// }
|
||||
})
|
||||
|
||||
async function postComment(comment) {
|
||||
chatData.value.push(await postChatMessage(comment, appID.value));
|
||||
}
|
||||
|
||||
async function postCommentInternal(comment) {
|
||||
chatData.value.push(await postAdminChatMessage(comment, appID.value));
|
||||
}
|
||||
|
||||
const emit = defineEmits(['submit']);
|
||||
|
||||
async function postApp(appData) {
|
||||
@@ -107,7 +101,7 @@ async function postApp(appData) {
|
||||
newApp.value = false;
|
||||
emit('submit');
|
||||
}
|
||||
// TODO: Handle fail to post
|
||||
// TODO: Handle fail to post
|
||||
}
|
||||
|
||||
async function handleApprove(id) {
|
||||
@@ -122,52 +116,59 @@ async function handleDeny(id) {
|
||||
|
||||
<template>
|
||||
<div v-if="!loading" class="w-full h-20">
|
||||
<div v-if="!newApp" class="flex flex-row justify-between items-center py-2 mb-8">
|
||||
<!-- Application header -->
|
||||
<div>
|
||||
<h3 class="scroll-m-20 text-2xl font-semibold tracking-tight">{{ member_name }}</h3>
|
||||
<p class="text-muted-foreground">Submitted: {{ submitDate.toLocaleString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit"
|
||||
}) }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-right" :class="[
|
||||
'font-semibold',
|
||||
status === ApplicationStatus.Pending && 'text-yellow-500',
|
||||
status === ApplicationStatus.Accepted && 'text-green-500',
|
||||
status === ApplicationStatus.Denied && 'text-red-500'
|
||||
]">{{ status }}</h3>
|
||||
<p v-if="status != ApplicationStatus.Pending" class="text-muted-foreground">{{ status }}: {{
|
||||
decisionDate.toLocaleString("en-US", {
|
||||
<div v-if="unauthorized" class="flex justify-center w-full my-10">
|
||||
You do not have permission to view this application.
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="!newApp" class="flex flex-row justify-between items-center py-2 mb-8">
|
||||
<!-- Application header -->
|
||||
<div>
|
||||
<h3 class="scroll-m-20 text-2xl font-semibold tracking-tight">{{ member_name }}</h3>
|
||||
<p class="text-muted-foreground">Submitted: {{ submitDate.toLocaleString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit"
|
||||
}) }}</p>
|
||||
<div class="mt-2" v-else-if="finalMode === 'view-recruiter'">
|
||||
<Button variant="success" class="mr-2" :onclick="() => { handleApprove(appID) }">
|
||||
<CheckIcon></CheckIcon>
|
||||
</Button>
|
||||
<Button variant="destructive" :onClick="() => { handleDeny(appID) }">
|
||||
<XIcon></XIcon>
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-right" :class="[
|
||||
'font-semibold',
|
||||
status === ApplicationStatus.Pending && 'text-yellow-500',
|
||||
status === ApplicationStatus.Accepted && 'text-green-500',
|
||||
status === ApplicationStatus.Denied && 'text-red-500'
|
||||
]">{{ status }}</h3>
|
||||
<p v-if="status != ApplicationStatus.Pending" class="text-muted-foreground">{{ status }}: {{
|
||||
decisionDate.toLocaleString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit"
|
||||
}) }}</p>
|
||||
<div class="mt-2" v-else-if="finalMode === 'view-recruiter'">
|
||||
<Button variant="success" class="mr-2" :onclick="() => { handleApprove(appID) }">
|
||||
<CheckIcon></CheckIcon>
|
||||
</Button>
|
||||
<Button variant="destructive" :onClick="() => { handleDeny(appID) }">
|
||||
<XIcon></XIcon>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex flex-row justify-between items-center py-2 mb-8">
|
||||
<h3 class="scroll-m-20 text-2xl font-semibold tracking-tight">Apply to join the 17th Rangers</h3>
|
||||
</div>
|
||||
<ApplicationForm :read-only="readOnly" :data="appData" @submit="(e) => { postApp(e) }" class="mb-7">
|
||||
</ApplicationForm>
|
||||
<div v-if="!newApp" class="pb-15">
|
||||
<h3 class="scroll-m-20 text-2xl font-semibold tracking-tight mb-4">Discussion</h3>
|
||||
<ApplicationChat :messages="chatData" @post="postComment" @post-internal="postCommentInternal">
|
||||
</ApplicationChat>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex flex-row justify-between items-center py-2 mb-8">
|
||||
<h3 class="scroll-m-20 text-2xl font-semibold tracking-tight">Apply to join the 17th Rangers</h3>
|
||||
</div>
|
||||
<ApplicationForm :read-only="readOnly" :data="appData" @submit="(e) => { postApp(e) }" class="mb-7">
|
||||
</ApplicationForm>
|
||||
<div v-if="!newApp">
|
||||
<h3 class="scroll-m-20 text-2xl font-semibold tracking-tight mb-4">Discussion</h3>
|
||||
<ApplicationChat :messages="chatData" @post="postComment"></ApplicationChat>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- TODO: Implement some kinda loading screen -->
|
||||
<div v-else class="flex items-center justify-center h-full">Loading</div>
|
||||
|
||||
@@ -3,15 +3,14 @@ import { ref, watch, nextTick, computed, onMounted } from 'vue'
|
||||
import FullCalendar from '@fullcalendar/vue3'
|
||||
import dayGridPlugin from '@fullcalendar/daygrid'
|
||||
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 { getCalendarEvent, getMonthCalendarEvents } from '@/api/calendar'
|
||||
import { CalendarEvent, CalendarEventShort } from '@shared/types/calendar'
|
||||
import { Calendar } from '@fullcalendar/core'
|
||||
import { CalendarEvent } from '@shared/types/calendar'
|
||||
import ViewCalendarEvent from '@/components/calendar/ViewCalendarEvent.vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useCalendarEvents } from '@/composables/useCalendarEvents'
|
||||
import { useCalendarNavigation } from '@/composables/useCalendarNavigation'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const monthLabels = [
|
||||
'January', 'February', 'March', 'April', 'May', 'June',
|
||||
@@ -24,13 +23,14 @@ function api() {
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const userStore = useUserStore();
|
||||
|
||||
function buildFullDate(month: number, year: number): Date {
|
||||
return new Date(year, month, 1); //default to first of month
|
||||
}
|
||||
|
||||
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 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
|
||||
function onDateClick(arg: { dateStr: string }) {
|
||||
if (!userStore.isLoggedIn) return;
|
||||
dialogRef.value?.openDialog(arg.dateStr);
|
||||
// For now, just open the panel with a draft payload.
|
||||
// activeEvent.value = {
|
||||
@@ -202,7 +203,7 @@ onMounted(() => {
|
||||
@click="goToday">
|
||||
Today
|
||||
</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"
|
||||
@click="onCreateEvent">
|
||||
<Plus class="h-4 w-4" />
|
||||
@@ -216,7 +217,8 @@ onMounted(() => {
|
||||
<aside v-if="panelOpen"
|
||||
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' }">
|
||||
<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>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useUserStore } from '@/stores/user';
|
||||
import { Check, Circle, Dot, Users, X } from 'lucide-vue-next'
|
||||
import { computed, ref } from 'vue';
|
||||
import Application from './Application.vue';
|
||||
import { restartApplication } from '@/api/application';
|
||||
|
||||
function goToLogin() {
|
||||
const redirectUrl = encodeURIComponent(window.location.origin + '/join')
|
||||
@@ -67,14 +68,25 @@ const currentStep = computed<number>(() => {
|
||||
case "denied":
|
||||
return 5;
|
||||
break;
|
||||
case "retired":
|
||||
return 5;
|
||||
break;
|
||||
}
|
||||
})
|
||||
|
||||
const finalPanel = ref<'app' | 'message'>('message');
|
||||
|
||||
const reloadKey = ref(0);
|
||||
|
||||
async function restartApp() {
|
||||
await restartApplication();
|
||||
await userStore.loadUser();
|
||||
reloadKey.value++;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col items-center mt-10 w-full">
|
||||
<div class="flex flex-col items-center mt-10 w-full" :key="reloadKey">
|
||||
|
||||
<!-- Stepper Container -->
|
||||
<div class="w-full flex justify-center">
|
||||
@@ -171,9 +183,9 @@ const finalPanel = ref<'app' | 'message'>('message');
|
||||
<li>When prompted, choose <em>“Yes”</em> to download all associated mods.</li>
|
||||
</ul>
|
||||
<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">
|
||||
Click here for the full installation guide
|
||||
Click here for the full installation guide (Requires Sign-in)
|
||||
</a>
|
||||
</p>
|
||||
<!-- CONTACT SECTION -->
|
||||
@@ -211,7 +223,7 @@ const finalPanel = ref<'app' | 'message'>('message');
|
||||
our forums and introduce yourself.
|
||||
</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
|
||||
will always be around to help.
|
||||
</p>
|
||||
@@ -219,8 +231,8 @@ const finalPanel = ref<'app' | 'message'>('message');
|
||||
</div>
|
||||
<!-- Denied message -->
|
||||
<div v-else-if="userStore.state === 'denied'">
|
||||
<div class="w-full max-w-2xl p-8">
|
||||
<h1 class="text-3xl sm:text-4xl font-bold mb-4 text-left text-destructive">
|
||||
<div class="w-full max-w-2xl flex flex-col gap-8">
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-left">
|
||||
Application Not Approved
|
||||
</h1>
|
||||
<div class="space-y-4 text-muted-foreground text-left leading-relaxed">
|
||||
@@ -246,6 +258,39 @@ const finalPanel = ref<'app' | 'message'>('message');
|
||||
Team</span>
|
||||
</p>
|
||||
</div>
|
||||
<Button class="w-min" @click="restartApp">New Application</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="userStore.state === 'retired'">
|
||||
<div class="w-full max-w-2xl flex flex-col gap-8">
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-left">
|
||||
You have retired from the 17th Ranger Battalion
|
||||
</h1>
|
||||
<div class="space-y-4 text-muted-foreground text-left leading-relaxed">
|
||||
<p>
|
||||
Thank you for your service and participation in the <strong>17th Ranger
|
||||
Battalion</strong>.
|
||||
Your time with us has been sincerely appreciated.
|
||||
</p>
|
||||
<p>
|
||||
Should you ever wish to return, you are welcome to <strong>reach out to our
|
||||
leadership
|
||||
team</strong>
|
||||
for guidance on the reinstatement process or to stay connected with the community.
|
||||
</p>
|
||||
<p>
|
||||
We recognize that circumstances change, and you will always have a place to
|
||||
reconnect with
|
||||
us
|
||||
should the opportunity arise in the future.
|
||||
</p>
|
||||
<p>
|
||||
All the best,<br />
|
||||
<span class="text-foreground font-medium">The 17th Ranger Battalion Leadership
|
||||
Team</span>
|
||||
</p>
|
||||
</div>
|
||||
<Button class="w-min" @click="restartApp">New Application</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import { getAllApplications, approveApplication, denyApplication, ApplicationStatus } from '@/api/application';
|
||||
import { getAllApplications, approveApplication, denyApplication } from '@/api/application';
|
||||
import { ApplicationStatus } from '@shared/types/application'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
|
||||
@@ -17,27 +17,24 @@ const showLOADialog = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="showLOADialog" v-on:update:open="showLOADialog = false">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Post LOA</DialogTitle>
|
||||
<DialogDescription>
|
||||
Post an LOA on behalf of a member.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<LoaForm :admin-mode="true" class="my-5 w-full"></LoaForm>
|
||||
<!-- <DialogFooter>
|
||||
<Button variant="secondary" @click="showLOADialog = false">Cancel</Button>
|
||||
<Button>Apply</Button>
|
||||
</DialogFooter> -->
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<div class="max-w-5xl mx-auto pt-10">
|
||||
<div class="flex justify-end mb-4">
|
||||
<Button @click="showLOADialog = true">Post LOA</Button>
|
||||
<div>
|
||||
<Dialog v-model:open="showLOADialog" v-on:update:open="showLOADialog = false">
|
||||
<DialogContent class="sm:max-w-fit">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Post LOA</DialogTitle>
|
||||
<DialogDescription>
|
||||
Post an LOA on behalf of a member.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<LoaForm :admin-mode="true" class="my-3"></LoaForm>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<div class="max-w-5xl mx-auto pt-10">
|
||||
<div class="flex justify-end mb-4">
|
||||
<Button @click="showLOADialog = true">Post LOA</Button>
|
||||
</div>
|
||||
<h1>LOA Log</h1>
|
||||
<LoaList :admin-mode="true"></LoaList>
|
||||
</div>
|
||||
<h1>LOA Log</h1>
|
||||
<LoaList></LoaList>
|
||||
</div>
|
||||
</template>
|
||||
148
ui/src/pages/MyApplications.vue
Normal file
148
ui/src/pages/MyApplications.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<script setup>
|
||||
import { loadMyApplications } from '@/api/application';
|
||||
import { ApplicationStatus } from '@shared/types/application';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import Button from '@/components/ui/button/Button.vue';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { CheckIcon, XIcon } from 'lucide-vue-next';
|
||||
import Application from './Application.vue';
|
||||
|
||||
const appList = ref([]);
|
||||
const now = Date.now();
|
||||
// relative time formatter (uses user locale)
|
||||
const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' })
|
||||
// exact date/time for tooltip
|
||||
const exactFmt = new Intl.DateTimeFormat(undefined, {
|
||||
dateStyle: 'medium', timeStyle: 'short', timeZone: 'America/Toronto'
|
||||
})
|
||||
|
||||
function formatAgo(iso) {
|
||||
const d = new Date(iso)
|
||||
if (isNaN(d)) return ''
|
||||
let diff = (d.getTime() - now) / 1000 // seconds relative to page load
|
||||
const divisions = [
|
||||
{ amount: 60, name: 'second' },
|
||||
{ amount: 60, name: 'minute' },
|
||||
{ amount: 24, name: 'hour' },
|
||||
{ amount: 7, name: 'day' },
|
||||
{ amount: 4.34524, name: 'week' }, // avg weeks per month
|
||||
{ amount: 12, name: 'month' },
|
||||
{ amount: Infinity, name: 'year' },
|
||||
]
|
||||
for (const div of divisions) {
|
||||
if (Math.abs(diff) < div.amount) {
|
||||
return rtf.format(Math.round(diff), div.name)
|
||||
}
|
||||
diff /= div.amount
|
||||
}
|
||||
}
|
||||
|
||||
function formatExact(iso) {
|
||||
const d = new Date(iso)
|
||||
return isNaN(d) ? '' : exactFmt.format(d)
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
function openApplication(id) {
|
||||
router.push(`/applications/${id}`)
|
||||
openPanel.value = true;
|
||||
}
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
watch(() => route.params.id, (newId) => {
|
||||
if (newId === undefined) {
|
||||
openPanel.value = false;
|
||||
}
|
||||
})
|
||||
|
||||
const openPanel = ref(false);
|
||||
|
||||
onMounted(async () => {
|
||||
appList.value = await loadMyApplications();
|
||||
|
||||
//preload application
|
||||
if (route.params.id != undefined) {
|
||||
openApplication(route.params.id)
|
||||
} else {
|
||||
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div class="px-20 mx-auto max-w-[100rem] w-full flex mt-5 h-52 min-h-0 overflow-hidden">
|
||||
<!-- application list -->
|
||||
<div :class="openPanel == false ? 'w-full' : 'w-2/5'" class="pr-9">
|
||||
<h1 class="scroll-m-20 text-2xl font-semibold tracking-tight mb-5">My Applications</h1>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Date Submitted</TableHead>
|
||||
<TableHead class="text-right">Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody class="overflow-y-auto scrollbar-themed">
|
||||
<TableRow v-for="app in appList" :key="app.id" class="cursor-pointer"
|
||||
:onClick="() => { openApplication(app.id) }">
|
||||
<TableCell :title="formatExact(app.submitted_at)">
|
||||
{{ formatAgo(app.submitted_at) }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right font-semibold" :class="[
|
||||
,
|
||||
app.app_status === ApplicationStatus.Pending && 'text-yellow-500',
|
||||
app.app_status === ApplicationStatus.Accepted && 'text-green-500',
|
||||
app.app_status === ApplicationStatus.Denied && 'text-destructive'
|
||||
]">{{ app.app_status }}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div v-if="openPanel" class="pl-9 border-l w-3/5" :key="$route.params.id">
|
||||
<div class="mb-5 flex justify-between">
|
||||
<p class="scroll-m-20 text-2xl font-semibold tracking-tight"> Application</p>
|
||||
</div>
|
||||
<div class="overflow-y-auto max-h-[80vh] h-full mt-5 scrollbar-themed">
|
||||
<Application :mode="'view-self-id'"></Application>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Firefox */
|
||||
.scrollbar-themed {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #555 #1f1f1f;
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
/* Chrome, Edge, Safari */
|
||||
.scrollbar-themed::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
/* slightly wider to allow padding look */
|
||||
}
|
||||
|
||||
.scrollbar-themed::-webkit-scrollbar-track {
|
||||
background: #1f1f1f;
|
||||
margin-left: 6px;
|
||||
/* ❗ adds space between content + scrollbar */
|
||||
}
|
||||
|
||||
.scrollbar-themed::-webkit-scrollbar-thumb {
|
||||
background: #555;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.scrollbar-themed::-webkit-scrollbar-thumb:hover {
|
||||
background: #777;
|
||||
}
|
||||
</style>
|
||||
@@ -2,6 +2,8 @@
|
||||
import LoaForm from '@/components/loa/loaForm.vue';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { Member } from '@/api/member';
|
||||
import LoaList from '@/components/loa/loaList.vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const user = userStore.user;
|
||||
@@ -13,8 +15,24 @@ const memberFull: Member = {
|
||||
status: null,
|
||||
status_date: null,
|
||||
};
|
||||
|
||||
const mode = ref<'submit' | 'view'>('submit')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LoaForm class="m-10" :member="memberFull"></LoaForm>
|
||||
<div class="max-w-5xl mx-auto flex w-full flex-col mt-4 mb-10">
|
||||
<div class="mb-8">
|
||||
<p class="scroll-m-20 text-2xl font-semibold tracking-tight">Leave of Absence</p>
|
||||
<div class="pt-3">
|
||||
<div class="flex w-min *:px-10 pt-2 border-b *:w-full *:text-center *:pb-1 *:cursor-pointer">
|
||||
<label :class="mode === 'submit' ? 'border-b-3 border-foreground' : 'mb-[2px]'"
|
||||
@click="mode = 'submit'">Submit</label>
|
||||
<label :class="mode === 'view' ? 'border-b-3 border-foreground' : 'mb-[2px]'"
|
||||
@click="mode = 'view'">History</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<LoaForm v-if="mode === 'submit'" :member="memberFull"></LoaForm>
|
||||
<LoaList v-if="mode === 'view'" :admin-mode="false"></LoaList>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user