LOA UI completed (without integration)
This commit is contained in:
12
api/routes/loa.js
Normal file
12
api/routes/loa.js
Normal 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;
|
||||||
@@ -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;');
|
||||||
|
|||||||
18
ui/src/components/ui/dialog/Dialog.vue
Normal file
18
ui/src/components/ui/dialog/Dialog.vue
Normal 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>
|
||||||
14
ui/src/components/ui/dialog/DialogClose.vue
Normal file
14
ui/src/components/ui/dialog/DialogClose.vue
Normal 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>
|
||||||
57
ui/src/components/ui/dialog/DialogContent.vue
Normal file
57
ui/src/components/ui/dialog/DialogContent.vue
Normal 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>
|
||||||
25
ui/src/components/ui/dialog/DialogDescription.vue
Normal file
25
ui/src/components/ui/dialog/DialogDescription.vue
Normal 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>
|
||||||
18
ui/src/components/ui/dialog/DialogFooter.vue
Normal file
18
ui/src/components/ui/dialog/DialogFooter.vue
Normal 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>
|
||||||
16
ui/src/components/ui/dialog/DialogHeader.vue
Normal file
16
ui/src/components/ui/dialog/DialogHeader.vue
Normal 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>
|
||||||
29
ui/src/components/ui/dialog/DialogOverlay.vue
Normal file
29
ui/src/components/ui/dialog/DialogOverlay.vue
Normal 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>
|
||||||
71
ui/src/components/ui/dialog/DialogScrollContent.vue
Normal file
71
ui/src/components/ui/dialog/DialogScrollContent.vue
Normal 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>
|
||||||
25
ui/src/components/ui/dialog/DialogTitle.vue
Normal file
25
ui/src/components/ui/dialog/DialogTitle.vue
Normal 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>
|
||||||
14
ui/src/components/ui/dialog/DialogTrigger.vue
Normal file
14
ui/src/components/ui/dialog/DialogTrigger.vue
Normal 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>
|
||||||
10
ui/src/components/ui/dialog/index.js
Normal file
10
ui/src/components/ui/dialog/index.js
Normal 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";
|
||||||
18
ui/src/components/ui/popover/Popover.vue
Normal file
18
ui/src/components/ui/popover/Popover.vue
Normal 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>
|
||||||
15
ui/src/components/ui/popover/PopoverAnchor.vue
Normal file
15
ui/src/components/ui/popover/PopoverAnchor.vue
Normal 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>
|
||||||
63
ui/src/components/ui/popover/PopoverContent.vue
Normal file
63
ui/src/components/ui/popover/PopoverContent.vue
Normal 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>
|
||||||
14
ui/src/components/ui/popover/PopoverTrigger.vue
Normal file
14
ui/src/components/ui/popover/PopoverTrigger.vue
Normal 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>
|
||||||
4
ui/src/components/ui/popover/index.js
Normal file
4
ui/src/components/ui/popover/index.js
Normal 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";
|
||||||
107
ui/src/components/ui/range-calendar/RangeCalendar.vue
Normal file
107
ui/src/components/ui/range-calendar/RangeCalendar.vue
Normal 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>
|
||||||
31
ui/src/components/ui/range-calendar/RangeCalendarCell.vue
Normal file
31
ui/src/components/ui/range-calendar/RangeCalendarCell.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
25
ui/src/components/ui/range-calendar/RangeCalendarGrid.vue
Normal file
25
ui/src/components/ui/range-calendar/RangeCalendarGrid.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
25
ui/src/components/ui/range-calendar/RangeCalendarGridRow.vue
Normal file
25
ui/src/components/ui/range-calendar/RangeCalendarGridRow.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
27
ui/src/components/ui/range-calendar/RangeCalendarHeader.vue
Normal file
27
ui/src/components/ui/range-calendar/RangeCalendarHeader.vue
Normal 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>
|
||||||
30
ui/src/components/ui/range-calendar/RangeCalendarHeading.vue
Normal file
30
ui/src/components/ui/range-calendar/RangeCalendarHeading.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
12
ui/src/components/ui/range-calendar/index.js
Normal file
12
ui/src/components/ui/range-calendar/index.js
Normal 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
122
ui/src/pages/LOA.vue
Normal 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>
|
||||||
@@ -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}
|
||||||
]
|
]
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user