Skip to content

Commit f9d8f5e

Browse files
Admin: Add sequence dependency check for sessions and courses with UI integration - refs BT#22586
1 parent 99b38f4 commit f9d8f5e

18 files changed

+385
-163
lines changed

assets/vue/components/basecomponents/ChamiloIcons.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,4 +144,5 @@ export const chamiloIconToClass = {
144144
"clear-all": "mdi mdi-broom",
145145
"qrcode": "mdi mdi-qrcode",
146146
"minus": "mdi mdi-minus",
147+
"shield-check": "mdi mdi-shield-check",
147148
}

assets/vue/components/course/CatalogueCourseCard.vue

Lines changed: 24 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@
132132
</router-link>
133133

134134
<Button
135-
v-else-if="hasDependencies && props.currentUserId"
135+
v-else-if="isLocked && hasRequirements"
136136
:label="$t('Check requirements')"
137137
icon="mdi mdi-shield-check"
138138
class="w-full p-button-warning"
@@ -177,6 +177,8 @@
177177
v-model="showDependenciesModal"
178178
:course-id="course.id"
179179
:session-id="course.sessionId || 0"
180+
:requirements="requirementList"
181+
:graph-image="graphImage"
180182
/>
181183
<Dialog
182184
v-model:visible="showDescriptionDialog"
@@ -192,23 +194,14 @@
192194
<script setup>
193195
import Rating from "primevue/rating"
194196
import Button from "primevue/button"
195-
import { computed, onMounted, ref } from "vue"
196-
import courseRelUserService from "../../services/courseRelUserService"
197+
import Dialog from "primevue/dialog"
198+
import { computed, ref, onMounted } from "vue"
197199
import { useRoute, useRouter } from "vue-router"
198200
import { useNotification } from "../../composables/notification"
199-
import Dialog from "primevue/dialog"
200201
import { usePlatformConfig } from "../../store/platformConfig"
201202
import CatalogueRequirementModal from "./CatalogueRequirementModal.vue"
202-
import courseService from "../../services/courseService"
203-
204-
const platformConfigStore = usePlatformConfig()
205-
const showDescriptionDialog = ref(false)
206-
const showDependenciesModal = ref(false)
207-
const hasDependencies = ref(false)
208-
209-
const allowDescription = computed(
210-
() => platformConfigStore.getSetting("course.show_courses_descriptions_in_catalog") !== "false",
211-
)
203+
import courseRelUserService from "../../services/courseRelUserService"
204+
import { useCourseRequirementStatus } from "../../composables/course/useCourseRequirementStatus"
212205
213206
const props = defineProps({
214207
course: Object,
@@ -228,6 +221,14 @@ const emit = defineEmits(["rate", "subscribed"])
228221
const router = useRouter()
229222
const route = useRoute()
230223
const { showErrorNotification, showSuccessNotification } = useNotification()
224+
const platformConfigStore = usePlatformConfig()
225+
226+
const showDescriptionDialog = ref(false)
227+
const showDependenciesModal = ref(false)
228+
229+
const allowDescription = computed(
230+
() => platformConfigStore.getSetting("course.show_courses_descriptions_in_catalog") !== "false",
231+
)
231232
232233
const isUserInCourse = computed(() => {
233234
if (!props.currentUserId) return false
@@ -303,9 +304,7 @@ function routeExists(name) {
303304
304305
const linkSettings = computed(() => {
305306
const settings = platformConfigStore.getSetting("course.course_catalog_settings")
306-
const result = settings?.link_settings ?? {}
307-
console.log("Link settings:", result)
308-
return result
307+
return settings?.link_settings ?? {}
309308
})
310309
311310
const imageLink = computed(() => {
@@ -320,10 +319,6 @@ const imageLink = computed(() => {
320319
return { name: routeName, params: { id: props.course.id } }
321320
}
322321
323-
if (routeName) {
324-
console.warn(`[CatalogueCourseCard] Route '${routeName}' does not exist.`)
325-
}
326-
327322
return null
328323
})
329324
@@ -334,32 +329,21 @@ const titleLink = computed(() => {
334329
return { name: routeName, params: { id: props.course.id } }
335330
}
336331
337-
if (routeName) {
338-
console.warn(`[CatalogueCourseCard] Route '${routeName}' does not exist.`)
339-
}
340-
341332
return null
342333
})
343334
344335
const showInfoPopup = computed(() => {
345336
const allowed = ["course_description_popup"]
346337
const value = linkSettings.value.info_url
347-
if (value && !allowed.includes(value)) {
348-
console.warn(`[CatalogueCourseCard] info_url '${value}' is not a recognized option.`)
349-
return false
350-
}
351-
return value === "course_description_popup"
338+
return value && allowed.includes(value)
352339
})
353340
354-
onMounted(async () => {
355-
try {
356-
const { sequenceList: list } = await courseService.getNextCourse(
357-
props.course.id,
358-
props.course.sessionId || 0
359-
)
360-
hasDependencies.value = list.length > 0
361-
} catch (e) {
362-
console.warn(`[CatalogueCourseCard] Failed to load dependencies for course ${props.course.id}`, e)
363-
}
341+
const { isLocked, hasRequirements, requirementList, graphImage, fetchStatus } = useCourseRequirementStatus(
342+
props.course.id,
343+
props.course.sessionId || 0,
344+
)
345+
346+
onMounted(() => {
347+
fetchStatus()
364348
})
365349
</script>

assets/vue/components/course/CatalogueRequirementModal.vue

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
v-model:visible="visible"
44
modal
55
:header="t('Required courses')"
6-
class="w-[30rem]"
6+
class="w-[30rem] z-[99999] !block !opacity-100"
77
>
88
<div v-if="sequenceList.length">
99
<div
@@ -14,12 +14,12 @@
1414
<h4 class="font-semibold text-gray-700 mb-2">{{ item.name }}</h4>
1515
<ul>
1616
<li
17-
v-for="(dep, id) in item.dependents"
17+
v-for="(req, id) in item.requirements"
1818
:key="id"
1919
class="flex items-center gap-2"
2020
>
21-
<i :class="dep.status ? 'mdi mdi-check-circle text-green-500' : 'mdi mdi-alert-circle text-red-500'" />
22-
<span>{{ dep.name }}</span>
21+
<i :class="req.status ? 'mdi mdi-check-circle text-green-500' : 'mdi mdi-alert-circle text-red-500'" />
22+
<span>{{ req.name }}</span>
2323
</li>
2424
</ul>
2525
</div>
@@ -30,40 +30,40 @@
3030
>
3131
{{ t("No dependencies") }}
3232
</div>
33+
<div
34+
v-if="graphImage"
35+
class="mb-4 text-center"
36+
>
37+
<img
38+
:src="graphImage"
39+
alt="Graph"
40+
class="max-w-full max-h-96 mx-auto border rounded"
41+
/>
42+
</div>
3343
</Dialog>
3444
</template>
3545

3646
<script setup>
37-
import { ref, watch, computed } from "vue"
47+
import Dialog from "primevue/dialog"
48+
import { computed } from "vue"
3849
import { useI18n } from "vue-i18n"
39-
import courseService from "../../services/courseService"
4050
4151
const { t } = useI18n()
52+
4253
const props = defineProps({
43-
visible: Boolean,
4454
modelValue: Boolean,
4555
courseId: Number,
4656
sessionId: Number,
57+
requirements: Array,
58+
graphImage: String,
4759
})
48-
const emit = defineEmits(["update:modelValue"])
49-
const sequenceList = ref([])
5060
51-
watch(
52-
() => props.modelValue,
53-
async (newVal) => {
54-
if (newVal && props.courseId) {
55-
try {
56-
const { sequenceList: list } = await courseService.getNextCourse(props.courseId, props.sessionId || 0)
57-
sequenceList.value = list || []
58-
} catch (e) {
59-
console.warn("Failed to load sequence info", e)
60-
}
61-
}
62-
},
63-
)
61+
const emit = defineEmits(["update:modelValue"])
6462
6563
const visible = computed({
6664
get: () => props.modelValue,
6765
set: (value) => emit("update:modelValue", value),
6866
})
67+
68+
const sequenceList = computed(() => props.requirements || [])
6969
</script>

assets/vue/components/course/CourseCard.vue

Lines changed: 63 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
<Card class="course-card">
33
<template #header>
44
<img
5-
v-if="disabled"
5+
v-if="isLocked"
66
:alt="course.title"
7-
:src="course.illustrationUrl"
7+
:src="course.illustrationUrl || '/img/session_default.svg'"
88
/>
99
<BaseAppLink
1010
v-else
@@ -13,13 +13,13 @@
1313
>
1414
<img
1515
:alt="course.title"
16-
:src="course.illustrationUrl"
16+
:src="course.illustrationUrl || '/img/session_default.svg'"
1717
/>
1818
</BaseAppLink>
1919
</template>
2020
<template #title>
21-
<div class="course-card__title">
22-
<div v-if="disabled">
21+
<div class="course-card__title flex items-center gap-2">
22+
<div v-if="isLocked">
2323
<div
2424
v-if="session"
2525
class="session__title"
@@ -43,27 +43,47 @@
4343
{{ course.title }}
4444
</BaseAppLink>
4545

46-
<div
47-
v-if="sessionDisplayDate"
48-
class="session__display-date"
49-
v-text="sessionDisplayDate"
46+
<BaseButton
47+
v-if="isLocked && hasRequirements"
48+
icon="shield-check"
49+
type="black"
50+
onlyIcon
51+
size="large"
52+
class="!bg-support-1 !text-support-3 !rounded-md !shadow-sm hover:!bg-support-2"
53+
@click="openRequirementsModal"
5054
/>
5155
</div>
56+
57+
<div
58+
v-if="sessionDisplayDate"
59+
class="session__display-date"
60+
v-text="sessionDisplayDate"
61+
/>
5262
</template>
5363
<template #footer>
5464
<BaseAvatarList :users="teachers" />
5565
</template>
5666
</Card>
67+
68+
<CatalogueRequirementModal
69+
v-model="showDependenciesModal"
70+
:course-id="course.id"
71+
:session-id="sessionId"
72+
:requirements="requirementList"
73+
:graph-image="graphImage"
74+
/>
5775
</template>
5876

5977
<script setup>
6078
import Card from "primevue/card"
6179
import BaseAvatarList from "../basecomponents/BaseAvatarList.vue"
62-
import { computed } from "vue"
63-
import { isEmpty } from "lodash"
80+
import { computed, onMounted, ref } from "vue"
6481
import { useFormatDate } from "../../composables/formatDate"
6582
import { usePlatformConfig } from "../../store/platformConfig"
6683
import { useI18n } from "vue-i18n"
84+
import { useCourseRequirementStatus } from "../../composables/course/useCourseRequirementStatus"
85+
import BaseButton from "../basecomponents/BaseButton.vue"
86+
import CatalogueRequirementModal from "./CatalogueRequirementModal.vue"
6787
6888
const { abbreviatedDatetime } = useFormatDate()
6989
@@ -89,14 +109,11 @@ const props = defineProps({
89109
},
90110
})
91111
92-
const platformConfigStore = usePlatformConfig()
93-
const showCourseDuration = computed(() => "true" === platformConfigStore.getSetting("course.show_course_duration"))
94-
95112
const { t } = useI18n()
96-
97-
const showRemainingDays = computed(() => {
98-
return platformConfigStore.getSetting("session.session_list_view_remaining_days") === "true"
99-
})
113+
const platformConfigStore = usePlatformConfig()
114+
const showRemainingDays = computed(
115+
() => platformConfigStore.getSetting("session.session_list_view_remaining_days") === "true",
116+
)
100117
101118
const daysRemainingText = computed(() => {
102119
if (!showRemainingDays.value || !props.session?.displayEndDate) return null
@@ -113,14 +130,16 @@ const daysRemainingText = computed(() => {
113130
return t("Expired")
114131
})
115132
133+
const showCourseDuration = computed(() => platformConfigStore.getSetting("course.show_course_duration") === "true")
134+
116135
const teachers = computed(() => {
117136
if (props.session?.courseCoachesSubscriptions) {
118137
return props.session.courseCoachesSubscriptions
119138
.filter((srcru) => srcru.course["@id"] === props.course["@id"])
120139
.map((srcru) => srcru.user)
121140
}
122141
123-
if (props.course.users && props.course.users.edges) {
142+
if (props.course.users?.edges) {
124143
return props.course.users.edges.map((edge) => ({
125144
id: edge.node.id,
126145
...edge.node.user,
@@ -131,22 +150,34 @@ const teachers = computed(() => {
131150
})
132151
133152
const sessionDisplayDate = computed(() => {
134-
if (daysRemainingText.value) {
135-
return daysRemainingText.value
136-
}
153+
if (daysRemainingText.value) return daysRemainingText.value
137154
138-
const dateString = []
155+
const parts = []
156+
if (props.session?.displayStartDate) parts.push(abbreviatedDatetime(props.session.displayStartDate))
157+
if (props.session?.displayEndDate) parts.push(abbreviatedDatetime(props.session.displayEndDate))
139158
140-
if (props.session) {
141-
if (!isEmpty(props.session.displayStartDate)) {
142-
dateString.push(abbreviatedDatetime(props.session.displayStartDate))
143-
}
159+
return parts.join("")
160+
})
144161
145-
if (!isEmpty(props.session.displayEndDate)) {
146-
dateString.push(abbreviatedDatetime(props.session.displayEndDate))
147-
}
148-
}
162+
const internalLocked = ref(false)
163+
const showDependenciesModal = ref(false)
149164
150-
return dateString.join("")
165+
const { hasRequirements, requirementList, graphImage, fetchStatus } = useCourseRequirementStatus(
166+
props.course.id,
167+
props.sessionId,
168+
(locked) => {
169+
internalLocked.value = locked
170+
},
171+
)
172+
173+
const isLocked = computed(() => props.disabled || internalLocked.value)
174+
175+
onMounted(() => {
176+
if (props.course?.id) {
177+
fetchStatus()
178+
}
151179
})
180+
function openRequirementsModal() {
181+
showDependenciesModal.value = true
182+
}
152183
</script>

assets/vue/components/course/NextCourseSequence.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ const sequenceList = ref([])
8787
8888
onMounted(async () => {
8989
try {
90-
const { sequenceList: list, graph } = await courseService.getNextCourse(course.value.id, session.value?.id || 0)
90+
const { sequenceList: list, graph } = await courseService.getNextCourse(course.value.id, session.value?.id || 0, true)
9191
sequenceList.value = list || []
9292
graphUrl.value = graph || null
9393
} catch (e) {

0 commit comments

Comments
 (0)