Refactor attendance page, add components for the check and the status

This commit is contained in:
Ramon Wenger 2024-11-13 16:50:52 +01:00
parent abe60e7468
commit f8b8347bb9
4 changed files with 196 additions and 157 deletions

View File

@ -1,27 +1,29 @@
<script setup lang="ts"> <script setup lang="ts">
import ItCheckbox from "@/components/ui/ItCheckbox.vue"; import ItCheckbox from "@/components/ui/ItCheckbox.vue";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import ItPersonRow from "@/components/ui/ItPersonRow.vue"; import ItPersonRow from "@/components/ui/ItPersonRow.vue";
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables"; import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
import type { AttendanceUserStatus } from "@/gql/graphql"; import type {
import { graphqlClient } from "@/graphql/client"; AttendanceUserStatus,
CourseSessionAttendanceCourseObjectType,
} from "@/gql/graphql";
import { ATTENDANCE_CHECK_MUTATION } from "@/graphql/mutations"; import { ATTENDANCE_CHECK_MUTATION } from "@/graphql/mutations";
import { ATTENDANCE_CHECK_QUERY } from "@/graphql/queries"; import { ATTENDANCE_CHECK_QUERY } from "@/graphql/queries";
import { exportAttendance } from "@/services/dashboard"; import { exportAttendance } from "@/services/dashboard";
import { useExpertCockpitStore } from "@/stores/expertCockpit";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import type { DropdownSelectable } from "@/types";
import { openDataAsXls } from "@/utils/export"; import { openDataAsXls } from "@/utils/export";
import { useMutation } from "@urql/vue"; import { useMutation, useQuery } from "@urql/vue";
import dayjs from "dayjs"; import { useDateFormat } from "@vueuse/core";
import { useTranslation } from "i18next-vue";
import log from "loglevel"; import log from "loglevel";
import { computed, onMounted, reactive, watch } from "vue"; import { computed, onMounted, ref } from "vue";
import AttendanceCheck from "../cockpitPage/AttendanceCheck.vue";
import AttendanceStatus from "../cockpitPage/AttendanceStatus.vue";
const { t } = useTranslation();
const attendanceMutation = useMutation(ATTENDANCE_CHECK_MUTATION); const attendanceMutation = useMutation(ATTENDANCE_CHECK_MUTATION);
const courseSessionDetailResult = useCourseSessionDetailQuery(); const courseSessionDetailResult = useCourseSessionDetailQuery();
const userStore = useUserStore(); const userStore = useUserStore();
const courseSession = useCurrentCourseSession(); const courseSession = useCurrentCourseSession();
const expertCockpitStore = useExpertCockpitStore();
const attendanceCourses = computed(() => { const attendanceCourses = computed(() => {
return courseSessionDetailResult.courseSessionDetail.value?.attendance_courses ?? []; return courseSessionDetailResult.courseSessionDetail.value?.attendance_courses ?? [];
@ -33,44 +35,28 @@ const courseSessionDetail = computed(() => {
const attendanceCourseCircleId = computed(() => { const attendanceCourseCircleId = computed(() => {
const selectedAttendandeCourse = attendanceCourses.value.find( const selectedAttendandeCourse = attendanceCourses.value.find(
(course) => course.id === state.attendanceCourseSelected.id (course) => course.id === currentCourse.value.id
); );
return selectedAttendandeCourse?.learning_content?.circle?.id; return selectedAttendandeCourse?.learning_content?.circle?.id;
}); });
const presenceCoursesDropdownOptions = computed(() => { // const currentCircle = computed(() => expertCockpitStore.currentCircle);
return attendanceCourses.value.map(
(attendanceCourse) =>
({
id: attendanceCourse.id,
name: `${t("a.Präsenzkurs")} ${
attendanceCourse.learning_content.circle?.title
} ${dayjs(attendanceCourse.due_date?.start).format("DD.MM.YYYY")}`,
}) as DropdownSelectable
);
});
const state = reactive({ const currentCourse = computed(() => expertCockpitStore.currentCourse);
userPresence: new Map<string, boolean>(), // const currentCourse = computed(() => {
attendanceCourseSelected: presenceCoursesDropdownOptions.value[0], // return attendanceCourses.value.find(
disclaimerConfirmed: false, // (i) => i.learning_content.circle?.id == currentCircle.value?.id
attendanceSaved: false, // );
}); // });
watch( const userPresence = ref(new Map<string, boolean>());
attendanceCourses, const disclaimerConfirmed = ref(false);
(newVal) => { const attendanceSaved = ref(false);
if (newVal && newVal.length > 0) {
state.attendanceCourseSelected = presenceCoursesDropdownOptions.value[0];
}
},
{ immediate: true }
);
function resetState() { function resetState() {
state.userPresence = new Map<string, boolean>(); userPresence.value = new Map<string, boolean>();
state.disclaimerConfirmed = false; disclaimerConfirmed.value = false;
state.attendanceSaved = false; attendanceSaved.value = false;
} }
const onSubmit = async () => { const onSubmit = async () => {
@ -78,54 +64,62 @@ const onSubmit = async () => {
user_id: string; user_id: string;
status: AttendanceUserStatus; status: AttendanceUserStatus;
}; };
const attendanceUserList: UserPresence[] = Array.from(state.userPresence.keys()).map( console.log(Array.from(userPresence.value.keys()));
const attendanceUserList: UserPresence[] = Array.from(userPresence.value.keys()).map(
(key) => ({ (key) => ({
user_id: key, user_id: key,
status: state.userPresence.get(key) ? "PRESENT" : "ABSENT", status: userPresence.value.get(key) ? "PRESENT" : "ABSENT",
}) })
); );
console.log(attendanceUserList);
const res = await attendanceMutation.executeMutation({ const res = await attendanceMutation.executeMutation({
attendanceCourseId: state.attendanceCourseSelected.id.toString(), attendanceCourseId: (
currentCourse.value as CourseSessionAttendanceCourseObjectType
).id.toString(),
attendanceUserList: attendanceUserList, attendanceUserList: attendanceUserList,
}); });
if (res.error) { if (res.error) {
log.error("Could not submit attendance check: ", res.error); log.error("Could not submit attendance check: ", res.error);
return; return;
} }
state.disclaimerConfirmed = false; disclaimerConfirmed.value = false;
state.attendanceSaved = true; attendanceSaved.value = true;
log.info("Attendance check submitted: ", res); log.info("Attendance check submitted: ", res);
}; };
const loadAttendanceData = async () => { const loadAttendanceData = async () => {
resetState(); resetState();
// with changing variables `useQuery` does not seem to work correctly // with changing variables `useQuery` does not seem to work correctly
if (state.attendanceCourseSelected) { if (currentCourse.value) {
const res = await graphqlClient.query( const result = await useQuery({
ATTENDANCE_CHECK_QUERY, query: ATTENDANCE_CHECK_QUERY,
{ variables: {
courseSessionId: state.attendanceCourseSelected.id.toString(), courseSessionId: currentCourse.value.id.toString(),
}, },
{ requestPolicy: "network-only",
requestPolicy: "network-only", });
} console.log(result.data.value?.course_session_attendance_course);
);
const attendanceUserList = const attendanceUserList =
res.data?.course_session_attendance_course?.attendance_user_list ?? []; result.data?.value?.course_session_attendance_course?.attendance_user_list ?? [];
console.log(attendanceUserList);
for (const user of attendanceUserList) { for (const user of attendanceUserList) {
if (!user) continue; if (!user) continue;
state.userPresence.set(user.user_id, user.status === "PRESENT"); userPresence.value.set(user.user_id, user.status === "PRESENT");
} }
if (attendanceUserList.length !== 0) { if (attendanceUserList.length !== 0) {
state.attendanceSaved = true; attendanceSaved.value = true;
} }
} }
}; };
function editAgain() { function editAgain() {
state.attendanceSaved = false; attendanceSaved.value = false;
} }
const toggleDisclaimer = (newValue: boolean) => {
disclaimerConfirmed.value = newValue;
};
async function exportData() { async function exportData() {
const data = await exportAttendance( const data = await exportAttendance(
{ {
@ -137,19 +131,26 @@ async function exportData() {
openDataAsXls(data.encoded_data, data.file_name); openDataAsXls(data.encoded_data, data.file_name);
} }
onMounted(() => { onMounted(async () => {
log.debug("AttendanceCheckPage mounted"); log.debug("AttendanceCheckPage mounted");
loadAttendanceData(); loadAttendanceData();
}); });
watch( const courseDueDate = computed(() => {
() => state.attendanceCourseSelected, if (currentCourse.value && currentCourse.value.due_date?.start) {
() => { return currentCourse.value.due_date.start;
log.debug("attendanceCourseSelected changed", state.attendanceCourseSelected); }
loadAttendanceData(); return "";
}, });
{ immediate: true }
); const formattedCourseDueDate = computed(() => {
if (courseDueDate.value) {
return useDateFormat(courseDueDate.value, "D. MMMM YYYY", {
locales: "de-CH",
});
}
return "";
});
</script> </script>
<template> <template>
@ -165,9 +166,8 @@ watch(
</router-link> </router-link>
</nav> </nav>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h3 class="pb-4 text-xl font-bold">{{ $t("a.Anwesenheit Präsenzkurse") }}</h3>
<button <button
v-if="state.attendanceSaved" v-if="attendanceSaved"
class="flex" class="flex"
data-cy="export-button" data-cy="export-button"
@click="exportData" @click="exportData"
@ -176,54 +176,32 @@ watch(
<span class="ml inline-block">{{ $t("a.Als Excel exportieren") }}</span> <span class="ml inline-block">{{ $t("a.Als Excel exportieren") }}</span>
</button> </button>
</div> </div>
<section v-if="attendanceCourses.length && state.attendanceCourseSelected"> <section v-if="attendanceCourses.length && currentCourse">
<div class="flex flex-row flex-wrap justify-between bg-white p-6"> <div class="flex flex-col justify-between gap-8 bg-white py-6">
<ItDropdownSelect <div class="flex flex-col gap-2 px-6">
v-model="state.attendanceCourseSelected" <h3 class="pb-1 text-4xl font-bold">{{ $t("a.Präsenzkurs") }}</h3>
:items="presenceCoursesDropdownOptions ?? []" <h5>Circle "{{ currentCourse?.learning_content.circle?.title }}"</h5>
></ItDropdownSelect> <h5>{{ formattedCourseDueDate }}</h5>
<div
v-if="!state.attendanceSaved"
class="flex flex-row flex-wrap items-center space-y-2 md:space-y-0"
>
<ItCheckbox
:checkbox-item="{
value: true,
checked: state.disclaimerConfirmed,
}"
@toggle="state.disclaimerConfirmed = !state.disclaimerConfirmed"
></ItCheckbox>
<p class="w-64 pr-4 text-sm">
{{
$t(
"Ich will die Anwesenheit der untenstehenden Personen definitiv bestätigen."
)
}}
</p>
<button
class="btn-primary"
:disabled="!state.disclaimerConfirmed"
@click="onSubmit"
>
{{ $t("Anwesenheit bestätigen") }}
</button>
</div> </div>
<div v-else class="self-center"> <div class="px-6">
<p class="text-base"> <AttendanceStatus
{{ $t("a.Die Anwesenheit wurde definitiv bestätigt") }} class="px-6"
</p> :done="attendanceSaved"
<button class="btn-link link" @click="editAgain()"> :date="courseDueDate"
{{ $t("a.Erneut bearbeiten") }} />
</button>
</div> </div>
</div> <AttendanceCheck
:attendance-saved="attendanceSaved"
:disclaimer-confirmed="disclaimerConfirmed"
@reopen="editAgain"
@toggle="toggleDisclaimer"
@confirm="onSubmit"
/>
<div class="mt-4 flex flex-col bg-white p-6"> <div class="border-t border-gray-500 px-6">
<div
v-for="(csu, index) in courseSessionDetailResult.filterMembers()"
:key="csu.user_id"
>
<ItPersonRow <ItPersonRow
v-for="(csu, index) in courseSessionDetailResult.filterMembers()"
:key="csu.user_id"
:name="`${csu.first_name} ${csu.last_name}`" :name="`${csu.first_name} ${csu.last_name}`"
:avatar-url="csu.avatar_url" :avatar-url="csu.avatar_url"
:class="0 === index ? 'border-none' : ''" :class="0 === index ? 'border-none' : ''"
@ -233,16 +211,13 @@ watch(
> >
<template #leading> <template #leading>
<ItCheckbox <ItCheckbox
:disabled="state.attendanceSaved" :disabled="attendanceSaved"
:checkbox-item="{ :checkbox-item="{
value: true, value: true,
checked: state.userPresence.get(csu.user_id) as boolean, checked: userPresence.get(csu.user_id) as boolean,
}" }"
@toggle=" @toggle="
state.userPresence.set( userPresence.set(csu.user_id, !userPresence.get(csu.user_id))
csu.user_id,
!state.userPresence.get(csu.user_id)
)
" "
></ItCheckbox> ></ItCheckbox>
</template> </template>

View File

@ -0,0 +1,45 @@
<script setup lang="ts">
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
export interface Props {
attendanceSaved: boolean;
disclaimerConfirmed: boolean;
}
defineProps<Props>();
defineEmits(["toggle", "reopen", "confirm"]);
</script>
<template>
<div v-if="!attendanceSaved" class="flex flex-col gap-4 px-6">
<div class="flex flex-row content-center items-center">
<ItCheckbox
:checkbox-item="{
value: true,
checked: disclaimerConfirmed,
}"
@toggle="$emit('toggle', !disclaimerConfirmed)"
></ItCheckbox>
<p class="text-sm">
{{
$t(
"Ich will die Anwesenheit der untenstehenden Personen definitiv bestätigen."
)
}}
</p>
</div>
<button
class="btn-primary w-64"
:disabled="!disclaimerConfirmed"
@click="$emit('confirm')"
>
{{ $t("Anwesenheit bestätigen") }}
</button>
</div>
<div v-else class="px-6">
<p class="text-base">
{{ $t("a.Die Anwesenheit wurde definitiv bestätigt") }}
</p>
<button class="btn-link link" @click="$emit('reopen')">
{{ $t("a.Erneut bearbeiten") }}
</button>
</div>
</template>

View File

@ -68,7 +68,7 @@ const text = computed(() => {
<template> <template>
<div <div
class="space-between flex flex-row items-center gap-1 rounded py-1 pl-2 pr-4" class="space-between inline-flex flex-row items-center gap-1 rounded py-1 pl-2 pr-4"
:class="style" :class="style"
> >
<component :is="icon" class="h-7 w-7" /> <component :is="icon" class="h-7 w-7" />

View File

@ -1,50 +1,15 @@
import { useCourseData } from "@/composables"; import { useCourseData, useCourseSessionDetailQuery } from "@/composables";
import type { CourseSessionAttendanceCourseObjectType } from "@/gql/graphql";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import type { CircleLight, CourseSessionUser, ExpertSessionUser } from "@/types"; import type { CircleLight, CourseSessionUser, ExpertSessionUser } from "@/types";
import log from "loglevel"; import log from "loglevel";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { computed, ref } from "vue";
type CircleExpertCockpit = CircleLight & { type CircleExpertCockpit = CircleLight & {
name: string; name: string;
}; };
export type ExpertCockpitStoreState = {
courseSessionMembers: CourseSessionUser[] | undefined;
circles: CircleExpertCockpit[] | undefined;
currentCircle: CircleExpertCockpit | undefined;
};
export const useExpertCockpitStore = defineStore({
id: "expertCockpit",
state: () => {
return {
courseSessionMembers: undefined,
circles: [],
currentCircle: undefined,
} as ExpertCockpitStoreState;
},
actions: {
async loadCircles(
courseSlug: string,
currentCourseSessionUser: CourseSessionUser | undefined
) {
log.debug("loadCircles called", courseSlug);
this.circles = await courseCircles(courseSlug, currentCourseSessionUser);
if (this.circles?.length) {
await this.setCurrentCourseCircle(this.circles[0].slug);
}
},
async setCurrentCourseCircle(circleSlug: string) {
this.currentCircle = this.circles?.find((c) => c.slug === circleSlug);
},
async setCurrentCourseCircleFromEvent(event: CircleLight) {
await this.setCurrentCourseCircle(event.slug);
},
},
});
async function courseCircles( async function courseCircles(
courseSlug: string, courseSlug: string,
currentCourseSessionUser: CourseSessionUser | undefined currentCourseSessionUser: CourseSessionUser | undefined
@ -74,3 +39,57 @@ async function courseCircles(
return []; return [];
} }
export type ExpertCockpitStoreState = {
courseSessionMembers: CourseSessionUser[] | undefined;
circles: CircleExpertCockpit[] | undefined;
currentCircle: CircleExpertCockpit | undefined;
};
export const useExpertCockpitStore = defineStore("expertCockpit", () => {
const courseSessionMembers = ref<CourseSessionUser[] | undefined>(undefined);
const circles = ref<CircleExpertCockpit[] | undefined>([]);
const currentCircle = ref<CircleExpertCockpit | undefined>(undefined);
const courseSessionDetailResult = useCourseSessionDetailQuery();
const attendanceCourses = computed(() => {
return (
courseSessionDetailResult.courseSessionDetail.value?.attendance_courses ?? []
);
});
const currentCourse = computed(() => {
return attendanceCourses.value.find(
(i) => i.learning_content.circle?.id == currentCircle.value?.id
);
});
const loadCircles = async (
courseSlug: string,
currentCourseSessionUser: CourseSessionUser | undefined
) => {
log.debug("loadCircles called", courseSlug);
circles.value = await courseCircles(courseSlug, currentCourseSessionUser);
if (circles.value?.length) {
await setCurrentCourseCircle(circles.value[0].slug);
}
};
const setCurrentCourseCircle = async (circleSlug: string) => {
currentCircle.value = circles.value?.find((c) => c.slug === circleSlug);
};
const setCurrentCourseCircleFromEvent = async (event: CircleLight) => {
await setCurrentCourseCircle(event.slug);
};
return {
courseSessionMembers,
circles,
currentCircle,
loadCircles,
currentCourse,
setCurrentCourseCircleFromEvent,
};
});