chore: resolved conflicts

This commit is contained in:
Jannat Patel
2026-01-29 19:55:13 +05:30
40 changed files with 2125 additions and 1214 deletions

View File

@@ -19,9 +19,16 @@
showOptions = true
}
"
@click="
(e) => {
showOptions = true
nextTick(() => {
setFocus()
})
}
"
@focus="
() => {
showOptions = true
if (!filterOptions.data || filterOptions.data.length === 0) {
reload('')
}
@@ -55,7 +62,11 @@
>
<div class="flex flex-col gap-1 p-1">
<div class="text-base font-medium text-ink-gray-8">
{{ option.description }}
{{
option.value == option.label
? option.description
: option.label
}}
</div>
<div class="text-sm text-ink-gray-5">
{{ option.value }}
@@ -112,7 +123,7 @@ import {
} from '@headlessui/vue'
import { createResource, Popover, Button } from 'frappe-ui'
import { ref, computed, nextTick, useAttrs } from 'vue'
import { watchDebounced } from '@vueuse/core'
import { set, watchDebounced } from '@vueuse/core'
import { X, Plus } from 'lucide-vue-next'
const props = defineProps({
@@ -146,18 +157,20 @@ const props = defineProps({
const values = defineModel()
const attrs = useAttrs()
const emails = ref([])
const search = ref(null)
const error = ref(null)
const query = ref('')
const text = ref('')
const showOptions = ref(false)
const emit = defineEmits(['update:modelValue'])
const selectedValue = computed({
get: () => query.value || '',
set: (val) => {
query.value = ''
val?.value && addValue(val.value)
showOptions.value = false
emit('update:modelValue', values.value)
},
})
@@ -229,6 +242,7 @@ const addValue = (value) => {
const removeValue = (value) => {
values.value = values.value.filter((v) => v !== value)
emit('update:modelValue', values.value)
}
function setFocus() {

View File

@@ -72,7 +72,7 @@ const emit = defineEmits<{
const props = withDefaults(
defineProps<{
modelValue: string
modelValue: string | null
label?: string
description?: string
type?: 'image' | 'video'

View File

@@ -37,7 +37,7 @@
<CertificationLinks :courseName="course.data.name" class="w-full" />
</div>
<router-link
v-else-if="course.data.paid_course"
v-else-if="course.data.paid_course && !isAdmin"
:to="{
name: 'Billing',
params: {
@@ -56,14 +56,14 @@
</Button>
</router-link>
<Badge
v-else-if="course.data.disable_self_learning"
v-else-if="course.data.disable_self_learning && !isAdmin"
theme="blue"
size="lg"
>
{{ __('Contact the Administrator to enroll for this course.') }}
{{ __('Contact the Administrator to enroll for this course') }}
</Badge>
<Button
v-else-if="!user.data?.is_moderator && !is_instructor()"
v-else-if="!isAdmin"
@click="enrollStudent()"
variant="solid"
class="w-full"
@@ -88,40 +88,11 @@
</template>
{{ __('Get Certificate') }}
</Button>
<Button
v-if="user.data?.is_moderator || is_instructor()"
class="w-full mt-2"
size="md"
@click="showProgressSummary"
>
<template #prefix>
<TrendingUp class="size-4 stroke-1.5" />
{{ __('Progress Summary') }}
</template>
</Button>
<router-link
v-if="user?.data?.is_moderator || is_instructor()"
:to="{
name: 'CourseForm',
params: {
courseName: course.data.name,
},
}"
>
<Button variant="subtle" class="w-full mt-2" size="md">
<template #prefix>
<Pencil class="size-4 stroke-1.5" />
</template>
<span>
{{ __('Edit') }}
</span>
</Button>
</router-link>
</div>
<div class="space-y-4">
<div
class="font-medium text-ink-gray-9"
:class="{ 'mt-8': !readOnlyMode }"
:class="{ 'mt-8': course.data.membership && !readOnlyMode }"
>
{{ __('This course has:') }}
</div>
@@ -168,12 +139,6 @@
</div>
</div>
</div>
<CourseProgressSummary
v-if="user.data?.is_moderator || is_instructor()"
v-model="showProgressModal"
:courseName="course.data.name"
:enrollments="course.data.enrollments"
/>
</template>
<script setup>
import {
@@ -191,12 +156,10 @@ import { Badge, Button, call, createResource, toast } from 'frappe-ui'
import { formatAmount } from '@/utils/'
import { useRouter } from 'vue-router'
import CertificationLinks from '@/components/CertificationLinks.vue'
import CourseProgressSummary from '@/components/Modals/CourseProgressSummary.vue'
import { useTelemetry } from 'frappe-ui/frappe'
const router = useRouter()
const user = inject('$user')
const showProgressModal = ref(false)
const readOnlyMode = window.read_only_mode
const { capture } = useTelemetry()
@@ -295,7 +258,7 @@ const fetchCertificate = () => {
})
}
const showProgressSummary = () => {
showProgressModal.value = true
}
const isAdmin = computed(() => {
return user.data?.is_moderator || is_instructor()
})
</script>

View File

@@ -15,7 +15,10 @@
{{ __(title) }}
</div>
<Button size="sm" v-if="allowEdit" @click="openChapterModal()">
{{ __('Add Chapter') }}
<template #prefix>
<Plus class="size-4 stroke-1.5" />
</template>
{{ __('Add') }}
</Button>
</div>
<div
@@ -174,6 +177,7 @@ import {
FilePenLine,
HelpCircle,
MonitorPlay,
Plus,
Trash2,
} from 'lucide-vue-next'
import { useRoute, useRouter } from 'vue-router'

View File

@@ -23,10 +23,8 @@
(value, close) => {
close()
router.push({
name: 'CourseForm',
params: {
courseName: 'new',
},
name: 'Courses',
query: { newCourse: '1' },
})
}
"

View File

@@ -1,231 +0,0 @@
<template>
<Dialog
v-model="show"
:options="{
title: __('Course Progress Summary'),
size: '5xl',
}"
>
<template #body-content>
<div
class="flex flex-col-reverse md:flex-row justify-between md:space-x-10 text-base mt-10"
>
<div class="w-full">
<div class="flex items-center justify-between space-x-5 mb-4">
<FormControl
v-model="searchFilter"
:placeholder="__('Search by Member')"
type="text"
class="w-full"
/>
</div>
<div class="max-h-[70vh] overflow-y-auto">
<ListView
v-if="progressList.loading || progressList.data?.length"
:columns="progressColumns"
:rows="progressList.data"
rowKey="name"
:options="{
selectable: false,
showTooltip: false,
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
>
<ListHeaderItem
:item="item"
v-for="item in progressColumns"
:key="item.key"
>
<template #prefix="{ item }">
<FeatherIcon
:name="item.icon?.toString()"
class="h-4 w-4"
/>
</template>
</ListHeaderItem>
</ListHeader>
<ListRows v-for="row in progressList.data">
<router-link
:to="{
name: 'Profile',
params: { username: row.member_username },
}"
>
<ListRow :row="row">
<template #default="{ column, item }">
<ListRowItem
:item="row[column.key]"
:align="column.align"
>
<template #prefix>
<div v-if="column.key == 'member_name'">
<Avatar
class="flex items-center"
:image="row['member_image']"
:label="item"
size="sm"
/>
</div>
</template>
<div>
{{ row[column.key].toString() }}
</div>
</ListRowItem>
</template>
</ListRow>
</router-link>
</ListRows>
</ListView>
<div
v-if="progressList.data && progressList.hasNextPage"
class="flex justify-center my-5"
>
<Button @click="progressList.next()">
{{ __('Load More') }}
</Button>
</div>
</div>
</div>
<div class="mb-4 self-start w-full space-y-5">
<div
class="flex flex-col md:flex-row items-center space-y-2 md:space-y-0 md:space-x-4"
>
<NumberChart
class="border rounded-md w-full"
:config="{
title: __('Enrollments'),
value: memberCount || 0,
}"
/>
<NumberChart
class="border rounded-md w-full"
:config="{
title: __('Average Progress %'),
value: chartDetails.data?.average_progress || 0,
}"
/>
</div>
<DonutChart
:config="{
data: chartDetails.data?.progress_distribution || [],
title: __('Progress Distribution'),
categoryColumn: 'category',
valueColumn: 'count',
colors: [
getColor('red', 400),
getColor('amber', 400),
getColor('pink', 400),
getColor('blue', 400),
getColor('green', 400),
],
}"
/>
</div>
</div>
</template>
</Dialog>
</template>
<script setup lang="ts">
import {
Avatar,
Button,
createListResource,
createResource,
Dialog,
DonutChart,
FeatherIcon,
FormControl,
ListView,
ListHeader,
ListHeaderItem,
ListRows,
ListRow,
ListRowItem,
NumberChart,
} from 'frappe-ui'
import { computed, ref, watch } from 'vue'
import { getColor } from '@/utils'
const show = defineModel<boolean>({ default: false })
const searchFilter = ref<string | null>(null)
type Filters = {
course: string | undefined
member_name?: string[]
}
const props = defineProps<{
courseName?: string
enrollments?: number
}>()
const memberCount = ref<number>(props.enrollments || 0)
const chartDetails = createResource({
url: 'lms.lms.api.get_course_progress_distribution',
params: {
course: props.courseName,
},
auto: true,
})
const progressList = createListResource({
doctype: 'LMS Enrollment',
filters: {
course: props.courseName,
},
fields: [
'name',
'member',
'member_name',
'member_image',
'member_username',
'progress',
],
pageLength: 50,
auto: true,
})
watch([searchFilter], () => {
let filterApplied = false
let filters: Filters = {
course: props.courseName,
}
if (searchFilter.value) {
filters.member_name = ['like', `%${searchFilter.value}%`]
filterApplied = true
}
progressList.update({
filters: filters,
})
progressList.reload(
{},
{
onSuccess(data: any[]) {
memberCount.value = filterApplied ? data.length : props.enrollments || 0
},
}
)
})
const progressColumns = computed(() => {
return [
{
label: __('Member'),
key: 'member_name',
width: '60%',
icon: 'user',
},
{
label: __('Progress'),
key: 'progress',
align: 'right',
icon: 'trending-up',
},
]
})
</script>

View File

@@ -0,0 +1,20 @@
<template>
<div class="border rounded-lg p-3 space-y-2">
<div class="text-ink-gray-5">
{{ __(title) }}
</div>
<div class="flex items-center space-x-2">
<slot name="prefix" />
<div class="font-semibold text-2xl">
{{ value }}
</div>
<slot name="suffix" />
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
title: string
value: number | string
}>()
</script>

View File

@@ -1,6 +1,9 @@
<template>
<Tooltip :text="`${props.progress}%`">
<div class="w-full bg-surface-gray-3 rounded-full h-1">
<div
class="w-full bg-surface-gray-3 rounded-full h-1"
:class="$attrs.class"
>
<div
class="bg-surface-gray-7 rounded-full"
:class="progressBarHeight"

View File

@@ -1,16 +1,25 @@
<template>
<div class="flex flex-col justify-between h-full">
<div class="flex flex-col h-full">
<div>
<div class="flex items-center justify-between">
<div class="font-semibold mb-1 text-ink-gray-9">
{{ __(label) }}
</div>
<Badge
v-if="isDirty"
:label="__('Not Saved')"
variant="subtle"
theme="orange"
/>
<div class="space-x-2">
<Badge
v-if="isDirty"
:label="__('Not Saved')"
variant="subtle"
theme="orange"
/>
<Button
variant="solid"
:loading="saveSettings.loading"
@click="update"
>
{{ __('Update') }}
</Button>
</div>
</div>
<div class="text-xs text-ink-gray-5">
{{ __(description) }}
@@ -19,11 +28,6 @@
<div class="overflow-y-auto">
<SettingFields :sections="sections" :data="branding.data" />
</div>
<div class="flex flex-row-reverse mt-auto">
<Button variant="solid" :loading="saveSettings.loading" @click="update">
{{ __('Update') }}
</Button>
</div>
</div>
</template>
<script setup>

View File

@@ -8,22 +8,24 @@
<h1 class="mb-3 px-2 pt-2 text-lg font-semibold text-ink-gray-9">
{{ __('Settings') }}
</h1>
<div v-for="tab in tabs" :key="tab.label">
<div
v-if="!tab.hideLabel"
class="mb-2 mt-3 flex cursor-pointer gap-1.5 px-1 text-base text-ink-gray-5 transition-all duration-300 ease-in-out"
>
<span>{{ __(tab.label) }}</span>
</div>
<nav class="space-y-1">
<div v-for="item in tab.items" @click="activeTab = item">
<SidebarLink
:link="item"
:key="item.label"
:activeTab="activeTab?.label"
/>
<div class="space-y-5">
<div v-for="tab in tabs" :key="tab.label">
<div
v-if="!tab.hideLabel"
class="mb-2 mt-3 flex cursor-pointer gap-1.5 px-1 text-base text-ink-gray-5 transition-all duration-300 ease-in-out"
>
<span>{{ __(tab.label) }}</span>
</div>
</nav>
<nav class="space-y-1">
<div v-for="item in tab.items" @click="activeTab = item">
<SidebarLink
:link="item"
:key="item.label"
:activeTab="activeTab?.label"
/>
</div>
</nav>
</div>
</div>
</div>
<div

View File

@@ -1,12 +1,33 @@
<template>
<div class="flex flex-col h-full text-base">
<div class="flex items-center space-x-2 mb-8 -ml-1.5">
<ChevronLeft
class="size-5 stroke-1.5 text-ink-gray-7 cursor-pointer"
@click="emit('updateStep', 'list')"
/>
<div class="text-xl font-semibold text-ink-gray-9">
{{ __('Transaction Details') }}
<div class="flex items-center justify-between mb-10 -ml-1.5">
<div class="flex items-center space-x-2">
<ChevronLeft
class="size-5 stroke-1.5 text-ink-gray-7 cursor-pointer"
@click="emit('updateStep', 'list')"
/>
<div class="text-xl font-semibold text-ink-gray-9">
{{ __('Transaction Details') }}
</div>
</div>
<div class="space-x-2">
<Button
v-if="
transactionData?.payment_for_document_type &&
transactionData?.payment_for_document
"
@click="openDetails()"
>
{{ __('Open the ') }}
{{
transactionData.payment_for_document_type == 'LMS Course'
? __('Course')
: __('Batch')
}}
</Button>
<Button variant="solid" @click="saveTransaction()">
{{ __('Save') }}
</Button>
</div>
</div>
<div v-if="transactionData" class="overflow-y-auto">
@@ -21,6 +42,12 @@
type="checkbox"
v-model="transactionData.payment_for_certificate"
/>
<FormControl
:label="__('Member Consent')"
type="checkbox"
v-model="transactionData.member_consent"
:disabled="true"
/>
</div>
<div class="grid grid-cols-3 gap-5 mt-5">
@@ -28,22 +55,27 @@
:label="__('Member')"
doctype="User"
v-model="transactionData.member"
:required="true"
/>
<FormControl
:label="__('Billing Name')"
v-model="transactionData.billing_name"
:required="true"
/>
<Link
:label="__('Source')"
v-model="transactionData.source"
doctype="LMS Source"
/>
<Link
<FormControl
type="select"
:options="documentTypeOptions"
:label="__('Payment For Document Type')"
v-model="transactionData.payment_for_document_type"
doctype="DocType"
/>
<Link
v-if="transactionData.payment_for_document_type"
:label="__('Payment For Document')"
v-model="transactionData.payment_for_document"
:doctype="transactionData.payment_for_document_type"
@@ -58,8 +90,13 @@
:label="__('Currency')"
v-model="transactionData.currency"
doctype="Currency"
:required="true"
/>
<FormControl
:label="__('Amount')"
v-model="transactionData.amount"
:required="true"
/>
<FormControl :label="__('Amount')" v-model="transactionData.amount" />
<FormControl
v-if="transactionData.amount_with_gst"
:label="__('Amount with GST')"
@@ -103,6 +140,7 @@
:label="__('Address')"
v-model="transactionData.address"
doctype="Address"
:required="true"
/>
<FormControl :label="__('GSTIN')" v-model="transactionData.gstin" />
<FormControl :label="__('PAN')" v-model="transactionData.pan" />
@@ -116,25 +154,12 @@
/>
</div>
</div>
<div class="space-x-2 mt-auto ml-auto">
<Button @click="openDetails()">
{{ __('Open the ') }}
{{
data.payment_for_document_type == 'LMS Course'
? __('Course')
: __('Batch')
}}
</Button>
<Button variant="solid" @click="saveTransaction()">
{{ __('Save') }}
</Button>
</div>
</div>
</template>
<script setup lang="ts">
import { Button, FormControl } from 'frappe-ui'
import { Button, FormControl, toast } from 'frappe-ui'
import { useRouter } from 'vue-router'
import { ref, watch } from 'vue'
import { computed, ref, watch } from 'vue'
import { ChevronLeft } from 'lucide-vue-next'
import Link from '@/components/Controls/Link.vue'
@@ -148,21 +173,40 @@ const props = defineProps<{
data: any
}>()
watch(
() => props.data,
(newVal) => {
transactionData.value = newVal ? { ...newVal } : null
},
{ immediate: true }
)
const saveTransaction = () => {
if (props.data?.name) {
updateTransaction()
} else {
createTransaction()
}
}
const saveTransaction = (close: () => void) => {
props.transactions.value.setValue
const createTransaction = () => {
console.log(props.transactions)
props.transactions.insert
.submit({
...transactionData.value,
})
.then(() => {
close()
toast.success(__('Transaction created successfully'))
})
.catch((err: any) => {
toast.error(__(err.messages?.[0] || err))
console.error(err)
})
}
const updateTransaction = () => {
props.transactions.setValue
.submit({
...transactionData.value,
})
.then(() => {
toast.success(__('Transaction updated successfully'))
})
.catch((err: any) => {
toast.error(__(err.messages?.[0] || err))
console.error(err)
})
}
@@ -181,4 +225,48 @@ const openDetails = () => {
show.value = false
}
}
const emptyTransactionData = {
payment_received: false,
payment_for_certificate: false,
member: null,
billing_name: null,
source: null,
payment_for_document_type: null,
payment_for_document: null,
member_consent: false,
currency: null,
amount: null,
amount_with_gst: null,
coupon: null,
coupon_code: null,
discount_amount: null,
original_amount: null,
order_id: null,
payment_id: null,
gstin: null,
pan: null,
address: null,
}
watch(
() => props.data,
(newVal) => {
transactionData.value = newVal ? { ...newVal } : emptyTransactionData
},
{ immediate: true }
)
const documentTypeOptions = computed(() => {
return [
{
label: __('Course'),
value: 'LMS Course',
},
{
label: __('Batch'),
value: 'LMS Batch',
},
]
})
</script>

View File

@@ -1,12 +1,20 @@
<template>
<div class="flex min-h-0 flex-col text-base">
<div class="mb-5">
<div class="text-xl font-semibold mb-1 text-ink-gray-9">
{{ __(label) }}
</div>
<div class="text-ink-gray-6 leading-5">
{{ __(description) }}
<div class="flex items-center justify-between mb-5">
<div>
<div class="text-xl font-semibold mb-1 text-ink-gray-9">
{{ __(label) }}
</div>
<div class="text-ink-gray-6 leading-5">
{{ __(description) }}
</div>
</div>
<Button @click="emit('updateStep', 'new', null)">
<template #prefix>
<FeatherIcon name="plus" class="h-4 w-4 stroke-1.5" />
</template>
{{ __('Add Transaction') }}
</Button>
</div>
<div class="flex items-center space-x-5 mb-4">

View File

@@ -1,6 +1,13 @@
<template>
<TransactionDetails
v-if="step == 'new'"
:transactions="transactions"
:data="data"
v-model:show="show"
@updateStep="updateStep"
/>
<TransactionList
v-if="step === 'list'"
v-else-if="step === 'list'"
:label="props.label"
:description="props.description"
:transactions="transactions"
@@ -33,6 +40,8 @@ const updateStep = (newStep: 'list' | 'new' | 'edit', newData: any) => {
step.value = newStep
if (newData) {
data.value = newData
} else {
data.value = null
}
}

View File

@@ -406,9 +406,13 @@ const steps = reactive([
minimize.value = true
let course = await getFirstCourse()
if (course) {
router.push({ name: 'CourseForm', params: { courseName: course } })
router.push({
name: 'CourseDetail',
params: { courseName: course },
hash: '#settings',
})
} else {
router.push({ name: 'CourseForm' })
router.push({ name: 'Courses', query: { newCourse: '1' } })
}
},
},
@@ -423,11 +427,12 @@ const steps = reactive([
let course = await getFirstCourse()
if (course) {
router.push({
name: 'CourseForm',
name: 'CourseDetail',
params: { courseName: course },
hash: '#settings',
})
} else {
router.push({ name: 'Courses' })
router.push({ name: 'Courses', query: { newCourse: '1' } })
}
},
},