277 lines
13 KiB
Vue
277 lines
13 KiB
Vue
<script setup lang="ts">
|
|
import { Check, Search } from "lucide-vue-next"
|
|
import { ComboboxAnchor, ComboboxEmpty, ComboboxGroup, ComboboxInput, ComboboxItem, ComboboxItemIndicator, ComboboxList } from "@/components/ui/combobox"
|
|
import { computed, onMounted, ref, watch } from "vue";
|
|
import { Member, getMembers } from "@/api/member";
|
|
import Button from "@/components/ui/button/Button.vue";
|
|
import {
|
|
CalendarDate,
|
|
DateFormatter,
|
|
fromDate,
|
|
getLocalTimeZone,
|
|
parseDate,
|
|
today,
|
|
} from "@internationalized/date"
|
|
import type { DateRange, DateValue } 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 { cn } from "@/lib/utils";
|
|
import { CalendarIcon } from "lucide-vue-next"
|
|
import Textarea from "@/components/ui/textarea/Textarea.vue";
|
|
import { adminSubmitLOA, getLoaPolicy, getLoaTypes, submitLOA } from "@/api/loa"; // <-- import the submit function
|
|
import { LOARequest, LOAType } from "@shared/types/loa";
|
|
import { useForm, Field as VeeField } from "vee-validate";
|
|
import {
|
|
Field,
|
|
FieldContent,
|
|
FieldDescription,
|
|
FieldGroup,
|
|
FieldLabel,
|
|
} from '@/components/ui/field'
|
|
import Combobox from "../ui/combobox/Combobox.vue";
|
|
import Select from "../ui/select/Select.vue";
|
|
import SelectTrigger from "../ui/select/SelectTrigger.vue";
|
|
import SelectValue from "../ui/select/SelectValue.vue";
|
|
import SelectContent from "../ui/select/SelectContent.vue";
|
|
import SelectItem from "../ui/select/SelectItem.vue";
|
|
import FieldError from "../ui/field/FieldError.vue";
|
|
|
|
const members = ref<Member[]>([])
|
|
const loaTypes = ref<LOAType[]>();
|
|
const policyString = ref<string | null>(null);
|
|
|
|
const currentMember = ref<Member | null>(null);
|
|
|
|
const props = withDefaults(defineProps<{
|
|
adminMode?: boolean;
|
|
member?: Member | null;
|
|
}>(), {
|
|
adminMode: false,
|
|
member: null,
|
|
});
|
|
|
|
const df = new Intl.DateTimeFormat('en-US', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
});
|
|
|
|
const userStore = useUserStore()
|
|
|
|
//form stuff
|
|
import { loaSchema } from '@shared/schemas/loaSchema'
|
|
import { toTypedSchema } from "@vee-validate/zod";
|
|
import Calendar from "../ui/calendar/Calendar.vue";
|
|
import { useUserStore } from "@/stores/user";
|
|
|
|
const { handleSubmit, values, resetForm } = useForm({
|
|
validationSchema: toTypedSchema(loaSchema),
|
|
})
|
|
|
|
const onSubmit = handleSubmit(async (values) => {
|
|
console.log(values);
|
|
const out: LOARequest = {
|
|
member_id: values.member_id,
|
|
start_date: values.start_date,
|
|
end_date: values.end_date,
|
|
type_id: values.type.id,
|
|
reason: values.reason
|
|
};
|
|
if (props.adminMode) {
|
|
await adminSubmitLOA(out);
|
|
} else {
|
|
await submitLOA(out);
|
|
userStore.loadUser();
|
|
}
|
|
})
|
|
|
|
onMounted(async () => {
|
|
if (props.member) {
|
|
currentMember.value = props.member;
|
|
}
|
|
try {
|
|
if (!props.adminMode) {
|
|
let policy = await getLoaPolicy() as any;
|
|
policyString.value = policy;
|
|
policyRef.value.innerHTML = policyString.value;
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
|
|
members.value = await getMembers();
|
|
loaTypes.value = await getLoaTypes();
|
|
resetForm({ values: { member_id: currentMember.value?.member_id } });
|
|
});
|
|
|
|
const policyRef = ref<HTMLElement>(null);
|
|
|
|
const defaultPlaceholder = today(getLocalTimeZone())
|
|
|
|
const minEndDate = computed(() => {
|
|
if (values.start_date) {
|
|
return new CalendarDate(values.start_date.getFullYear(), values.start_date.getMonth() + 1, values.start_date.getDate())
|
|
} else {
|
|
return null;
|
|
}
|
|
})
|
|
|
|
const maxEndDate = computed(() => {
|
|
if (values.type && values.start_date) {
|
|
let endDateObj = new Date(values.start_date.getTime() + values.type.max_length_days * 24 * 60 * 60 * 1000);
|
|
return new CalendarDate(endDateObj.getFullYear(), endDateObj.getMonth() + 1, endDateObj.getDate())
|
|
} else {
|
|
return null;
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="flex flex-row-reverse gap-6 mx-auto w-full" :class="!adminMode ? 'max-w-5xl' : 'max-w-5xl'">
|
|
<div v-if="!adminMode" class="flex-1 flex flex-col space-x-4 rounded-md border p-4">
|
|
<p class="scroll-m-20 text-2xl font-semibold tracking-tight">LOA Policy</p>
|
|
<div ref="policyRef" class="bookstack-container">
|
|
<!-- LOA policy gets loaded here -->
|
|
</div>
|
|
</div>
|
|
<div class="flex-1 flex flex-col gap-5">
|
|
<form @submit="onSubmit" class="flex flex-col gap-2">
|
|
<div class="flex w-full gap-5">
|
|
<VeeField v-slot="{ field, errors }" name="member_id">
|
|
<Field>
|
|
<FieldContent>
|
|
<FieldLabel>Member</FieldLabel>
|
|
<Combobox :model-value="field.value" @update:model-value="field.onChange"
|
|
:disabled="!adminMode">
|
|
<ComboboxAnchor class="w-full">
|
|
<ComboboxInput placeholder="Search members..." class="w-full pl-3"
|
|
:display-value="(id) => {
|
|
const m = members.find(mem => mem.member_id === id)
|
|
return m ? m.member_name : ''
|
|
}" />
|
|
</ComboboxAnchor>
|
|
<ComboboxList class="*:w-64">
|
|
<ComboboxEmpty class="text-muted-foreground w-full">No results</ComboboxEmpty>
|
|
<ComboboxGroup>
|
|
<template v-for="member in members" :key="member.member_id">
|
|
<ComboboxItem :value="member.member_id"
|
|
class="data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative cursor-pointer select-none px-2 py-1.5 w-full">
|
|
{{ member.member_name }}
|
|
<ComboboxItemIndicator
|
|
class="absolute left-2 inline-flex items-center">
|
|
<Check class="h-4 w-4" />
|
|
</ComboboxItemIndicator>
|
|
</ComboboxItem>
|
|
</template>
|
|
</ComboboxGroup>
|
|
</ComboboxList>
|
|
</Combobox>
|
|
<div class="h-4">
|
|
<FieldError v-if="errors.length" :errors="errors"></FieldError>
|
|
</div>
|
|
</FieldContent>
|
|
</Field>
|
|
</VeeField>
|
|
<VeeField v-slot="{ field, errors }" name="type">
|
|
<Field class="w-full">
|
|
<FieldContent>
|
|
<FieldLabel>Type</FieldLabel>
|
|
<Select :model-value="field.value" @update:model-value="field.onChange">
|
|
<SelectTrigger class="w-full">
|
|
<SelectValue placeholder="Select type"></SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem v-for="type in loaTypes" :value="type">
|
|
{{ type.name }}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<div class="h-4">
|
|
<FieldError v-if="errors.length" :errors="errors"></FieldError>
|
|
</div>
|
|
</FieldContent>
|
|
</Field>
|
|
</VeeField>
|
|
</div>
|
|
<div class="flex gap-5">
|
|
<VeeField v-slot="{ field, errors }" name="start_date">
|
|
<Field>
|
|
<FieldContent>
|
|
<FieldLabel>Start Date</FieldLabel>
|
|
<Popover>
|
|
<PopoverTrigger as-child>
|
|
<Button variant="outline" :class="cn(
|
|
'w-full justify-start text-left font-normal',
|
|
!field.value && 'text-muted-foreground',
|
|
)">
|
|
<CalendarIcon class="mr-2 h-4 w-4" />
|
|
{{ field.value ? df.format(field.value) : "Pick a date" }}
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent class="w-auto p-0">
|
|
<Calendar
|
|
:model-value="field.value
|
|
? new CalendarDate(field.value.getFullYear(), field.value.getMonth() + 1, field.value.getDate()) : null"
|
|
@update:model-value="(val: CalendarDate) => field.onChange(val.toDate(getLocalTimeZone()))"
|
|
layout="month-and-year" :min-value="today(getLocalTimeZone())" />
|
|
</PopoverContent>
|
|
</Popover>
|
|
<div class="h-4">
|
|
<FieldError v-if="errors.length" :errors="errors"></FieldError>
|
|
</div>
|
|
</FieldContent>
|
|
</Field>
|
|
</VeeField>
|
|
<VeeField v-slot="{ field, errors }" name="end_date">
|
|
<Field>
|
|
<FieldContent>
|
|
<FieldLabel>End Date</FieldLabel>
|
|
<Popover>
|
|
<PopoverTrigger as-child>
|
|
<Button variant="outline" :class="cn(
|
|
'w-full justify-start text-left font-normal',
|
|
!field.value && 'text-muted-foreground',
|
|
)">
|
|
<CalendarIcon class="mr-2 h-4 w-4" />
|
|
{{ field.value ? df.format(field.value) : "Pick a date" }}
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent class="w-auto p-0">
|
|
<Calendar
|
|
:model-value="field.value ? new CalendarDate(field.value.getFullYear(), field.value.getMonth() + 1, field.value.getDate()) : null"
|
|
@update:model-value="(val: CalendarDate) => field.onChange(val.toDate(getLocalTimeZone()))"
|
|
:default-placeholder="defaultPlaceholder" :min-value="minEndDate"
|
|
:max-value="maxEndDate" layout="month-and-year">
|
|
</Calendar>
|
|
</PopoverContent>
|
|
</Popover>
|
|
<div class="h-4">
|
|
<FieldError v-if="errors.length" :errors="errors"></FieldError>
|
|
</div>
|
|
</FieldContent>
|
|
</Field>
|
|
</VeeField>
|
|
</div>
|
|
<div>
|
|
<VeeField v-slot="{ field, errors }" name="reason">
|
|
<Field>
|
|
<FieldContent>
|
|
<FieldLabel>Reason</FieldLabel>
|
|
<Textarea :model-value="field.value" @update:model-value="field.onChange"
|
|
placeholder="Reason for LOA" class="resize-none h-28"></Textarea>
|
|
<div class="h-4">
|
|
<FieldError v-if="errors.length" :errors="errors"></FieldError>
|
|
</div>
|
|
</FieldContent>
|
|
</Field>
|
|
</VeeField>
|
|
</div>
|
|
<div class="flex justify-end">
|
|
<Button type="submit">Submit</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</template> |