Added support for challenges and tweaked qual awarding rules

This commit is contained in:
2026-05-22 09:00:32 -04:00
parent 7532436b9a
commit f8b1811b74
10 changed files with 259 additions and 54 deletions
@@ -34,6 +34,7 @@ const { handleSubmit, resetForm, errors, values, setFieldValue } = useForm({
validateOnMount: false,
initialValues: {
course_id: null,
is_challenge: false,
event_date: "",
remarks: "",
attendees: [],
@@ -59,6 +60,17 @@ watch(() => values.course_id, (newCourseId, oldCourseId) => {
});
});
watch(() => values.is_challenge, (isChallenge) => {
if (!isChallenge) {
return;
}
values.attendees.forEach((a, index) => {
// @ts-ignore
setFieldValue(`attendees[${index}].passed_bookwork`, false);
});
});
const submitForm = handleSubmit(onSubmit);
function toMySQLDateTime(date: Date): string {
@@ -92,6 +104,15 @@ async function onSubmit(vals) {
const { remove, push, fields } = useFieldArray('attendees');
const selectedCourse = computed<Course | undefined>(() => { return trainings.value?.find(c => c.id == values.course_id) })
const bookworkDisabledMessage = computed(() => {
if (values.is_challenge) {
return "Bookwork is waived for challenge reports";
}
if (!selectedCourse.value?.hasBookwork) {
return "This course does not have bookwork";
}
return "";
});
const trainings = ref<Course[] | null>(null);
const members = ref<MemberLight[] | null>(null);
@@ -195,6 +216,24 @@ const filteredMembers = computed(() => {
</VeeField>
</FieldGroup>
</div>
<div class="w-[190px]">
<FieldGroup>
<VeeField name="is_challenge">
<Field>
<FieldLabel class="scroll-m-20 text-lg tracking-tight">Challenge</FieldLabel>
<div class="h-9 px-2 border rounded flex items-center gap-2">
<Checkbox :model-value="values.is_challenge"
@update:model-value="(v) => setFieldValue('is_challenge', !!v)"></Checkbox>
<p class="text-sm text-muted-foreground">Mark report as challenge</p>
</div>
<FieldDescription class="mt-2 text-xs">
Challenge waives bookwork, but qualification pass is still required.
</FieldDescription>
</Field>
</VeeField>
</FieldGroup>
</div>
</div>
<VeeFieldArray name="attendees" v-slot="{ fields, push, remove }">
@@ -335,9 +374,9 @@ const filteredMembers = computed(() => {
<VeeField v-slot="{ field }" :name="`attendees[${index}].passed_bookwork`" type="checkbox"
:value="false" :unchecked-value="true">
<div class="flex flex-col items-center">
<Tooltip :open="!selectedCourse?.hasBookwork"
message="This course does not have bookwork">
<Checkbox :disabled="!selectedCourse?.hasBookwork"
<Tooltip :open="!!bookworkDisabledMessage"
:message="bookworkDisabledMessage">
<Checkbox :disabled="!selectedCourse?.hasBookwork || values.is_challenge"
:name="`attendees[${index}].passed_bookwork`" :model-value="!field.checked"
@update:model-value="field['onUpdate:modelValue']">
</Checkbox>
+21 -7
View File
@@ -34,6 +34,7 @@ import {
} from '@/components/ui/pagination'
import Tooltip from '@/components/tooltip/Tooltip.vue';
import { CopyLink } from '@/lib/copyLink';
import Badge from '@/components/ui/badge/Badge.vue';
enum sidePanelState { view, create, closed };
@@ -187,6 +188,10 @@ function formatDate(date: Date | string): string {
});
}
function isChallengeReport(report: { is_challenge?: boolean | number | null }): boolean {
return report?.is_challenge === true || Number(report?.is_challenge || 0) === 1;
}
function setPageSize(size: number) {
pageSize.value = size
pageNum.value = 1;
@@ -246,9 +251,12 @@ const expanded = ref<number>(null);
<button v-for="report in trainingReports" :key="`mobile-report-${report.event_id}`"
class="w-full rounded-lg border bg-card px-3 py-2 text-left transition-colors hover:bg-muted/40"
@click="openTrainingReport(report.event_id)">
<p class="font-semibold text-foreground">
{{ report.course_name.length > 35 ? report.course_shortname : report.course_name }}
</p>
<div class="flex items-center gap-2">
<p class="font-semibold text-foreground">
{{ report.course_name.length > 35 ? report.course_shortname : report.course_name }}
</p>
<Badge v-if="isChallengeReport(report)" variant="outline" class="uppercase text-xs">Challenge</Badge>
</div>
<p class="mt-1 text-sm text-muted-foreground">{{ formatDate(report.date) }}</p>
<div class="mt-2 flex items-center gap-2 text-xs text-muted-foreground">
<span class="font-medium">Posted by:</span>
@@ -274,8 +282,12 @@ const expanded = ref<number>(null);
<TableBody v-if="loaded">
<TableRow class="cursor-pointer" v-for="report in trainingReports" :key="report.event_id"
@click="openTrainingReport(report.event_id)">
<TableCell class="font-medium">{{ report.course_name.length > 30 ? report.course_shortname :
report.course_name }}</TableCell>
<TableCell class="font-medium">
<div class="flex items-center gap-2">
<span>{{ report.course_name.length > 30 ? report.course_shortname : report.course_name }}</span>
<Badge v-if="isChallengeReport(report)" variant="outline" class="uppercase text-[10px]">Challenge</Badge>
</div>
</TableCell>
<TableCell>{{ report.date.split('T')[0] }}</TableCell>
<TableCell class="text-right">
<MemberCard v-if="report.created_by" :member-id="report.created_by"></MemberCard>
@@ -341,8 +353,10 @@ const expanded = ref<number>(null);
</div>
<div v-if="TRLoaded" :class="isMobile ? 'my-3 pb-8' : 'my-5 max-h-[70vh] overflow-y-auto overflow-x-hidden scrollbar-themed'">
<div class="flex flex-col mb-5 border rounded-lg bg-muted/70 p-2 py-3 px-4">
<p class="scroll-m-20 text-xl font-semibold tracking-tight">{{ focusedTrainingReport.course_name }}
</p>
<div class="flex items-center gap-2">
<p class="scroll-m-20 text-xl font-semibold tracking-tight">{{ focusedTrainingReport.course_name }}</p>
<Badge v-if="isChallengeReport(focusedTrainingReport)" variant="outline" class="uppercase text-xs">Challenge</Badge>
</div>
<div class="mt-2 flex flex-col gap-2 text-sm sm:flex-row sm:items-center sm:gap-10">
<p class="text-muted-foreground">{{ formatDate(focusedTrainingReport.event_date) }}</p>
<div class="flex gap-2 items-center">Created by: