Did more stuff than I even wanna write. Notably:

- Auth/account management
- Navigation system
- Admin views for LOA stuff
This commit is contained in:
2025-09-18 20:33:19 -04:00
parent 4fcd485e75
commit f708349a99
20 changed files with 2139 additions and 85 deletions

View File

@@ -1,171 +0,0 @@
<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 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>
</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>

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import LoaForm from '@/components/loa/loaForm.vue';
import LoaList from '@/components/loa/loaList.vue';
</script>
<template>
<div class="max-w-5xl mx-auto pt-10">
<!-- <LoaForm class="m-10"></LoaForm> -->
<h1>LOA Log</h1>
<LoaList></LoaList>
</div>
</template>

View File

@@ -0,0 +1,3 @@
<template>
<LoaForm class="m-10"></LoaForm>
</template>

95
ui/src/pages/Transfer.vue Normal file
View File

@@ -0,0 +1,95 @@
<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 { onMounted, ref } from "vue";
import { Member, getMembers } from "@/api/member";
import Button from "@/components/ui/button/Button.vue";
import { Status, getAllStatuses, assignStatus } from "@/api/status";
import { Rank, getRanks } from "@/api/rank";
const members = ref<Member[]>([])
const statuses = ref<Status[]>([])
const allRanks = ref<Rank[]>([])
const currentMember = ref<Member | null>(null);
const currentStatus = ref<Status | null>(null);
const currentRank = ref<Rank | null>(null);
onMounted(async () => {
members.value = await getMembers();
statuses.value = await getAllStatuses();
allRanks.value = await getRanks();
});
</script>
<template>
<div class="flex flex-row gap-6 mx-auto m-10 max-w-5xl">
<Combobox v-model="currentMember">
<ComboboxAnchor class="w-[300px]">
<ComboboxInput placeholder="Search members..." class="w-full pl-9"
:display-value="(v) => v ? v.member_name : ''" />
</ComboboxAnchor>
<ComboboxList class="w-[300px]">
<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>
<!-- Status Combobox -->
<Combobox v-model="currentStatus">
<ComboboxAnchor class="w-[300px]">
<ComboboxInput placeholder="Search statuses..." class="w-full pl-9"
:display-value="(v) => v ? v.name : ''" />
</ComboboxAnchor>
<ComboboxList class="w-[300px]">
<ComboboxEmpty class="text-muted-foreground">No results</ComboboxEmpty>
<ComboboxGroup>
<template v-for="status in statuses" :key="status.id">
<ComboboxItem :value="status"
class="data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative cursor-pointer select-none px-2 py-1.5">
{{ status.name }}
<ComboboxItemIndicator class="absolute left-2 inline-flex items-center">
<Check class="h-4 w-4" />
</ComboboxItemIndicator>
</ComboboxItem>
</template>
</ComboboxGroup>
</ComboboxList>
</Combobox>
<!-- rank -->
<Combobox v-model="currentRank">
<ComboboxAnchor class="w-[300px]">
<ComboboxInput placeholder="Search ranks..." class="w-full pl-9"
:display-value="(v) => v ? v.short_name : ''" />
</ComboboxAnchor>
<ComboboxList class="w-[300px]">
<ComboboxEmpty class="text-muted-foreground">No results</ComboboxEmpty>
<ComboboxGroup>
<template v-for="rank in allRanks" :key="rank.id">
<ComboboxItem :value="rank"
class="data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative cursor-pointer select-none px-2 py-1.5">
{{ rank.short_name }}
<ComboboxItemIndicator class="absolute left-2 inline-flex items-center">
<Check class="h-4 w-4" />
</ComboboxItemIndicator>
</ComboboxItem>
</template>
</ComboboxGroup>
</ComboboxList>
</Combobox>
<Button :onClick="() => { }">Submit</Button>
</div>
</template>

View File

@@ -11,22 +11,24 @@ import {
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { computed, ref } from "vue";
import { Member, getMembers } from "@/api/member";
import { useRouter } from 'vue-router';
import { Ellipsis } from "lucide-vue-next";
import Input from "@/components/ui/input/Input.vue";
import LoaForm from "@/components/loa/loaForm.vue";
const members = ref<Member[]>([]);
const router = useRouter();
@@ -46,9 +48,24 @@ const searchedMembers = computed(() => {
return members.value.filter(member => member.member_name.toLowerCase().includes(searchVal.value.toLowerCase()));
});
// page state systems
const showLOADialog = ref(false);
const LOAuserId = ref<number | null>(null);
</script>
<template>
<Dialog v-model:open="showLOADialog" v-on:update:open="showLOADialog = false">
<DialogContent>
<DialogHeader>
<DialogTitle>LOA Menu</DialogTitle>
<DialogDescription>
Something something flavor text.
</DialogDescription>
</DialogHeader>
<LoaForm :adminMode="true"></LoaForm>
</DialogContent>
</Dialog>
<!-- table menu -->
<div class="w-4xl mx-auto">
<div class="flex justify-between mb-4">
@@ -72,6 +89,8 @@ const searchedMembers = computed(() => {
</TableCell>
<TableCell>{{ member.rank }}</TableCell>
<TableCell>{{ member.status }}</TableCell>
<TableCell>{{ member.status }}</TableCell>
<TableCell>{{ member.status }}</TableCell>
<TableCell @click.stop="console.log('hi')" class="text-right">
<DropdownMenu>
<DropdownMenuTrigger class="cursor-pointer">
@@ -80,7 +99,7 @@ const searchedMembers = computed(() => {
<DropdownMenuContent>
<DropdownMenuItem>Change Rank</DropdownMenuItem>
<DropdownMenuItem>Transfer</DropdownMenuItem>
<DropdownMenuItem>LOA</DropdownMenuItem>
<DropdownMenuItem @click="showLOADialog = true">LOA</DropdownMenuItem>
<DropdownMenuItem :variant="'destructive'">Retire</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>