Did more stuff than I even wanna write. Notably:
- Auth/account management - Navigation system - Admin views for LOA stuff
This commit is contained in:
167
ui/src/components/loa/loaForm.vue
Normal file
167
ui/src/components/loa/loaForm.vue
Normal file
@@ -0,0 +1,167 @@
|
||||
<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";
|
||||
import { submitLOA } from "@/api/loa"; // <-- import the submit function
|
||||
|
||||
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>
|
||||
|
||||
const reason = ref(""); // <-- reason for LOA
|
||||
const submitting = ref(false);
|
||||
const submitError = ref<string | null>(null);
|
||||
const submitSuccess = ref(false);
|
||||
|
||||
onMounted(async () => {
|
||||
members.value = await getMembers();
|
||||
});
|
||||
|
||||
// Submit handler
|
||||
async function handleSubmit() {
|
||||
submitError.value = null;
|
||||
submitSuccess.value = false;
|
||||
submitting.value = true;
|
||||
|
||||
// Use currentMember if adminMode, otherwise use your own member id (stubbed as 89 here)
|
||||
const member_id = currentMember.value?.member_id ?? 89;
|
||||
|
||||
// Format dates as ISO strings
|
||||
const filed_date = toMariaDBDatetime(new Date());
|
||||
const start_date = toMariaDBDatetime(value.value.start?.toDate(getLocalTimeZone()));
|
||||
const end_date = toMariaDBDatetime(value.value.end?.toDate(getLocalTimeZone()));
|
||||
|
||||
if (!member_id || !filed_date || !start_date || !end_date) {
|
||||
submitError.value = "Missing required fields";
|
||||
submitting.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const req = {
|
||||
member_id,
|
||||
filed_date,
|
||||
start_date,
|
||||
end_date,
|
||||
reason: reason.value,
|
||||
};
|
||||
|
||||
const result = await submitLOA(req);
|
||||
submitting.value = false;
|
||||
|
||||
if (result.id) {
|
||||
submitSuccess.value = true;
|
||||
reason.value = "";
|
||||
} else {
|
||||
submitError.value = result.error || "Failed to submit LOA";
|
||||
}
|
||||
}
|
||||
|
||||
function toMariaDBDatetime(date: Date): string {
|
||||
return date.toISOString().slice(0, 19).replace('T', ' ');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-row-reverse gap-6 mx-auto " :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>
|
||||
</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 v-model="reason" placeholder="Reason for LOA" class="w-full resize-none" />
|
||||
<div class="flex justify-end">
|
||||
<Button :onClick="handleSubmit" :disabled="submitting" class="w-min">Submit</Button>
|
||||
</div>
|
||||
<div v-if="submitError" class="text-red-500 text-sm mt-2">{{ submitError }}</div>
|
||||
<div v-if="submitSuccess" class="text-green-500 text-sm mt-2">LOA submitted successfully!</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
59
ui/src/components/loa/loaList.vue
Normal file
59
ui/src/components/loa/loaList.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import { getAllLOAs, LOARequest } from "@/api/loa";
|
||||
import { onMounted, ref } from "vue";
|
||||
|
||||
const LOAList = ref<LOARequest[]>([]);
|
||||
|
||||
onMounted(async () => {
|
||||
LOAList.value = await getAllLOAs();
|
||||
});
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
if (!dateStr) return "";
|
||||
return new Date(dateStr).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-5xl mx-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead class="w-[100px]">Member</TableHead>
|
||||
<TableHead>Start</TableHead>
|
||||
<TableHead>End</TableHead>
|
||||
<TableHead>Reason</TableHead>
|
||||
<TableHead>Posted on</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow
|
||||
v-for="post in LOAList"
|
||||
:key="post.id"
|
||||
class="hover:bg-muted/50"
|
||||
>
|
||||
<TableCell class="font-medium">
|
||||
{{ post.name }}
|
||||
</TableCell>
|
||||
<TableCell>{{ formatDate(post.start_date) }}</TableCell>
|
||||
<TableCell>{{ formatDate(post.end_date) }}</TableCell>
|
||||
<TableCell>{{ post.reason }}</TableCell>
|
||||
<TableCell>{{ formatDate(post.filed_date) }}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</template>
|
||||
25
ui/src/components/ui/badge/Badge.vue
Normal file
25
ui/src/components/ui/badge/Badge.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script setup>
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { Primitive } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { badgeVariants } from ".";
|
||||
|
||||
const props = defineProps({
|
||||
asChild: { type: Boolean, required: false },
|
||||
as: { type: [String, Object, Function], required: false },
|
||||
variant: { type: null, required: false },
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
data-slot="badge"
|
||||
:class="cn(badgeVariants({ variant }), props.class)"
|
||||
v-bind="delegatedProps"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
24
ui/src/components/ui/badge/index.js
Normal file
24
ui/src/components/ui/badge/index.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
export { default as Badge } from "./Badge.vue";
|
||||
|
||||
export const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user