chore: fixed merge conflicts
This commit is contained in:
@@ -3,7 +3,49 @@
|
||||
class="sticky flex items-center justify-between top-0 z-10 border-b bg-surface-white px-3 py-2.5 sm:px-5"
|
||||
>
|
||||
<Breadcrumbs :items="breadcrumbs" />
|
||||
<router-link
|
||||
<Dropdown
|
||||
v-if="canCreateBatch()"
|
||||
:options="[
|
||||
{
|
||||
label: __('New Batch'),
|
||||
icon: 'users',
|
||||
onClick() {
|
||||
router.push({
|
||||
name: 'BatchForm',
|
||||
params: { batchName: 'new' },
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
label: __('Import Batch'),
|
||||
icon: 'upload',
|
||||
onClick() {
|
||||
router.push({
|
||||
name: 'NewDataImport',
|
||||
params: { doctype: 'LMS Batch' },
|
||||
})
|
||||
},
|
||||
},
|
||||
]"
|
||||
>
|
||||
<template v-slot="{ open }">
|
||||
<Button variant="solid">
|
||||
<template #prefix>
|
||||
<Plus class="h-4 w-4 stroke-1.5" />
|
||||
</template>
|
||||
{{ __('Create') }}
|
||||
<template #suffix>
|
||||
<ChevronDown
|
||||
:class="[
|
||||
'w-4 h-4 stroke-1.5 ml-1 transform transition-transform',
|
||||
open ? 'rotate-180' : '',
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Button>
|
||||
</template>
|
||||
</Dropdown>
|
||||
<!-- <router-link
|
||||
v-if="canCreateBatch()"
|
||||
:to="{
|
||||
name: 'BatchForm',
|
||||
@@ -16,7 +58,7 @@
|
||||
</template>
|
||||
{{ __('Create') }}
|
||||
</Button>
|
||||
</router-link>
|
||||
</router-link> -->
|
||||
</header>
|
||||
<div class="p-5 pb-10">
|
||||
<div
|
||||
@@ -90,13 +132,15 @@ import {
|
||||
Button,
|
||||
call,
|
||||
createListResource,
|
||||
Dropdown,
|
||||
FormControl,
|
||||
Select,
|
||||
TabButtons,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { computed, inject, onMounted, ref, watch } from 'vue'
|
||||
import { Plus } from 'lucide-vue-next'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ChevronDown, Plus } from 'lucide-vue-next'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import BatchCard from '@/components/BatchCard.vue'
|
||||
import EmptyState from '@/components/EmptyState.vue'
|
||||
@@ -115,6 +159,7 @@ const is_student = computed(() => user.data?.is_student)
|
||||
const currentTab = ref(is_student.value ? 'All' : 'Upcoming')
|
||||
const orderBy = ref('start_date')
|
||||
const readOnlyMode = window.read_only_mode
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(() => {
|
||||
setFiltersFromQuery()
|
||||
|
||||
@@ -3,20 +3,51 @@
|
||||
class="sticky flex items-center justify-between top-0 z-10 border-b bg-surface-white px-3 py-2.5 sm:px-5"
|
||||
>
|
||||
<Breadcrumbs :items="breadcrumbs" />
|
||||
<router-link
|
||||
|
||||
<Dropdown
|
||||
placement="start"
|
||||
side="bottom"
|
||||
v-if="canCreateCourse()"
|
||||
:to="{
|
||||
name: 'CourseForm',
|
||||
params: { courseName: 'new' },
|
||||
}"
|
||||
:options="[
|
||||
{
|
||||
label: __('New Course'),
|
||||
icon: 'book-open',
|
||||
onClick() {
|
||||
router.push({
|
||||
name: 'CourseForm',
|
||||
params: { courseName: 'new' },
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
label: __('Import Course'),
|
||||
icon: 'upload',
|
||||
onClick() {
|
||||
router.push({
|
||||
name: 'NewDataImport',
|
||||
params: { doctype: 'LMS Course' },
|
||||
})
|
||||
},
|
||||
},
|
||||
]"
|
||||
>
|
||||
<Button variant="solid">
|
||||
<template #prefix>
|
||||
<Plus class="h-4 w-4 stroke-1.5" />
|
||||
</template>
|
||||
{{ __('Create') }}
|
||||
</Button>
|
||||
</router-link>
|
||||
<template v-slot="{ open }">
|
||||
<Button variant="solid">
|
||||
<template #prefix>
|
||||
<Plus class="h-4 w-4 stroke-1.5" />
|
||||
</template>
|
||||
{{ __('Create') }}
|
||||
<template #suffix>
|
||||
<ChevronDown
|
||||
:class="[
|
||||
'w-4 h-4 stroke-1.5 ml-1 transform transition-transform',
|
||||
open ? 'rotate-180' : '',
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Button>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</header>
|
||||
<div class="p-5 pb-10">
|
||||
<div
|
||||
@@ -85,13 +116,14 @@ import {
|
||||
Button,
|
||||
call,
|
||||
createListResource,
|
||||
Dropdown,
|
||||
FormControl,
|
||||
Select,
|
||||
TabButtons,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { computed, inject, onMounted, ref, watch } from 'vue'
|
||||
import { Plus } from 'lucide-vue-next'
|
||||
import { ChevronDown, Plus } from 'lucide-vue-next'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { canCreateCourse } from '@/utils'
|
||||
import CourseCard from '@/components/CourseCard.vue'
|
||||
|
||||
50
frontend/src/pages/DataImport.vue
Normal file
50
frontend/src/pages/DataImport.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<DataImport
|
||||
:doctype="route.params.doctype"
|
||||
:importName="route.params.importName"
|
||||
:doctypeMap="doctypeMap"
|
||||
/>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { usePageMeta } from 'frappe-ui'
|
||||
import { DataImport } from 'frappe-ui/frappe'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { inject, onMounted } from 'vue'
|
||||
|
||||
const { brand } = sessionStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const user = inject<any>('$user')
|
||||
|
||||
onMounted(() => {
|
||||
if (!user.data?.is_moderator) {
|
||||
router.push({
|
||||
name: 'Courses',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const doctypeMap = {
|
||||
'LMS Course': {
|
||||
title: 'Courses',
|
||||
listRoute: '/courses',
|
||||
pageRoute: `/courses/docname`,
|
||||
},
|
||||
'LMS Batch': {
|
||||
title: 'Batches',
|
||||
listRoute: '/batches',
|
||||
},
|
||||
'LMS Category': {
|
||||
title: 'Categories',
|
||||
listRoute: '/lms',
|
||||
},
|
||||
}
|
||||
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: __('Data Import'),
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
<div v-if="createdBatches.data?.length" class="mt-10">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<span class="font-semibold text-lg">
|
||||
<span class="font-semibold text-lg text-ink-gray-9">
|
||||
{{ __('Upcoming Batches') }}
|
||||
</span>
|
||||
<router-link
|
||||
@@ -88,7 +88,7 @@
|
||||
|
||||
<div class="grid grid-cols-2 gap-5 mt-10">
|
||||
<div v-if="evals?.data?.length">
|
||||
<div class="font-semibold text-lg mb-3">
|
||||
<div class="font-semibold text-lg text-ink-gray-9 mb-3">
|
||||
{{ __('Upcoming Evaluations') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||
@@ -124,7 +124,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="liveClasses?.data?.length">
|
||||
<div class="font-semibold text-lg mb-3">
|
||||
<div class="font-semibold text-lg text-ink-gray-9 mb-3">
|
||||
{{ __('Upcoming Live Classes') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||
|
||||
@@ -17,86 +17,91 @@
|
||||
</header>
|
||||
<div class="max-w-4xl mx-auto pt-5 p-4">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-lg font-semibold text-ink-gray-9 mb-2">
|
||||
{{ applications.data?.length || 0 }}
|
||||
{{
|
||||
applications.data?.length === 1
|
||||
? __('Application')
|
||||
: __('Applications')
|
||||
}}
|
||||
<h1 class="text-xl font-semibold text-ink-gray-7 mb-4 md:mb-0">
|
||||
{{ applicationCount }}
|
||||
{{ applicationCount === 1 ? __('Application') : __('Applications') }}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<ListView
|
||||
v-if="applications.data?.length"
|
||||
:columns="applicationColumns"
|
||||
:rows="applicantRows"
|
||||
row-key="name"
|
||||
:options="{
|
||||
showTooltip: false,
|
||||
selectable: false,
|
||||
}"
|
||||
>
|
||||
<ListHeader
|
||||
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
|
||||
<div v-if="applications.data?.length">
|
||||
<ListView
|
||||
:columns="applicationColumns"
|
||||
:rows="applicantRows"
|
||||
row-key="name"
|
||||
:options="{
|
||||
showTooltip: false,
|
||||
selectable: false,
|
||||
}"
|
||||
>
|
||||
<ListHeaderItem
|
||||
:item="item"
|
||||
v-for="item in applicationColumns"
|
||||
:key="item.key"
|
||||
<ListHeader
|
||||
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
|
||||
>
|
||||
<template #prefix="{ item }">
|
||||
<FeatherIcon
|
||||
v-if="item.icon"
|
||||
:name="item.icon?.toString()"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
</template>
|
||||
</ListHeaderItem>
|
||||
</ListHeader>
|
||||
<ListRows>
|
||||
<ListRow
|
||||
:row="row"
|
||||
v-slot="{ column, item }"
|
||||
v-for="row in applicantRows"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
<ListRowItem :item="item">
|
||||
<div
|
||||
v-if="column.key === 'full_name'"
|
||||
class="flex items-center space-x-3"
|
||||
>
|
||||
<Avatar
|
||||
size="sm"
|
||||
:image="row['user_image']"
|
||||
:label="row['full_name']"
|
||||
<ListHeaderItem
|
||||
:item="item"
|
||||
v-for="item in applicationColumns"
|
||||
:key="item.key"
|
||||
>
|
||||
<template #prefix="{ item }">
|
||||
<FeatherIcon
|
||||
v-if="item.icon"
|
||||
:name="item.icon?.toString()"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
</template>
|
||||
</ListHeaderItem>
|
||||
</ListHeader>
|
||||
<ListRows>
|
||||
<ListRow
|
||||
:row="row"
|
||||
v-slot="{ column, item }"
|
||||
v-for="row in applicantRows"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
<ListRowItem :item="item">
|
||||
<div
|
||||
v-if="column.key === 'full_name'"
|
||||
class="flex items-center space-x-3"
|
||||
>
|
||||
<Avatar
|
||||
size="sm"
|
||||
:image="row['user_image']"
|
||||
:label="row['full_name']"
|
||||
/>
|
||||
|
||||
<span>{{ item }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="column.key === 'actions'"
|
||||
class="flex justify-center"
|
||||
>
|
||||
<Dropdown :options="getActionOptions(row)">
|
||||
<Button variant="ghost">
|
||||
<FeatherIcon name="more-horizontal" class="w-4 h-4" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="column.key === 'applied_on'"
|
||||
class="text-sm text-ink-gray-6"
|
||||
>
|
||||
{{ item }}
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ item }}
|
||||
</div>
|
||||
</ListRowItem>
|
||||
</ListRow>
|
||||
</ListRows>
|
||||
</ListView>
|
||||
<span>{{ item }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="column.key === 'actions'"
|
||||
class="flex justify-center"
|
||||
>
|
||||
<Dropdown :options="getActionOptions(row)">
|
||||
<Button variant="ghost">
|
||||
<FeatherIcon name="more-horizontal" class="w-4 h-4" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="column.key === 'applied_on'"
|
||||
class="text-sm text-ink-gray-6"
|
||||
>
|
||||
{{ item }}
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ item }}
|
||||
</div>
|
||||
</ListRowItem>
|
||||
</ListRow>
|
||||
</ListRows>
|
||||
</ListView>
|
||||
<div class="flex justify-center mt-5">
|
||||
<Button v-if="applications.hasNextPage" @click="applications.next()">
|
||||
<template #prefix>
|
||||
<RefreshCw class="size-4 stroke-1.5" />
|
||||
</template>
|
||||
{{ __('Load More') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<EmptyState v-else-if="!applications.loading" type="Job Applications" />
|
||||
</div>
|
||||
|
||||
@@ -150,6 +155,7 @@ import {
|
||||
Avatar,
|
||||
Button,
|
||||
Breadcrumbs,
|
||||
call,
|
||||
Dialog,
|
||||
Dropdown,
|
||||
FeatherIcon,
|
||||
@@ -166,8 +172,8 @@ import {
|
||||
usePageMeta,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
|
||||
import { inject, ref, computed, reactive } from 'vue'
|
||||
import { RefreshCw } from 'lucide-vue-next'
|
||||
import { computed, inject, onMounted, ref, reactive } from 'vue'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import EmptyState from '@/components/EmptyState.vue'
|
||||
|
||||
@@ -175,6 +181,7 @@ const dayjs = inject('$dayjs')
|
||||
const { brand } = sessionStore()
|
||||
const showEmailModal = ref(false)
|
||||
const selectedApplicant = ref(null)
|
||||
const applicationCount = ref(0)
|
||||
const emailForm = reactive({
|
||||
subject: '',
|
||||
message: '',
|
||||
@@ -188,6 +195,19 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getApplicationCount()
|
||||
})
|
||||
|
||||
const getApplicationCount = () => {
|
||||
call('frappe.client.get_count', {
|
||||
doctype: 'LMS Job Application',
|
||||
filters: { job: props.job },
|
||||
}).then((count) => {
|
||||
applicationCount.value = count
|
||||
})
|
||||
}
|
||||
|
||||
const applications = createListResource({
|
||||
doctype: 'LMS Job Application',
|
||||
fields: [
|
||||
@@ -253,7 +273,6 @@ const sendEmail = (close) => {
|
||||
}
|
||||
|
||||
const downloadResume = (resumeUrl) => {
|
||||
console.log(resumeUrl)
|
||||
window.open(resumeUrl, '_blank')
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</Button>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="user.data.name == job.data?.owner"
|
||||
v-if="canManageJob"
|
||||
:to="{
|
||||
name: 'JobForm',
|
||||
params: { jobName: job.data?.name },
|
||||
@@ -240,9 +240,7 @@ const redirectToWebsite = (url) => {
|
||||
|
||||
const canManageJob = computed(() => {
|
||||
if (!user.data?.name || !job.data) return false
|
||||
return (
|
||||
user.data.name === job.data.owner || user.data.roles?.includes('Moderator')
|
||||
)
|
||||
return user.data.name === job.data.owner || user.data?.is_moderator
|
||||
})
|
||||
|
||||
usePageMeta(() => {
|
||||
|
||||
@@ -207,6 +207,11 @@ const jobDetail = createResource({
|
||||
}
|
||||
},
|
||||
onSuccess(data) {
|
||||
if (data.owner != user.data?.name && !user.data?.is_moderator) {
|
||||
router.push({
|
||||
name: 'Jobs',
|
||||
})
|
||||
}
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (Object.hasOwn(job, key)) job[key] = data[key]
|
||||
})
|
||||
@@ -242,7 +247,11 @@ const job = reactive({
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (!user.data) window.location.href = '/login'
|
||||
if (!user.data) {
|
||||
router.push({
|
||||
name: 'Jobs',
|
||||
})
|
||||
}
|
||||
|
||||
if (props.jobName != 'new') jobDetail.reload()
|
||||
})
|
||||
|
||||
@@ -32,10 +32,13 @@
|
||||
{{ __('{0} Open Jobs').format(jobCount) }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="grid grid-cols-1 gap-2 md:grid-cols-4"
|
||||
:class="user.data ? 'md:grid-cols-3' : 'md:grid-cols-2'"
|
||||
>
|
||||
<div class="flex items-center justify-between space-x-4">
|
||||
<TabButtons
|
||||
v-if="tabs.length > 1"
|
||||
v-model="activeTab"
|
||||
:buttons="tabs"
|
||||
@change="updateJobs"
|
||||
/>
|
||||
<FormControl
|
||||
type="text"
|
||||
:placeholder="__('Search')"
|
||||
@@ -55,13 +58,13 @@
|
||||
doctype="Country"
|
||||
v-model="country"
|
||||
:placeholder="__('Country')"
|
||||
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
|
||||
class="min-w-32 lg:min-w-0 lg:w-32 xl:w-32"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="jobType"
|
||||
type="select"
|
||||
:options="jobTypes"
|
||||
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
|
||||
class="min-w-32 lg:min-w-0 lg:w-32 xl:w-32"
|
||||
:placeholder="__('Type')"
|
||||
@change="updateJobs"
|
||||
/>
|
||||
@@ -69,7 +72,7 @@
|
||||
v-model="workMode"
|
||||
type="select"
|
||||
:options="workModes"
|
||||
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
|
||||
class="min-w-32 lg:min-w-0 lg:w-32 xl:w-32"
|
||||
:placeholder="__('Work Mode')"
|
||||
@change="updateJobs"
|
||||
/>
|
||||
@@ -100,6 +103,7 @@ import {
|
||||
call,
|
||||
createResource,
|
||||
FormControl,
|
||||
TabButtons,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { Plus, Search } from 'lucide-vue-next'
|
||||
@@ -118,9 +122,38 @@ const country = ref(null)
|
||||
const filters = ref({})
|
||||
const orFilters = ref({})
|
||||
const jobCount = ref(0)
|
||||
const closedJobs = ref(0)
|
||||
const activeTab = ref('Open')
|
||||
const readOnlyMode = window.read_only_mode
|
||||
|
||||
onMounted(() => {
|
||||
getClosedJobCount()
|
||||
setFiltersFromURL()
|
||||
updateJobs()
|
||||
})
|
||||
|
||||
const isModerator = computed(() => {
|
||||
return user.data?.is_moderator
|
||||
})
|
||||
|
||||
const getClosedJobCount = () => {
|
||||
const filters = {
|
||||
status: 'Closed',
|
||||
}
|
||||
|
||||
if (!isModerator.value) {
|
||||
filters.owner = user.data?.name
|
||||
}
|
||||
|
||||
call('frappe.client.get_count', {
|
||||
doctype: 'Job Opportunity',
|
||||
filters: filters,
|
||||
}).then((count) => {
|
||||
closedJobs.value = count
|
||||
})
|
||||
}
|
||||
|
||||
const setFiltersFromURL = () => {
|
||||
let queries = new URLSearchParams(location.search)
|
||||
if (queries.has('type')) {
|
||||
jobType.value = queries.get('type')
|
||||
@@ -128,7 +161,22 @@ onMounted(() => {
|
||||
if (queries.has('work_mode')) {
|
||||
workMode.value = queries.get('work_mode')
|
||||
}
|
||||
updateJobs()
|
||||
}
|
||||
|
||||
const tabs = computed(() => {
|
||||
const tabsArray = [
|
||||
{
|
||||
label: __('Open'),
|
||||
},
|
||||
]
|
||||
|
||||
if (closedJobs.value) {
|
||||
tabsArray.push({
|
||||
label: __('Closed'),
|
||||
})
|
||||
}
|
||||
|
||||
return tabsArray
|
||||
})
|
||||
|
||||
const jobs = createResource({
|
||||
@@ -149,7 +197,6 @@ const updateJobs = () => {
|
||||
|
||||
const updateFilters = () => {
|
||||
filters.value.status = 'Open'
|
||||
filters.value.disabled = 0
|
||||
|
||||
if (jobType.value) {
|
||||
filters.value.type = jobType.value
|
||||
@@ -178,8 +225,22 @@ const updateFilters = () => {
|
||||
} else {
|
||||
delete filters.value.country
|
||||
}
|
||||
|
||||
if (activeTab.value === 'Closed') {
|
||||
filters.value.status = 'Closed'
|
||||
if (!isModerator.value) {
|
||||
filters.value.owner = user.data?.name
|
||||
}
|
||||
} else {
|
||||
filters.value.status = 'Open'
|
||||
delete filters.value.owner
|
||||
}
|
||||
}
|
||||
|
||||
watch(activeTab, (val) => {
|
||||
updateJobs()
|
||||
})
|
||||
|
||||
watch(country, (val) => {
|
||||
updateJobs()
|
||||
})
|
||||
@@ -190,7 +251,7 @@ watch(jobs, () => {
|
||||
|
||||
const jobTypes = computed(() => {
|
||||
return [
|
||||
'',
|
||||
{ label: '', value: '' },
|
||||
{ label: __('Full Time'), value: 'Full Time' },
|
||||
{ label: __('Part Time'), value: 'Part Time' },
|
||||
{ label: __('Contract'), value: 'Contract' },
|
||||
@@ -200,7 +261,7 @@ const jobTypes = computed(() => {
|
||||
|
||||
const workModes = computed(() => {
|
||||
return [
|
||||
'',
|
||||
{ label: '', value: '' },
|
||||
{ label: 'On site', value: 'On-site' },
|
||||
{ label: 'Hybrid', value: 'Hybrid' },
|
||||
{ label: 'Remote', value: 'Remote' },
|
||||
|
||||
@@ -58,15 +58,15 @@
|
||||
</Button>
|
||||
</div>
|
||||
<ListView
|
||||
v-if="programCourses.data.length > 0"
|
||||
v-if="program.program_courses?.length > 0"
|
||||
:columns="courseColumns"
|
||||
:rows="programCourses.data"
|
||||
:rows="program.program_courses"
|
||||
:options="{
|
||||
selectable: true,
|
||||
resizeColumn: true,
|
||||
showTooltip: false,
|
||||
}"
|
||||
rowKey="name"
|
||||
:rowKey="programName === 'new' ? 'course' : 'name'"
|
||||
>
|
||||
<ListHeader
|
||||
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
|
||||
@@ -75,8 +75,8 @@
|
||||
</ListHeader>
|
||||
<ListRows>
|
||||
<Draggable
|
||||
:list="programCourses.data"
|
||||
item-key="name"
|
||||
:list="program.program_courses"
|
||||
:item-key="programName === 'new' ? 'course' : 'name'"
|
||||
group="items"
|
||||
@end="updateOrder"
|
||||
class="cursor-move"
|
||||
@@ -133,14 +133,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<ListView
|
||||
v-if="programMembers.data.length > 0"
|
||||
v-if="program.program_members?.length > 0"
|
||||
:columns="memberColumns"
|
||||
:rows="programMembers.data"
|
||||
:rows="program.program_members"
|
||||
:options="{
|
||||
selectable: true,
|
||||
resizeColumn: true,
|
||||
}"
|
||||
rowKey="name"
|
||||
:rowKey="programName === 'new' ? 'member' : 'name'"
|
||||
>
|
||||
<ListHeader
|
||||
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" />
|
||||
</ListHeader>
|
||||
<ListRows>
|
||||
<ListRow :row="row" v-for="row in programMembers.data" />
|
||||
<ListRow :row="row" v-for="row in program.program_members" />
|
||||
</ListRows>
|
||||
<ListSelectBanner>
|
||||
<template #actions="{ unselectAll, selections }">
|
||||
@@ -217,13 +217,12 @@
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ close }">
|
||||
<div class="flex justify-end space-x-2 group">
|
||||
<div class="flex justify-end space-x-2">
|
||||
<Button
|
||||
v-if="programName != 'new'"
|
||||
@click="deleteProgram(close)"
|
||||
variant="outline"
|
||||
theme="red"
|
||||
class="invisible group-hover:visible"
|
||||
>
|
||||
<template #prefix>
|
||||
<Trash2 class="size-4 stroke-1.5" />
|
||||
@@ -252,7 +251,7 @@ import {
|
||||
ListRow,
|
||||
toast,
|
||||
} 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 { Programs, Program } from '@/types/programs'
|
||||
import { escapeHTML, openSettings } from '@/utils'
|
||||
@@ -269,6 +268,9 @@ const member = ref<string>('')
|
||||
const showProgressDialog = ref(false)
|
||||
const dirty = ref(false)
|
||||
|
||||
const app = getCurrentInstance()
|
||||
const { $dialog } = app.appContext.config.globalProperties
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
programName: string | null
|
||||
@@ -427,25 +429,22 @@ const addCourse = (close: () => void) => {
|
||||
return
|
||||
}
|
||||
|
||||
programCourses.insert.submit(
|
||||
{
|
||||
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))
|
||||
},
|
||||
}
|
||||
const existingCourse = program.value.program_courses.find(
|
||||
(c: any) => c.course === course.value
|
||||
)
|
||||
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) => {
|
||||
@@ -454,24 +453,21 @@ const addMember = (close: () => void) => {
|
||||
return
|
||||
}
|
||||
|
||||
programMembers.insert.submit(
|
||||
{
|
||||
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))
|
||||
},
|
||||
}
|
||||
const existingMember = program.value.program_members.find(
|
||||
(m) => m.member === member.value
|
||||
)
|
||||
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 (
|
||||
@@ -509,57 +505,83 @@ const updateCounts = async (
|
||||
const updateOrder = async (e: DragEvent) => {
|
||||
let sourceIdx = e.from.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()) {
|
||||
programCourses.setValue.submit(
|
||||
{
|
||||
name: course.name,
|
||||
idx: index + 1,
|
||||
},
|
||||
{
|
||||
onError(err: any) {
|
||||
toast.warning(__(err.messages?.[0] || err))
|
||||
if (props.programName === 'new') {
|
||||
let courses = program.value.program_courses
|
||||
courses.splice(targetIdx, 0, courses.splice(sourceIdx, 1)[0])
|
||||
courses.forEach((course, index) => {
|
||||
course.idx = index + 1
|
||||
})
|
||||
dirty.value = true
|
||||
} else {
|
||||
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,
|
||||
},
|
||||
}
|
||||
)
|
||||
await wait(100)
|
||||
{
|
||||
onError(err: any) {
|
||||
toast.warning(__(err.messages?.[0] || err))
|
||||
},
|
||||
}
|
||||
)
|
||||
await wait(100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wait = (ms: number) => new Promise((res) => setTimeout(res, ms))
|
||||
|
||||
const remove = async (
|
||||
const remove = (
|
||||
selections: string[],
|
||||
unselectAll: () => void,
|
||||
type: string
|
||||
) => {
|
||||
selections = Array.from(selections)
|
||||
for (const selection of selections) {
|
||||
if (type == 'courses') {
|
||||
await programCourses.delete.submit(selection)
|
||||
await updateCounts('course', 'remove')
|
||||
} else {
|
||||
await programMembers.delete.submit(selection)
|
||||
await updateCounts('member', 'remove')
|
||||
}
|
||||
await programs.value.reload()
|
||||
await wait(100)
|
||||
const selectionsArray = Array.from(selections)
|
||||
if (type === 'courses') {
|
||||
program.value.program_courses = program.value.program_courses.filter(
|
||||
(c: any) => !selectionsArray.includes(c.name || c.course)
|
||||
)
|
||||
} else {
|
||||
program.value.program_members = program.value.program_members.filter(
|
||||
(m: any) => !selectionsArray.includes(m.name || m.member)
|
||||
)
|
||||
}
|
||||
dirty.value = true
|
||||
unselectAll()
|
||||
}
|
||||
|
||||
const deleteProgram = (close: () => void) => {
|
||||
if (props.programName == 'new') return
|
||||
programs.value?.delete.submit(props.programName, {
|
||||
onSuccess() {
|
||||
toast.success(__('Program deleted successfully'))
|
||||
close()
|
||||
},
|
||||
onError(err: any) {
|
||||
toast.warning(__(err.messages?.[0] || err))
|
||||
},
|
||||
$dialog({
|
||||
title: __('Delete Program'),
|
||||
message: __(
|
||||
'Are you sure you want to delete this program? This action cannot be undone.'
|
||||
),
|
||||
actions: [
|
||||
{
|
||||
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 [
|
||||
{
|
||||
label: 'Title',
|
||||
key: 'course_title',
|
||||
key: props.programName === 'new' ? 'course' : 'course_title',
|
||||
width: 1,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -31,11 +31,11 @@
|
||||
categoryColumn: 'category',
|
||||
valueColumn: 'count',
|
||||
colors: [
|
||||
theme.colors.red['400'],
|
||||
theme.colors.amber['400'],
|
||||
theme.colors.pink['400'],
|
||||
theme.colors.blue['400'],
|
||||
theme.colors.green['400'],
|
||||
getColor('red', 400),
|
||||
getColor('amber', 400),
|
||||
getColor('pink', 400),
|
||||
getColor('blue', 400),
|
||||
getColor('green', 400),
|
||||
],
|
||||
}"
|
||||
/>
|
||||
@@ -74,7 +74,7 @@ import {
|
||||
} from 'frappe-ui'
|
||||
import type { ProgramMember } from '@/types'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { theme } from '@/utils/theme'
|
||||
import { getColor } from '@/utils'
|
||||
|
||||
const show = defineModel<boolean>({ default: false })
|
||||
const searchFilter = ref<string | null>(null)
|
||||
|
||||
Reference in New Issue
Block a user