Move document upload to cockpit

This commit is contained in:
Reto Aebersold 2023-09-21 17:47:58 +02:00
parent 7c6d448268
commit 417b2c58b8
8 changed files with 338 additions and 245 deletions

View File

@ -20,14 +20,19 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
defineProps<{ withDefaults(
doc: { defineProps<{
url: string; doc: {
name: string; url: string;
}; name: string;
canDelete: boolean; };
subtitle: string; canDelete?: boolean;
}>(); subtitle: string;
}>(),
{
canDelete: false,
}
);
const emit = defineEmits(["delete"]); const emit = defineEmits(["delete"]);
</script> </script>

View File

@ -9,9 +9,9 @@ const courseSession = useCurrentCourseSession();
const circleDates = computed(() => { const circleDates = computed(() => {
const dueDates = courseSession.value.due_dates.filter((dueDate) => { const dueDates = courseSession.value.due_dates.filter((dueDate) => {
if (!cockpitStore.selectedCircle) return false; if (!cockpitStore.currentCircle) return false;
return ( return (
cockpitStore.selectedCircle.translation_key == dueDate?.circle?.translation_key cockpitStore.currentCircle.translation_key == dueDate?.circle?.translation_key
); );
}); });
return dueDates.slice(0, 4); return dueDates.slice(0, 4);

View File

@ -10,6 +10,7 @@ import { useCompetenceStore } from "@/stores/competence";
import { useLearningPathStore } from "@/stores/learningPath"; import { useLearningPathStore } from "@/stores/learningPath";
import log from "loglevel"; import log from "loglevel";
import CockpitDates from "@/pages/cockpit/cockpitPage/CockpitDates.vue"; import CockpitDates from "@/pages/cockpit/cockpitPage/CockpitDates.vue";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue"; import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
const props = defineProps<{ const props = defineProps<{
@ -22,12 +23,13 @@ const cockpitStore = useCockpitStore();
const competenceStore = useCompetenceStore(); const competenceStore = useCompetenceStore();
const learningPathStore = useLearningPathStore(); const learningPathStore = useLearningPathStore();
const courseSession = useCurrentCourseSession(); const courseSession = useCurrentCourseSession();
const courseSessionsStore = useCourseSessionsStore();
function userCountStatusForCircle(userId: string) { function userCountStatusForCircle(userId: string) {
if (!cockpitStore.selectedCircle) return { FAIL: 0, SUCCESS: 0, UNKNOWN: 0 }; if (!cockpitStore.currentCircle) return { FAIL: 0, SUCCESS: 0, UNKNOWN: 0 };
const criteria = competenceStore.flatPerformanceCriteria( const criteria = competenceStore.flatPerformanceCriteria(
userId, userId,
cockpitStore.selectedCircle.id cockpitStore.currentCircle.id
); );
return competenceStore.calcStatusCount(criteria); return competenceStore.calcStatusCount(criteria);
} }
@ -35,146 +37,173 @@ function userCountStatusForCircle(userId: string) {
<template> <template>
<div class="bg-gray-200"> <div class="bg-gray-200">
<div class="container-large"> <div v-if="cockpitStore.currentCircle" class="container-large">
<div class="mb-9 flex flex-col lg:flex-row lg:items-center lg:justify-between"> <div class="mb-9 flex flex-col lg:flex-row lg:items-center lg:justify-between">
<h1>Cockpit</h1> <h1>Cockpit</h1>
<ItDropdownSelect <ItDropdownSelect
v-model="cockpitStore.selectedCircle" :model-value="cockpitStore.selectedCircle"
class="mt-4 w-full lg:mt-0 lg:w-96" class="mt-4 w-full lg:mt-0 lg:w-96"
:items="cockpitStore.circles" :items="cockpitStore.circles"
@update:model-value="cockpitStore.setCurrentCourseCircleFromEvent"
></ItDropdownSelect> ></ItDropdownSelect>
</div> </div>
<template v-if="cockpitStore.selectedCircle"> <!-- Status -->
<!-- Status --> <div class="mb-4 gap-4 lg:grid lg:grid-cols-3 lg:grid-rows-none">
<div class="mb-4 gap-4 lg:grid lg:grid-cols-3 lg:grid-rows-none"> <div class="my-4 flex flex-col justify-between bg-white p-6 lg:my-0">
<div class="my-4 flex flex-col justify-between bg-white p-6 lg:my-0"> <div>
<div> <h3 class="heading-3 mb-4 flex items-center gap-2">
<h3 class="heading-3 mb-4 flex items-center gap-2"> {{ $t("Trainerunterlagen") }}
{{ $t("Trainerunterlagen") }} </h3>
</h3> <div class="mb-4">
<div class="mb-4"> {{ $t("cockpit.trainerFilesText") }}
{{ $t("cockpit.trainerFilesText") }}
</div>
</div>
<div>
<a
href="https://vbvbern.sharepoint.com/sites/myVBV-AFA_K-CI"
class="btn-secondary min-w-min"
target="_blank"
>
{{ $t("MS Teams öffnen") }}
</a>
</div> </div>
</div> </div>
<div class="my-4 flex flex-col justify-between bg-white p-6 lg:my-0"> <div>
<div> <a
<h3 class="heading-3 mb-4 flex items-center gap-2"> href="https://vbvbern.sharepoint.com/sites/myVBV-AFA_K-CI"
{{ $t("Anwesenheitskontrolle Präsenzkurse") }} class="btn-secondary min-w-min"
</h3> target="_blank"
<div class="mb-4"> >
{{ {{ $t("MS Teams öffnen") }}
$t( </a>
"Hier überprüfst und bestätigst du die Anwesenheit deiner Teilnehmenden."
)
}}
</div>
</div>
<div>
<router-link
:to="`/course/${props.courseSlug}/cockpit/attendance`"
class="btn-secondary min-w-min"
>
{{ $t("Anwesenheit prüfen") }}
</router-link>
</div>
</div>
<div class="bg-white p-6">
<CockpitDates></CockpitDates>
</div> </div>
</div> </div>
<SubmissionsOverview <div class="my-4 flex flex-col justify-between bg-white p-6 lg:my-0">
:course-session="courseSession" <div>
:selected-circle="cockpitStore.selectedCircle.id" <h3 class="heading-3 mb-4 flex items-center gap-2">
></SubmissionsOverview> {{ $t("a.Unterlagen für Teilnehmenden") }}
<div class="pt-4"> </h3>
<!-- progress --> <div class="mb-4">
<div v-if="cockpitStore.courseSessionMembers" class="bg-white p-6"> {{ $t("a.Stelle deinen Lernenden zusätzliche Inhalte zur Verfügung.") }}
<h1 class="heading-3 mb-5">{{ $t("cockpit.progress") }}</h1> </div>
<ul> <div
<ItPersonRow v-if="courseSessionsStore.circleDocuments.length"
v-for="csu in cockpitStore.courseSessionMembers" class="mb-4 flex items-center gap-x-2"
:key="csu.user_id + csu.session_title" >
:name="`${csu.first_name} ${csu.last_name}`" <it-icon-document />
:avatar-url="csu.avatar_url" {{ courseSessionsStore.circleDocuments.length }} {{ $t("a.Unterlagen") }}
> </div>
<template #center> </div>
<div <div>
class="mt-2 flex w-full flex-col items-center justify-between lg:mt-0 lg:flex-row" <router-link
> :to="`/course/${props.courseSlug}/cockpit/documents`"
<LearningPathDiagram class="btn-secondary min-w-min"
v-if=" >
learningPathStore.learningPathForUser( {{ $t("a.Zum Unterlagen-Upload") }}
props.courseSlug, </router-link>
csu.user_id </div>
) </div>
" <div class="my-4 flex flex-col justify-between bg-white p-6 lg:my-0">
:learning-path=" <div>
<h3 class="heading-3 mb-4 flex items-center gap-2">
{{ $t("Anwesenheitskontrolle Präsenzkurse") }}
</h3>
<div class="mb-4">
{{
$t(
"Hier überprüfst und bestätigst du die Anwesenheit deiner Teilnehmenden."
)
}}
</div>
</div>
<div>
<router-link
:to="`/course/${props.courseSlug}/cockpit/attendance`"
class="btn-secondary min-w-min"
>
{{ $t("Anwesenheit prüfen") }}
</router-link>
</div>
</div>
</div>
<div class="mb-4 bg-white p-6">
<CockpitDates></CockpitDates>
</div>
<SubmissionsOverview
:course-session="courseSession"
:selected-circle="cockpitStore.currentCircle.id"
></SubmissionsOverview>
<div class="pt-4">
<!-- progress -->
<div v-if="cockpitStore.courseSessionMembers" class="bg-white p-6">
<h1 class="heading-3 mb-5">{{ $t("cockpit.progress") }}</h1>
<ul>
<ItPersonRow
v-for="csu in cockpitStore.courseSessionMembers"
:key="csu.user_id + csu.session_title"
:name="`${csu.first_name} ${csu.last_name}`"
:avatar-url="csu.avatar_url"
>
<template #center>
<div
class="mt-2 flex w-full flex-col items-center justify-between lg:mt-0 lg:flex-row"
>
<LearningPathDiagram
v-if="
learningPathStore.learningPathForUser(
props.courseSlug,
csu.user_id
)
"
:learning-path="
learningPathStore.learningPathForUser( learningPathStore.learningPathForUser(
props.courseSlug, props.courseSlug,
csu.user_id csu.user_id
) as LearningPath ) as LearningPath
" "
:show-circle-translation-keys="[ :show-circle-translation-keys="[
cockpitStore.selectedCircle.translation_key, cockpitStore.currentCircle.translation_key,
]" ]"
diagram-type="singleSmall" diagram-type="singleSmall"
class="mr-4" class="mr-4"
></LearningPathDiagram> ></LearningPathDiagram>
<p class="lg:min-w-[150px]"> <p class="lg:min-w-[150px]">
{{ cockpitStore.selectedCircle.name }} {{ cockpitStore.currentCircle.title }}
</p> </p>
<div class="ml-4 flex flex-row items-center"> <div class="ml-4 flex flex-row items-center">
<div class="mr-6 flex flex-row items-center"> <div class="mr-6 flex flex-row items-center">
<it-icon-smiley-thinking <it-icon-smiley-thinking
class="mr-2 inline-block h-8 w-8" class="mr-2 inline-block h-8 w-8"
></it-icon-smiley-thinking> ></it-icon-smiley-thinking>
<p class="text-bold inline-block w-6"> <p class="text-bold inline-block w-6">
{{ userCountStatusForCircle(csu.user_id).FAIL }} {{ userCountStatusForCircle(csu.user_id).FAIL }}
</p> </p>
</div>
<li class="mr-6 flex flex-row items-center">
<it-icon-smiley-happy
class="mr-2 inline-block h-8 w-8"
></it-icon-smiley-happy>
<p class="text-bold inline-block w-6">
{{ userCountStatusForCircle(csu.user_id).SUCCESS }}
</p>
</li>
<li class="flex flex-row items-center">
<it-icon-smiley-neutral
class="mr-2 inline-block h-8 w-8"
></it-icon-smiley-neutral>
<p class="text-bold inline-block w-6">
{{ userCountStatusForCircle(csu.user_id).UNKNOWN }}
</p>
</li>
</div> </div>
<li class="mr-6 flex flex-row items-center">
<it-icon-smiley-happy
class="mr-2 inline-block h-8 w-8"
></it-icon-smiley-happy>
<p class="text-bold inline-block w-6">
{{ userCountStatusForCircle(csu.user_id).SUCCESS }}
</p>
</li>
<li class="flex flex-row items-center">
<it-icon-smiley-neutral
class="mr-2 inline-block h-8 w-8"
></it-icon-smiley-neutral>
<p class="text-bold inline-block w-6">
{{ userCountStatusForCircle(csu.user_id).UNKNOWN }}
</p>
</li>
</div> </div>
</template> </div>
<template #link> </template>
<router-link <template #link>
:to="`/course/${props.courseSlug}/cockpit/profile/${csu.user_id}`" <router-link
class="link w-full lg:text-right" :to="`/course/${props.courseSlug}/cockpit/profile/${csu.user_id}`"
> class="link w-full lg:text-right"
{{ $t("general.profileLink") }} >
</router-link> {{ $t("general.profileLink") }}
</template> </router-link>
</ItPersonRow> </template>
</ul> </ItPersonRow>
</div> </ul>
</div> </div>
</template> </div>
<span v-else class="text-lg text-orange-600"> </div>
<div v-else class="container-large mt-4">
<span class="text-lg text-orange-600">
{{ $t("a.Kein Circle verfügbar oder ausgewählt.") }} {{ $t("a.Kein Circle verfügbar oder ausgewählt.") }}
</span> </span>
</div> </div>

View File

@ -0,0 +1,132 @@
<script setup lang="ts">
import { useCurrentCourseSession } from "@/composables";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import { useCockpitStore } from "@/stores/cockpit";
import ItModal from "@/components/ui/ItModal.vue";
import DocumentUploadForm from "@/pages/cockpit/documentPage/DocumentUploadForm.vue";
import { computed, ref, watch } from "vue";
import { useCircleStore } from "@/stores/circle";
import { useTranslation } from "i18next-vue";
import type { CircleDocument, DocumentUploadData } from "@/types";
import dialog from "@/utils/confirm-dialog";
import log from "loglevel";
import { uploadCircleDocument } from "@/services/files";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import DocumentListItem from "@/components/circle/DocumentListItem.vue";
const cockpitStore = useCockpitStore();
const courseSession = useCurrentCourseSession();
const courseSessionsStore = useCourseSessionsStore();
const circleStore = useCircleStore();
const { t } = useTranslation();
const showUploadModal = ref(false);
const showUploadErrorMessage = ref(false);
const isUploading = ref(false);
const dropdownLearningSequences = computed(() =>
circleStore.circle?.learningSequences.map((sequence) => ({
id: sequence.id,
name: sequence.title,
}))
);
const deleteDocument = async (doc: CircleDocument) => {
const options = {
title: t("circlePage.documents.deleteModalTitle"),
content: t("circlePage.documents.deleteModalWarning", { title: doc.name }),
};
try {
await dialog.confirm(options);
courseSessionsStore.removeDocument(doc.id);
} catch (e) {
log.debug("rejected");
}
};
watch(showUploadModal, () => (showUploadErrorMessage.value = false));
async function uploadDocument(data: DocumentUploadData) {
isUploading.value = true;
showUploadErrorMessage.value = false;
try {
if (!courseSessionsStore.currentCourseSession) {
throw new Error("No course session found");
}
const newDocument = await uploadCircleDocument(
data,
courseSessionsStore.currentCourseSession.id
);
courseSessionsStore.addDocument(newDocument);
showUploadModal.value = false;
isUploading.value = false;
} catch (error) {
log.error(error);
showUploadErrorMessage.value = true;
isUploading.value = false;
}
}
</script>
<template>
<div class="bg-gray-200">
<div v-if="courseSession" class="container-large">
<nav class="py-4 pb-4">
<router-link
class="btn-text inline-flex items-center pl-0"
:to="`/course/${courseSession.course.slug}/cockpit`"
>
<it-icon-arrow-left />
<span>{{ t("general.back") }}</span>
</router-link>
</nav>
<main>
<div class="mb-9 flex flex-col lg:flex-row lg:items-center lg:justify-between">
<h2>{{ t("a.Unterlagen für Teilnehmenden") }}</h2>
<ItDropdownSelect
:model-value="cockpitStore.selectedCircle"
class="mt-4 w-full lg:mt-0 lg:w-96"
:items="cockpitStore.circles"
@update:model-value="cockpitStore.setCurrentCourseCircleFromEvent"
></ItDropdownSelect>
</div>
<div class="bg-white p-6">
<button class="btn-primary text-xl" @click="showUploadModal = true">
{{ t("circlePage.documents.action") }}
</button>
<ul
v-if="courseSessionsStore.circleDocuments.length"
class="mt-8 border-t border-t-gray-500"
>
<template
v-for="learningSequence of courseSessionsStore.circleDocuments"
:key="learningSequence.id"
>
<DocumentListItem
v-for="doc of learningSequence.documents"
:key="doc.url"
:subtitle="learningSequence.title"
:can-delete="courseSessionsStore.canUploadCircleDocuments"
:doc="doc"
@delete="deleteDocument(doc)"
/>
</template>
</ul>
</div>
<ItModal v-model="showUploadModal">
<template #title>{{ t("circlePage.documents.action") }}</template>
<template #body>
<DocumentUploadForm
:learning-sequences="dropdownLearningSequences"
:show-upload-error-message="showUploadErrorMessage"
:is-uploading="isUploading"
@form-submit="uploadDocument"
/>
</template>
</ItModal>
</main>
</div>
</div>
</template>

View File

@ -5,12 +5,7 @@
</h3> </h3>
<div> <div>
<div class="mt-4 leading-relaxed"> <div class="mt-4 leading-relaxed">
<template v-if="!courseSessionsStore.canUploadCircleDocuments"> {{ $t("circlePage.documents.userDescription") }}
{{ $t("circlePage.documents.userDescription") }}
</template>
<template v-else>
{{ $t("circlePage.documents.expertDescription") }}
</template>
</div> </div>
</div> </div>
<ul <ul
@ -25,103 +20,16 @@
v-for="doc of learningSequence.documents" v-for="doc of learningSequence.documents"
:key="doc.url" :key="doc.url"
:subtitle="learningSequence.title" :subtitle="learningSequence.title"
:can-delete="courseSessionsStore.canUploadCircleDocuments"
:doc="doc" :doc="doc"
@delete="deleteDocument(doc)"
/> />
</template> </template>
</ul> </ul>
<div v-if="courseSessionsStore.canUploadCircleDocuments">
<button class="btn-primary mt-8 text-xl" @click="showUploadModal = true">
{{ $t("circlePage.documents.action") }}
</button>
</div>
</div>
<ItModal v-model="showUploadModal">
<template #title>{{ $t("circlePage.documents.action") }}</template>
<template #body>
<DocumentUploadForm
:learning-sequences="dropdownLearningSequences"
:show-upload-error-message="showUploadErrorMessage"
:is-uploading="isUploading"
@form-submit="uploadDocument"
/>
</template>
</ItModal>
<div
v-if="courseSessionsStore.canUploadCircleDocuments"
class="mt-8 flex flex-col gap-y-4 border p-6"
>
<h3 class="text-blue-dark">
{{ $t("circlePage.documents.trainerTitle") }}
</h3>
<div class="leading-relaxed">
{{ $t("circlePage.documents.trainerDescription") }}
</div>
<a target="_blank" class="link" :href="$t('circlePage.documents.trainerLinkSrc')">
{{ $t("circlePage.documents.trainerLinkText") }}
</a>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import ItModal from "@/components/ui/ItModal.vue";
import { uploadCircleDocument } from "@/services/files";
import { useCircleStore } from "@/stores/circle";
import { useCourseSessionsStore } from "@/stores/courseSessions"; import { useCourseSessionsStore } from "@/stores/courseSessions";
import type { CircleDocument, DocumentUploadData } from "@/types"; import DocumentListItem from "@/components/circle/DocumentListItem.vue";
import dialog from "@/utils/confirm-dialog";
import log from "loglevel";
import { computed, ref, watch } from "vue";
import DocumentListItem from "./DocumentListItem.vue";
import DocumentUploadForm from "./DocumentUploadForm.vue";
import { useTranslation } from "i18next-vue";
const courseSessionsStore = useCourseSessionsStore(); const courseSessionsStore = useCourseSessionsStore();
const circleStore = useCircleStore();
const showUploadModal = ref(false);
const showUploadErrorMessage = ref(false);
const isUploading = ref(false);
const dropdownLearningSequences = computed(() =>
circleStore.circle?.learningSequences.map((sequence) => ({
id: sequence.id,
name: sequence.title,
}))
);
// confirm dialog
const { t } = useTranslation();
const deleteDocument = async (doc: CircleDocument) => {
const options = {
title: t("circlePage.documents.deleteModalTitle"),
content: t("circlePage.documents.deleteModalWarning", { title: doc.name }),
};
try {
await dialog.confirm(options);
courseSessionsStore.removeDocument(doc.id);
} catch (e) {
log.debug("rejected");
}
};
watch(showUploadModal, () => (showUploadErrorMessage.value = false));
async function uploadDocument(data: DocumentUploadData) {
isUploading.value = true;
showUploadErrorMessage.value = false;
try {
if (!courseSessionsStore.currentCourseSession) {
throw new Error("No course session found");
}
const newDocument = await uploadCircleDocument(
data,
courseSessionsStore.currentCourseSession.id
);
courseSessionsStore.addDocument(newDocument);
showUploadModal.value = false;
isUploading.value = false;
} catch (error) {
log.error(error);
showUploadErrorMessage.value = true;
isUploading.value = false;
}
}
</script> </script>

View File

@ -165,6 +165,11 @@ const router = createRouter({
import("@/pages/cockpit/attendanceCheckPage/AttendanceCheckPage.vue"), import("@/pages/cockpit/attendanceCheckPage/AttendanceCheckPage.vue"),
props: true, props: true,
}, },
{
path: "documents",
component: () => import("@/pages/cockpit/documentPage/DocumentPage.vue"),
props: true,
},
], ],
}, },
{ {

View File

@ -2,26 +2,18 @@ import { itGetCached } from "@/fetchHelpers";
import type { CourseSessionUser, ExpertSessionUser } from "@/types"; import type { CourseSessionUser, ExpertSessionUser } from "@/types";
import log from "loglevel"; import log from "loglevel";
import { useCircleStore } from "@/stores/circle";
import { useLearningPathStore } from "@/stores/learningPath"; import { useLearningPathStore } from "@/stores/learningPath";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
export type CockpitStoreState = { export type CockpitStoreState = {
courseSessionMembers: CourseSessionUser[] | undefined; courseSessionMembers: CourseSessionUser[] | undefined;
selectedCircle:
| {
id: number;
name: string;
slug: string;
translation_key: string;
}
| undefined;
circles: { circles: {
id: number; id: string;
name: string; name: string;
slug: string;
translation_key: string;
}[]; }[];
currentCourseSlug: string | undefined;
}; };
export const useCockpitStore = defineStore({ export const useCockpitStore = defineStore({
@ -29,29 +21,38 @@ export const useCockpitStore = defineStore({
state: () => { state: () => {
return { return {
courseSessionMembers: undefined, courseSessionMembers: undefined,
selectedCircle: undefined,
circles: [], circles: [],
currentCourseSlug: undefined,
} as CockpitStoreState; } as CockpitStoreState;
}, },
actions: { actions: {
async loadCircles(courseSlug: string, courseSessionId: number) { async loadCircles(courseSlug: string, courseSessionId: number) {
log.debug("loadCircles called"); log.debug("loadCircles called", courseSlug, courseSessionId);
this.currentCourseSlug = courseSlug;
const f = await courseCircles(courseSlug, courseSessionId); const f = await courseCircles(this.currentCourseSlug, courseSessionId);
this.circles = f.map((c) => { this.circles = f.map((c) => {
return { return {
id: c.id, id: c.slug,
name: c.title, name: c.title,
slug: c.slug,
translation_key: c.translation_key,
}; };
}); });
if (this.circles.length > 0) { if (this.circles.length > 0) {
this.selectedCircle = this.circles[0]; await this.setCurrentCourseCircle(this.circles[0].id);
} }
}, },
async setCurrentCourseCircle(circleSlug: string) {
if (!this.currentCourseSlug) {
throw new Error("currentCourseSlug is undefined");
}
const circleStore = useCircleStore();
await circleStore.loadCircle(this.currentCourseSlug, circleSlug);
},
async setCurrentCourseCircleFromEvent(event: { id: string }) {
await this.setCurrentCourseCircle(event.id);
},
async loadCourseSessionMembers(courseSessionId: number, reload = false) { async loadCourseSessionMembers(courseSessionId: number, reload = false) {
log.debug("loadCourseSessionMembers called"); log.debug("loadCourseSessionMembers called");
const users = (await itGetCached( const users = (await itGetCached(
@ -65,6 +66,19 @@ export const useCockpitStore = defineStore({
return this.courseSessionMembers; return this.courseSessionMembers;
}, },
}, },
getters: {
currentCircle: () => {
const circleStore = useCircleStore();
return circleStore.circle;
},
selectedCircle: () => {
const circleStore = useCircleStore();
return {
id: circleStore.circle?.id || 0,
name: circleStore.circle?.title || "",
};
},
},
}); });
async function courseCircles(courseSlug: string, courseSessionId: number) { async function courseCircles(courseSlug: string, courseSessionId: number) {