Merge pull request #1807 from rehanrehman389/program-fix

Misc Programs Issues
This commit is contained in:
Jannat Patel
2025-11-25 10:40:59 +05:30
committed by GitHub
2 changed files with 106 additions and 84 deletions

View File

@@ -58,15 +58,15 @@
</Button> </Button>
</div> </div>
<ListView <ListView
v-if="programCourses.data.length > 0" v-if="program.program_courses.length > 0"
:columns="courseColumns" :columns="courseColumns"
:rows="programCourses.data" :rows="program.program_courses"
:options="{ :options="{
selectable: true, selectable: true,
resizeColumn: true, resizeColumn: true,
showTooltip: false, showTooltip: false,
}" }"
rowKey="name" :rowKey="programName === 'new' ? 'course' : 'name'"
> >
<ListHeader <ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2" class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
@@ -75,8 +75,8 @@
</ListHeader> </ListHeader>
<ListRows> <ListRows>
<Draggable <Draggable
:list="programCourses.data" :list="program.program_courses"
item-key="name" :item-key="programName === 'new' ? 'course' : 'name'"
group="items" group="items"
@end="updateOrder" @end="updateOrder"
class="cursor-move" class="cursor-move"
@@ -133,14 +133,14 @@
</div> </div>
</div> </div>
<ListView <ListView
v-if="programMembers.data.length > 0" v-if="program.program_members.length > 0"
:columns="memberColumns" :columns="memberColumns"
:rows="programMembers.data" :rows="program.program_members"
:options="{ :options="{
selectable: true, selectable: true,
resizeColumn: true, resizeColumn: true,
}" }"
rowKey="name" :rowKey="programName === 'new' ? 'member' : 'name'"
> >
<ListHeader <ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2" class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
@@ -148,7 +148,7 @@
<ListHeaderItem :item="item" v-for="item in memberColumns" /> <ListHeaderItem :item="item" v-for="item in memberColumns" />
</ListHeader> </ListHeader>
<ListRows> <ListRows>
<ListRow :row="row" v-for="row in programMembers.data" /> <ListRow :row="row" v-for="row in program.program_members" />
</ListRows> </ListRows>
<ListSelectBanner> <ListSelectBanner>
<template #actions="{ unselectAll, selections }"> <template #actions="{ unselectAll, selections }">
@@ -217,13 +217,12 @@
/> />
</template> </template>
<template #actions="{ close }"> <template #actions="{ close }">
<div class="flex justify-end space-x-2 group"> <div class="flex justify-end space-x-2">
<Button <Button
v-if="programName != 'new'" v-if="programName != 'new'"
@click="deleteProgram(close)" @click="deleteProgram(close)"
variant="outline" variant="outline"
theme="red" theme="red"
class="invisible group-hover:visible"
> >
<template #prefix> <template #prefix>
<Trash2 class="size-4 stroke-1.5" /> <Trash2 class="size-4 stroke-1.5" />
@@ -252,7 +251,7 @@ import {
ListRow, ListRow,
toast, toast,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, ref, watch } from 'vue' import { computed, ref, watch, getCurrentInstance } from 'vue'
import { Plus, Trash2, TrendingUp } from 'lucide-vue-next' import { Plus, Trash2, TrendingUp } from 'lucide-vue-next'
import { Programs, Program } from '@/types/programs' import { Programs, Program } from '@/types/programs'
import { escapeHTML, openSettings } from '@/utils' import { escapeHTML, openSettings } from '@/utils'
@@ -269,6 +268,9 @@ const member = ref<string>('')
const showProgressDialog = ref(false) const showProgressDialog = ref(false)
const dirty = ref(false) const dirty = ref(false)
const app = getCurrentInstance()
const { $dialog } = app.appContext.config.globalProperties
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
programName: string | null programName: string | null
@@ -427,25 +429,22 @@ const addCourse = (close: () => void) => {
return return
} }
programCourses.insert.submit( const existingCourse = program.value.program_courses.find(
{ (c) => c.course === course.value
parent: props.programName,
parenttype: 'LMS Program',
parentfield: 'program_courses',
course: course.value,
idx: programCourses.data.length + 1,
},
{
onSuccess() {
updateCounts('course', 'add')
close()
toast.success(__('Course added to program successfully'))
},
onError(err: any) {
toast.warning(__(err.messages?.[0] || err))
},
}
) )
if (!existingCourse) {
program.value.program_courses.push({
course: course.value,
idx: program.value.program_courses.length + 1,
})
if (props.programName !== 'new') {
dirty.value = true
}
close()
toast.success(__('Course added to program successfully'))
} else {
toast.warning(__('Course already added to program'))
}
} }
const addMember = (close: () => void) => { const addMember = (close: () => void) => {
@@ -454,24 +453,21 @@ const addMember = (close: () => void) => {
return return
} }
programMembers.insert.submit( const existingMember = program.value.program_members.find(
{ (m) => m.member === member.value
parent: props.programName,
parenttype: 'LMS Program',
parentfield: 'program_members',
member: member.value,
},
{
onSuccess() {
updateCounts('member', 'add')
close()
toast.success(__('Member added to program successfully'))
},
onError(err: any) {
toast.warning(__(err.messages?.[0] || err))
},
}
) )
if (!existingMember) {
program.value.program_members.push({
member: member.value,
})
if (props.programName !== 'new') {
dirty.value = true
}
close()
toast.success(__('Member added to program successfully'))
} else {
toast.warning(__('Member already added to program'))
}
} }
const updateCounts = async ( const updateCounts = async (
@@ -509,57 +505,83 @@ const updateCounts = async (
const updateOrder = async (e: DragEvent) => { const updateOrder = async (e: DragEvent) => {
let sourceIdx = e.from.dataset.idx let sourceIdx = e.from.dataset.idx
let targetIdx = e.to.dataset.idx let targetIdx = e.to.dataset.idx
let courses = programCourses.data
courses.splice(targetIdx, 0, courses.splice(sourceIdx, 1)[0])
for (const [index, course] of courses.entries()) { if (props.programName === 'new') {
programCourses.setValue.submit( let courses = program.value.program_courses
{ courses.splice(targetIdx, 0, courses.splice(sourceIdx, 1)[0])
name: course.name, courses.forEach((course, index) => {
idx: index + 1, course.idx = index + 1
}, })
{ dirty.value = true
onError(err: any) { } else {
toast.warning(__(err.messages?.[0] || err)) let courses = programCourses.data
courses.splice(targetIdx, 0, courses.splice(sourceIdx, 1)[0])
for (const [index, course] of courses.entries()) {
programCourses.setValue.submit(
{
name: course.name,
idx: index + 1,
}, },
} {
) onError(err: any) {
await wait(100) toast.warning(__(err.messages?.[0] || err))
},
}
)
await wait(100)
}
} }
} }
const wait = (ms: number) => new Promise((res) => setTimeout(res, ms)) const wait = (ms: number) => new Promise((res) => setTimeout(res, ms))
const remove = async ( const remove = (
selections: string[], selections: string[],
unselectAll: () => void, unselectAll: () => void,
type: string type: string
) => { ) => {
selections = Array.from(selections) const selectionsArray = Array.from(selections)
for (const selection of selections) { if (type === 'courses') {
if (type == 'courses') { program.value.program_courses = program.value.program_courses.filter(
await programCourses.delete.submit(selection) (c) => !selectionsArray.includes(c.name || c.course)
await updateCounts('course', 'remove') )
} else { } else {
await programMembers.delete.submit(selection) program.value.program_members = program.value.program_members.filter(
await updateCounts('member', 'remove') (m) => !selectionsArray.includes(m.name || m.member)
} )
await programs.value.reload()
await wait(100)
} }
dirty.value = true
unselectAll() unselectAll()
} }
const deleteProgram = (close: () => void) => { const deleteProgram = (close: () => void) => {
if (props.programName == 'new') return if (props.programName == 'new') return
programs.value?.delete.submit(props.programName, { $dialog({
onSuccess() { title: __('Delete Program'),
toast.success(__('Program deleted successfully')) message: __(
close() 'Are you sure you want to delete this program? This action cannot be undone.'
}, ),
onError(err: any) { actions: [
toast.warning(__(err.messages?.[0] || err)) {
}, label: __('Delete'),
theme: 'red',
variant: 'solid',
onClick(closeDialog) {
programs.value?.delete.submit(props.programName, {
onSuccess() {
toast.success(__('Program deleted successfully'))
close()
closeDialog()
},
onError(err: any) {
toast.warning(__(err.messages?.[0] || err))
closeDialog()
},
})
},
},
],
}) })
} }
@@ -567,7 +589,7 @@ const courseColumns = computed(() => {
return [ return [
{ {
label: 'Title', label: 'Title',
key: 'course_title', key: props.programName === 'new' ? 'course' : 'course_title',
width: 1, width: 1,
}, },
] ]

View File

@@ -17,7 +17,7 @@ class LMSProgram(Document):
duplicates = {course for course in courses if courses.count(course) > 1} duplicates = {course for course in courses if courses.count(course) > 1}
if len(duplicates): if len(duplicates):
frappe.throw( frappe.throw(
_("Course {0} has already been added to this batch.").format( _("Course {0} has already been added to this program.").format(
frappe.bold(next(iter(duplicates))) frappe.bold(next(iter(duplicates)))
) )
) )
@@ -27,7 +27,7 @@ class LMSProgram(Document):
duplicates = {member for member in members if members.count(member) > 1} duplicates = {member for member in members if members.count(member) > 1}
if len(duplicates): if len(duplicates):
frappe.throw( frappe.throw(
_("Member {0} has already been added to this batch.").format( _("Member {0} has already been added to this program.").format(
frappe.bold(next(iter(duplicates))) frappe.bold(next(iter(duplicates)))
) )
) )