LOA UI completed (without integration)

This commit is contained in:
2025-09-15 18:32:20 -04:00
parent 456c29a731
commit 303b72a160
33 changed files with 984 additions and 7 deletions

12
api/routes/loa.js Normal file
View File

@@ -0,0 +1,12 @@
const express = require('express');
const router = express.Router();
// DB pool (same as used in api/index.js)
const pool = require('../db');
//post a new LOA
router.post("/", async (req, res) => {
})
module.exports = router;

View File

@@ -4,12 +4,7 @@ const ur = express.Router();
const pool = require('../db'); const pool = require('../db');
// Placeholder router for rank-related routes. //insert a new latest rank for a user
// Implement rank endpoints here, for example:
// router.get('/', async (req, res) => { /* ... */ });
// router.post('/change', async (req, res) => { /* ... */ });
//insert a new latest role for a user
ur.post('/', async (req, res) => { ur.post('/', async (req, res) => {
try { try {
const App = req.body?.App || {}; const App = req.body?.App || {};
@@ -33,6 +28,7 @@ ur.post('/', async (req, res) => {
} }
}); });
//get all ranks
r.get('/', async (req, res) => { r.get('/', async (req, res) => {
try { try {
const result = await pool.query('SELECT id, name, short_name, sort_id FROM ranks;'); const result = await pool.query('SELECT id, name, short_name, sort_id FROM ranks;');

View File

@@ -0,0 +1,18 @@
<script setup>
import { DialogRoot, useForwardPropsEmits } from "reka-ui";
const props = defineProps({
open: { type: Boolean, required: false },
defaultOpen: { type: Boolean, required: false },
modal: { type: Boolean, required: false },
});
const emits = defineEmits(["update:open"]);
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<DialogRoot data-slot="dialog" v-bind="forwarded">
<slot />
</DialogRoot>
</template>

View File

@@ -0,0 +1,14 @@
<script setup>
import { DialogClose } from "reka-ui";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
});
</script>
<template>
<DialogClose data-slot="dialog-close" v-bind="props">
<slot />
</DialogClose>
</template>

View File

@@ -0,0 +1,57 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { X } from "lucide-vue-next";
import {
DialogClose,
DialogContent,
DialogPortal,
useForwardPropsEmits,
} from "reka-ui";
import { cn } from "@/lib/utils";
import DialogOverlay from "./DialogOverlay.vue";
const props = defineProps({
forceMount: { type: Boolean, required: false },
disableOutsidePointerEvents: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
class: { type: null, required: false },
});
const emits = defineEmits([
"escapeKeyDown",
"pointerDownOutside",
"focusOutside",
"interactOutside",
"openAutoFocus",
"closeAutoFocus",
]);
const delegatedProps = reactiveOmit(props, "class");
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DialogPortal>
<DialogOverlay />
<DialogContent
data-slot="dialog-content"
v-bind="forwarded"
:class="
cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
props.class,
)
"
>
<slot />
<DialogClose
class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<X />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>
</DialogPortal>
</template>

View File

@@ -0,0 +1,25 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { DialogDescription, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
class: { type: null, required: false },
});
const delegatedProps = reactiveOmit(props, "class");
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DialogDescription
data-slot="dialog-description"
v-bind="forwardedProps"
:class="cn('text-muted-foreground text-sm', props.class)"
>
<slot />
</DialogDescription>
</template>

View File

@@ -0,0 +1,18 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<div
data-slot="dialog-footer"
:class="
cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', props.class)
"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,16 @@
<script setup>
import { cn } from "@/lib/utils";
const props = defineProps({
class: { type: null, required: false },
});
</script>
<template>
<div
data-slot="dialog-header"
:class="cn('flex flex-col gap-2 text-center sm:text-left', props.class)"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,29 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { DialogOverlay } from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
forceMount: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
class: { type: null, required: false },
});
const delegatedProps = reactiveOmit(props, "class");
</script>
<template>
<DialogOverlay
data-slot="dialog-overlay"
v-bind="delegatedProps"
:class="
cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
props.class,
)
"
>
<slot />
</DialogOverlay>
</template>

View File

@@ -0,0 +1,71 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { X } from "lucide-vue-next";
import {
DialogClose,
DialogContent,
DialogOverlay,
DialogPortal,
useForwardPropsEmits,
} from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
forceMount: { type: Boolean, required: false },
disableOutsidePointerEvents: { type: Boolean, required: false },
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
class: { type: null, required: false },
});
const emits = defineEmits([
"escapeKeyDown",
"pointerDownOutside",
"focusOutside",
"interactOutside",
"openAutoFocus",
"closeAutoFocus",
]);
const delegatedProps = reactiveOmit(props, "class");
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DialogPortal>
<DialogOverlay
class="fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
>
<DialogContent
:class="
cn(
'relative z-50 grid w-full max-w-lg my-8 gap-4 border border-border bg-background p-6 shadow-lg duration-200 sm:rounded-lg md:w-full',
props.class,
)
"
v-bind="forwarded"
@pointer-down-outside="
(event) => {
const originalEvent = event.detail.originalEvent;
const target = originalEvent.target;
if (
originalEvent.offsetX > target.clientWidth ||
originalEvent.offsetY > target.clientHeight
) {
event.preventDefault();
}
}
"
>
<slot />
<DialogClose
class="absolute top-4 right-4 p-0.5 transition-colors rounded-md hover:bg-secondary"
>
<X class="w-4 h-4" />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>
</DialogOverlay>
</DialogPortal>
</template>

View File

@@ -0,0 +1,25 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { DialogTitle, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
class: { type: null, required: false },
});
const delegatedProps = reactiveOmit(props, "class");
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DialogTitle
data-slot="dialog-title"
v-bind="forwardedProps"
:class="cn('text-lg leading-none font-semibold', props.class)"
>
<slot />
</DialogTitle>
</template>

View File

@@ -0,0 +1,14 @@
<script setup>
import { DialogTrigger } from "reka-ui";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
});
</script>
<template>
<DialogTrigger data-slot="dialog-trigger" v-bind="props">
<slot />
</DialogTrigger>
</template>

View File

@@ -0,0 +1,10 @@
export { default as Dialog } from "./Dialog.vue";
export { default as DialogClose } from "./DialogClose.vue";
export { default as DialogContent } from "./DialogContent.vue";
export { default as DialogDescription } from "./DialogDescription.vue";
export { default as DialogFooter } from "./DialogFooter.vue";
export { default as DialogHeader } from "./DialogHeader.vue";
export { default as DialogOverlay } from "./DialogOverlay.vue";
export { default as DialogScrollContent } from "./DialogScrollContent.vue";
export { default as DialogTitle } from "./DialogTitle.vue";
export { default as DialogTrigger } from "./DialogTrigger.vue";

View File

@@ -0,0 +1,18 @@
<script setup>
import { PopoverRoot, useForwardPropsEmits } from "reka-ui";
const props = defineProps({
defaultOpen: { type: Boolean, required: false },
open: { type: Boolean, required: false },
modal: { type: Boolean, required: false },
});
const emits = defineEmits(["update:open"]);
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<PopoverRoot data-slot="popover" v-bind="forwarded">
<slot />
</PopoverRoot>
</template>

View File

@@ -0,0 +1,15 @@
<script setup>
import { PopoverAnchor } from "reka-ui";
const props = defineProps({
reference: { type: null, required: false },
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
});
</script>
<template>
<PopoverAnchor data-slot="popover-anchor" v-bind="props">
<slot />
</PopoverAnchor>
</template>

View File

@@ -0,0 +1,63 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { PopoverContent, PopoverPortal, useForwardPropsEmits } from "reka-ui";
import { cn } from "@/lib/utils";
defineOptions({
inheritAttrs: false,
});
const props = defineProps({
forceMount: { type: Boolean, required: false },
side: { type: null, required: false },
sideOffset: { type: Number, required: false, default: 4 },
sideFlip: { type: Boolean, required: false },
align: { type: null, required: false, default: "center" },
alignOffset: { type: Number, required: false },
alignFlip: { type: Boolean, required: false },
avoidCollisions: { type: Boolean, required: false },
collisionBoundary: { type: null, required: false },
collisionPadding: { type: [Number, Object], required: false },
arrowPadding: { type: Number, required: false },
sticky: { type: String, required: false },
hideWhenDetached: { type: Boolean, required: false },
positionStrategy: { type: String, required: false },
updatePositionStrategy: { type: String, required: false },
disableUpdateOnLayoutShift: { type: Boolean, required: false },
prioritizePosition: { type: Boolean, required: false },
reference: { type: null, required: false },
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
disableOutsidePointerEvents: { type: Boolean, required: false },
class: { type: null, required: false },
});
const emits = defineEmits([
"escapeKeyDown",
"pointerDownOutside",
"focusOutside",
"interactOutside",
"openAutoFocus",
"closeAutoFocus",
]);
const delegatedProps = reactiveOmit(props, "class");
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<PopoverPortal>
<PopoverContent
data-slot="popover-content"
v-bind="{ ...forwarded, ...$attrs }"
:class="
cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border p-4 shadow-md origin-(--reka-popover-content-transform-origin) outline-hidden',
props.class,
)
"
>
<slot />
</PopoverContent>
</PopoverPortal>
</template>

View File

@@ -0,0 +1,14 @@
<script setup>
import { PopoverTrigger } from "reka-ui";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
});
</script>
<template>
<PopoverTrigger data-slot="popover-trigger" v-bind="props">
<slot />
</PopoverTrigger>
</template>

View File

@@ -0,0 +1,4 @@
export { default as Popover } from "./Popover.vue";
export { default as PopoverAnchor } from "./PopoverAnchor.vue";
export { default as PopoverContent } from "./PopoverContent.vue";
export { default as PopoverTrigger } from "./PopoverTrigger.vue";

View File

@@ -0,0 +1,107 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { RangeCalendarRoot, useForwardPropsEmits } from "reka-ui";
import { cn } from "@/lib/utils";
import {
RangeCalendarCell,
RangeCalendarCellTrigger,
RangeCalendarGrid,
RangeCalendarGridBody,
RangeCalendarGridHead,
RangeCalendarGridRow,
RangeCalendarHeadCell,
RangeCalendarHeader,
RangeCalendarHeading,
RangeCalendarNextButton,
RangeCalendarPrevButton,
} from ".";
const props = defineProps({
defaultPlaceholder: { type: null, required: false },
defaultValue: { type: Object, required: false },
modelValue: { type: [Object, null], required: false },
placeholder: { type: null, required: false },
allowNonContiguousRanges: { type: Boolean, required: false },
pagedNavigation: { type: Boolean, required: false },
preventDeselect: { type: Boolean, required: false },
maximumDays: { type: Number, required: false },
weekStartsOn: { type: Number, required: false },
weekdayFormat: { type: String, required: false },
calendarLabel: { type: String, required: false },
fixedWeeks: { type: Boolean, required: false },
maxValue: { type: null, required: false },
minValue: { type: null, required: false },
locale: { type: String, required: false },
numberOfMonths: { type: Number, required: false },
disabled: { type: Boolean, required: false },
readonly: { type: Boolean, required: false },
initialFocus: { type: Boolean, required: false },
isDateDisabled: { type: Function, required: false },
isDateUnavailable: { type: Function, required: false },
isDateHighlightable: { type: Function, required: false },
dir: { type: String, required: false },
nextPage: { type: Function, required: false },
prevPage: { type: Function, required: false },
disableDaysOutsideCurrentView: { type: Boolean, required: false },
fixedDate: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
class: { type: null, required: false },
});
const emits = defineEmits([
"update:modelValue",
"update:validModelValue",
"update:placeholder",
"update:startValue",
]);
const delegatedProps = reactiveOmit(props, "class");
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<RangeCalendarRoot
v-slot="{ grid, weekDays }"
data-slot="range-calendar"
:class="cn('p-3', props.class)"
v-bind="forwarded"
>
<RangeCalendarHeader>
<RangeCalendarHeading />
<div class="flex items-center gap-1">
<RangeCalendarPrevButton />
<RangeCalendarNextButton />
</div>
</RangeCalendarHeader>
<div class="flex flex-col gap-y-4 mt-4 sm:flex-row sm:gap-x-4 sm:gap-y-0">
<RangeCalendarGrid v-for="month in grid" :key="month.value.toString()">
<RangeCalendarGridHead>
<RangeCalendarGridRow>
<RangeCalendarHeadCell v-for="day in weekDays" :key="day">
{{ day }}
</RangeCalendarHeadCell>
</RangeCalendarGridRow>
</RangeCalendarGridHead>
<RangeCalendarGridBody>
<RangeCalendarGridRow
v-for="(weekDates, index) in month.rows"
:key="`weekDate-${index}`"
class="mt-2 w-full"
>
<RangeCalendarCell
v-for="weekDate in weekDates"
:key="weekDate.toString()"
:date="weekDate"
>
<RangeCalendarCellTrigger :day="weekDate" :month="month.value" />
</RangeCalendarCell>
</RangeCalendarGridRow>
</RangeCalendarGridBody>
</RangeCalendarGrid>
</div>
</RangeCalendarRoot>
</template>

View File

@@ -0,0 +1,31 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { RangeCalendarCell, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
date: { type: null, required: true },
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
class: { type: null, required: false },
});
const delegatedProps = reactiveOmit(props, "class");
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<RangeCalendarCell
data-slot="range-calendar-cell"
:class="
cn(
'relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:bg-accent first:[&:has([data-selected])]:rounded-l-md last:[&:has([data-selected])]:rounded-r-md [&:has([data-selected][data-selection-end])]:rounded-r-md [&:has([data-selected][data-selection-start])]:rounded-l-md',
props.class,
)
"
v-bind="forwardedProps"
>
<slot />
</RangeCalendarCell>
</template>

View File

@@ -0,0 +1,45 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { RangeCalendarCellTrigger, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";
import { buttonVariants } from '@/components/ui/button';
const props = defineProps({
day: { type: null, required: true },
month: { type: null, required: true },
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false, default: "button" },
class: { type: null, required: false },
});
const delegatedProps = reactiveOmit(props, "class");
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<RangeCalendarCellTrigger
data-slot="range-calendar-trigger"
:class="
cn(
buttonVariants({ variant: 'ghost' }),
'h-8 w-8 p-0 font-normal data-[selected]:opacity-100',
'[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground',
// Selection Start
'data-[selection-start]:bg-primary data-[selection-start]:text-primary-foreground data-[selection-start]:hover:bg-primary data-[selection-start]:hover:text-primary-foreground data-[selection-start]:focus:bg-primary data-[selection-start]:focus:text-primary-foreground',
// Selection End
'data-[selection-end]:bg-primary data-[selection-end]:text-primary-foreground data-[selection-end]:hover:bg-primary data-[selection-end]:hover:text-primary-foreground data-[selection-end]:focus:bg-primary data-[selection-end]:focus:text-primary-foreground',
// Outside months
'data-[outside-view]:text-muted-foreground',
// Disabled
'data-[disabled]:text-muted-foreground data-[disabled]:opacity-50',
// Unavailable
'data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through',
props.class,
)
"
v-bind="forwardedProps"
>
<slot />
</RangeCalendarCellTrigger>
</template>

View File

@@ -0,0 +1,25 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { RangeCalendarGrid, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
class: { type: null, required: false },
});
const delegatedProps = reactiveOmit(props, "class");
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<RangeCalendarGrid
data-slot="range-calendar-grid"
:class="cn('w-full border-collapse space-x-1', props.class)"
v-bind="forwardedProps"
>
<slot />
</RangeCalendarGrid>
</template>

View File

@@ -0,0 +1,14 @@
<script setup>
import { RangeCalendarGridBody } from "reka-ui";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
});
</script>
<template>
<RangeCalendarGridBody data-slot="range-calendar-grid-body" v-bind="props">
<slot />
</RangeCalendarGridBody>
</template>

View File

@@ -0,0 +1,14 @@
<script setup>
import { RangeCalendarGridHead } from "reka-ui";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
});
</script>
<template>
<RangeCalendarGridHead data-slot="range-calendar-grid-head" v-bind="props">
<slot />
</RangeCalendarGridHead>
</template>

View File

@@ -0,0 +1,25 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { RangeCalendarGridRow, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
class: { type: null, required: false },
});
const delegatedProps = reactiveOmit(props, "class");
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<RangeCalendarGridRow
data-slot="range-calendar-grid-row"
:class="cn('flex', props.class)"
v-bind="forwardedProps"
>
<slot />
</RangeCalendarGridRow>
</template>

View File

@@ -0,0 +1,30 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { RangeCalendarHeadCell, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
class: { type: null, required: false },
});
const delegatedProps = reactiveOmit(props, "class");
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<RangeCalendarHeadCell
data-slot="range-calendar-head-cell"
:class="
cn(
'w-8 rounded-md text-[0.8rem] font-normal text-muted-foreground',
props.class,
)
"
v-bind="forwardedProps"
>
<slot />
</RangeCalendarHeadCell>
</template>

View File

@@ -0,0 +1,27 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { RangeCalendarHeader, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
class: { type: null, required: false },
});
const delegatedProps = reactiveOmit(props, "class");
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<RangeCalendarHeader
data-slot="range-calendar-header"
:class="
cn('flex justify-center pt-1 relative items-center w-full', props.class)
"
v-bind="forwardedProps"
>
<slot />
</RangeCalendarHeader>
</template>

View File

@@ -0,0 +1,30 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { RangeCalendarHeading, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
class: { type: null, required: false },
});
defineSlots();
const delegatedProps = reactiveOmit(props, "class");
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<RangeCalendarHeading
v-slot="{ headingValue }"
data-slot="range-calendar-heading"
:class="cn('text-sm font-medium', props.class)"
v-bind="forwardedProps"
>
<slot :heading-value>
{{ headingValue }}
</slot>
</RangeCalendarHeading>
</template>

View File

@@ -0,0 +1,37 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { ChevronRight } from "lucide-vue-next";
import { RangeCalendarNext, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";
import { buttonVariants } from '@/components/ui/button';
const props = defineProps({
nextPage: { type: Function, required: false },
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
class: { type: null, required: false },
});
const delegatedProps = reactiveOmit(props, "class");
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<RangeCalendarNext
data-slot="range-calendar-next-button"
:class="
cn(
buttonVariants({ variant: 'outline' }),
'absolute right-1',
'size-7 bg-transparent p-0 opacity-50 hover:opacity-100',
props.class,
)
"
v-bind="forwardedProps"
>
<slot>
<ChevronRight class="size-4" />
</slot>
</RangeCalendarNext>
</template>

View File

@@ -0,0 +1,37 @@
<script setup>
import { reactiveOmit } from "@vueuse/core";
import { ChevronLeft } from "lucide-vue-next";
import { RangeCalendarPrev, useForwardProps } from "reka-ui";
import { cn } from "@/lib/utils";
import { buttonVariants } from '@/components/ui/button';
const props = defineProps({
prevPage: { type: Function, required: false },
asChild: { type: Boolean, required: false },
as: { type: [String, Object, Function], required: false },
class: { type: null, required: false },
});
const delegatedProps = reactiveOmit(props, "class");
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<RangeCalendarPrev
data-slot="range-calendar-prev-button"
:class="
cn(
buttonVariants({ variant: 'outline' }),
'absolute left-1',
'size-7 bg-transparent p-0 opacity-50 hover:opacity-100',
props.class,
)
"
v-bind="forwardedProps"
>
<slot>
<ChevronLeft class="size-4" />
</slot>
</RangeCalendarPrev>
</template>

View File

@@ -0,0 +1,12 @@
export { default as RangeCalendar } from "./RangeCalendar.vue";
export { default as RangeCalendarCell } from "./RangeCalendarCell.vue";
export { default as RangeCalendarCellTrigger } from "./RangeCalendarCellTrigger.vue";
export { default as RangeCalendarGrid } from "./RangeCalendarGrid.vue";
export { default as RangeCalendarGridBody } from "./RangeCalendarGridBody.vue";
export { default as RangeCalendarGridHead } from "./RangeCalendarGridHead.vue";
export { default as RangeCalendarGridRow } from "./RangeCalendarGridRow.vue";
export { default as RangeCalendarHeadCell } from "./RangeCalendarHeadCell.vue";
export { default as RangeCalendarHeader } from "./RangeCalendarHeader.vue";
export { default as RangeCalendarHeading } from "./RangeCalendarHeading.vue";
export { default as RangeCalendarNextButton } from "./RangeCalendarNextButton.vue";
export { default as RangeCalendarPrevButton } from "./RangeCalendarPrevButton.vue";

122
ui/src/pages/LOA.vue Normal file
View File

@@ -0,0 +1,122 @@
<script setup lang="ts">
import { Check, Search } from "lucide-vue-next"
import { Combobox, ComboboxAnchor, ComboboxEmpty, ComboboxGroup, ComboboxInput, ComboboxItem, ComboboxItemIndicator, ComboboxList } from "@/components/ui/combobox"
import { getRanks, Rank } from "@/api/rank"
import { onMounted, ref } from "vue";
import { Member, getMembers } from "@/api/member";
import Button from "@/components/ui/button/Button.vue";
import {
CalendarDate,
DateFormatter,
getLocalTimeZone,
} from "@internationalized/date"
import type { DateRange } from "reka-ui"
import type { Ref } from "vue"
import Popover from "@/components/ui/popover/Popover.vue";
import PopoverTrigger from "@/components/ui/popover/PopoverTrigger.vue";
import PopoverContent from "@/components/ui/popover/PopoverContent.vue";
import { RangeCalendar } from "@/components/ui/range-calendar"
import { cn } from "@/lib/utils";
import { CalendarIcon } from "lucide-vue-next"
import Input from "@/components/ui/input/Input.vue";
import Textarea from "@/components/ui/textarea/Textarea.vue";
import Separator from "@/components/ui/separator/Separator.vue";
const members = ref<Member[]>([])
const currentMember = ref<Member | null>(null);
defineProps({
adminMode: {
type: Boolean,
default: false
}
})
const df = new DateFormatter("en-US", {
dateStyle: "medium",
})
const value = ref({
start: new CalendarDate(2022, 1, 20),
end: new CalendarDate(2022, 1, 20).add({ days: 20 }),
}) as Ref<DateRange>
onMounted(async () => {
members.value = await getMembers();
});
</script>
<template>
<div class="flex flex-row-reverse gap-6 mx-auto m-10" :class="!adminMode ? 'max-w-5xl' : 'max-w-2xl'">
<div v-if="!adminMode" class="flex-1 flex space-x-4 rounded-md border p-4">
<div class="flex-2 space-y-1">
<p class="text-sm font-medium leading-none">
LOA Policy
</p>
<p class="text-sm text-muted-foreground">
Policy goes here.
</p>
</div>
<Switch />
</div>
<div class="flex-1 flex flex-col gap-5">
<div class="flex w-full gap-5 ">
<Combobox class="w-1/2" v-model="currentMember" :disabled="!adminMode">
<ComboboxAnchor class="w-full">
<ComboboxInput placeholder="Search members..." class="w-full pl-9"
:display-value="(v) => v ? v.member_name : ''" />
</ComboboxAnchor>
<ComboboxList class="w-full">
<ComboboxEmpty class="text-muted-foreground">No results</ComboboxEmpty>
<ComboboxGroup>
<template v-for="member in members" :key="member.member_id">
<ComboboxItem :value="member"
class="data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative cursor-pointer select-none px-2 py-1.5">
{{ member.member_name }}
<ComboboxItemIndicator class="absolute left-2 inline-flex items-center">
<Check class="h-4 w-4" />
</ComboboxItemIndicator>
</ComboboxItem>
</template>
</ComboboxGroup>
</ComboboxList>
</Combobox>
<Popover>
<PopoverTrigger as-child>
<Button variant="outline" :class="cn(
'w-1/2 justify-start text-left font-normal',
!value && 'text-muted-foreground',
)">
<CalendarIcon class="mr-2 h-4 w-4" />
<template v-if="value.start">
<template v-if="value.end">
{{ df.format(value.start.toDate(getLocalTimeZone())) }} - {{
df.format(value.end.toDate(getLocalTimeZone())) }}
</template>
<template v-else>
{{ df.format(value.start.toDate(getLocalTimeZone())) }}
</template>
</template>
<template v-else>
Pick a date
</template>
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<RangeCalendar v-model="value" initial-focus :number-of-months="2"
@update:start-value="(startDate) => value.start = startDate" />
</PopoverContent>
</Popover>
</div>
<Textarea placeholder="Reason for LOA" class="w-full resize-none" />
<div class="flex justify-end">
<Button :onClick="() => { }" class="w-min">Submit</Button>
</div>
</div>
</div>
</template>

View File

@@ -3,6 +3,7 @@ import ManageApplications from '@/pages/ManageApplications.vue'
import Application from '@/pages/Application.vue' import Application from '@/pages/Application.vue'
import RankChange from '@/pages/RankChange.vue' import RankChange from '@/pages/RankChange.vue'
import MemberList from '@/pages/memberList.vue' import MemberList from '@/pages/memberList.vue'
import LOA from '@/pages/LOA.vue'
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@@ -10,7 +11,8 @@ const router = createRouter({
{ path: '/applications', component: ManageApplications }, { path: '/applications', component: ManageApplications },
{ path: '/applications/:id', component: Application }, { path: '/applications/:id', component: Application },
{ path: '/changeRank', component: RankChange }, { path: '/changeRank', component: RankChange },
{ path: '/members', component: MemberList} { path: '/members', component: MemberList},
{ path: '/loa', component: LOA}
] ]
}) })