chore: merged conflicts

This commit is contained in:
Jannat Patel
2025-11-07 15:45:21 +05:30
80 changed files with 6099 additions and 3891 deletions

View File

@@ -62,7 +62,7 @@
<Tooltip :text="__('Enrolled Students')">
<span class="flex items-center">
<Users class="h-4 w-4 stroke-1.5 mr-1" />
{{ course.enrollments }}
{{ formatAmount(course.enrollments) }}
</span>
</Tooltip>
</div>
@@ -116,27 +116,30 @@
<CourseInstructors :instructors="course.instructors" />
</div>
<div v-if="course.paid_course" class="font-semibold">
{{ course.price }}
</div>
<div class="flex items-center space-x-2">
<div v-if="course.paid_course" class="font-semibold">
{{ course.price }}
</div>
<Tooltip
v-if="course.paid_certificate || course.enable_certification"
:text="__('Get Certified')"
>
<GraduationCap class="size-5 stroke-1.5 text-ink-gray-7" />
</Tooltip>
<Tooltip
v-if="course.paid_certificate || course.enable_certification"
:text="__('Get Certified')"
>
<GraduationCap class="size-5 stroke-1.5 text-ink-gray-7" />
</Tooltip>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { Award, BookOpen, GraduationCap, Star, Users } from 'lucide-vue-next'
import UserAvatar from '@/components/UserAvatar.vue'
import { sessionStore } from '@/stores/session'
import { Tooltip } from 'frappe-ui'
import { theme } from '@/utils/theme'
import { formatAmount } from '@/utils'
import CourseInstructors from '@/components/CourseInstructors.vue'
import UserAvatar from '@/components/UserAvatar.vue'
import ProgressBar from '@/components/ProgressBar.vue'
const { user } = sessionStore()

View File

@@ -1,11 +1,12 @@
<template>
<div class="">
<div class="text-ink-gray-7">
<span v-if="instructors?.length == 1">
<router-link
:to="{
name: 'Profile',
params: { username: instructors[0].username },
}"
class="text-ink-gray-7 hover:text-ink-gray-9"
>
{{ instructors[0].full_name }}
</router-link>
@@ -16,6 +17,7 @@
name: 'Profile',
params: { username: instructors[0].username },
}"
class="text-ink-gray-7 hover:text-ink-gray-9"
>
{{ instructors[0].first_name }}
</router-link>
@@ -25,6 +27,7 @@
name: 'Profile',
params: { username: instructors[1].username },
}"
class="text-ink-gray-7 hover:text-ink-gray-9"
>
{{ instructors[1].first_name }}
</router-link>
@@ -35,6 +38,7 @@
name: 'Profile',
params: { username: instructors[0].username },
}"
class="text-ink-gray-7 hover:text-ink-gray-9"
>
{{ instructors[0].first_name }}
</router-link>

View File

@@ -20,10 +20,10 @@
</template>
</Dialog>
<Popover :show="iosInstallMessage" placement="top">
<Popover :show="iosInstallMessage" placement="top-start">
<template #body>
<div
class="fixed bottom-[4rem] left-1/2 -translate-x-1/2 z-20 w-[90%] flex flex-col gap-3 rounded bg-blue-100 py-5 drop-shadow-xl"
class="fixed top-[20rem] translate-x-1/3 z-20 flex flex-col gap-3 rounded bg-surface-white py-5 drop-shadow-xl"
>
<div
class="mb-1 flex flex-row items-center justify-between px-3 text-center"
@@ -41,7 +41,7 @@
</div>
<div class="px-3 text-xs text-gray-800">
<span class="flex flex-col gap-2">
<span>
<span class="leading-5">
{{
__(
'Get the app on your iPhone for easy access & a better experience'

View File

@@ -3,7 +3,7 @@
class="flex flex-col border rounded-md p-3 h-full hover:border-outline-gray-3"
>
<div class="flex space-x-4 mb-4">
<div class="flex flex-col space-y-2 flex-1">
<div class="flex flex-col space-y-2 flex-1 break-all">
<div class="text-lg font-semibold text-ink-gray-9">
{{ job.company_name }}
</div>

View File

@@ -165,6 +165,14 @@ const addAssignments = () => {
})
}
const addProgrammingExercises = () => {
otherLinks.value.push({
label: 'Programming Exercises',
icon: 'Code',
to: 'ProgrammingExercises',
})
}
const addPrograms = async () => {
let canAddProgram = await checkIfCanAddProgram()
if (!canAddProgram) return

View File

@@ -113,6 +113,14 @@ watch(
{ flush: 'post' }
)
watch(show, (newVal) => {
if (newVal && props.assignmentID === 'new') {
assignment.title = ''
assignment.type = ''
assignment.question = ''
}
})
const saveAssignment = () => {
if (props.assignmentID == 'new') {
assignments.value.insert.submit(

View File

@@ -11,7 +11,7 @@
<Avatar :image="student.user_image" size="3xl" />
<div class="space-y-1">
<div class="flex items-center space-x-2">
<div class="text-xl font-semibold">
<div class="text-xl font-semibold text-ink-gray-9">
{{ student.full_name }}
</div>
<Badge
@@ -36,7 +36,9 @@
v-if="Object.keys(student.assessments).length"
class="space-y-2 text-sm"
>
<div class="flex items-center border-b pb-1 font-medium">
<div
class="flex items-center border-b pb-1 font-medium text-ink-gray-9"
>
<span class="flex-1">
{{ __('Assessment') }}
</span>
@@ -86,7 +88,9 @@
v-if="Object.keys(student.courses).length"
class="space-y-2 text-sm"
>
<div class="flex items-center border-b pb-1 font-medium">
<div
class="flex items-center border-b pb-1 font-medium text-ink-gray-9"
>
<span class="flex-1">
{{ __('Courses') }}
</span>

View File

@@ -50,7 +50,7 @@
<FileText class="h-5 w-5 stroke-1.5 text-ink-gray-7" />
</div>
<div class="flex flex-col">
<span>
<span class="text-ink-gray-9">
{{ chapter.scorm_package.file_name }}
</span>
<span class="text-sm text-ink-gray-4 mt-1">

View File

@@ -66,7 +66,11 @@
</template>
{{ __('View Certificate') }}
</Button>
<Button v-else @click="openCallLink(event.venue)" class="w-full">
<Button
v-else-if="userIsEvaluator()"
@click="openCallLink(event.venue)"
class="w-full"
>
<template #prefix>
<Video class="h-4 w-4 stroke-1.5" />
</template>
@@ -83,21 +87,31 @@
class="flex flex-col space-y-4 p-5"
>
<div class="flex items-center justify-between">
<Rating v-model="evaluation.rating" :label="__('Rating')" />
<Rating
v-model="evaluation.rating"
:label="__('Rating')"
:disabled="!userIsEvaluator()"
/>
<FormControl
type="select"
:options="statusOptions"
v-model="evaluation.status"
:label="__('Status')"
class="w-1/2"
:disabled="!userIsEvaluator()"
/>
</div>
<Textarea
v-model="evaluation.summary"
:label="__('Summary')"
:rows="7"
:disabled="!userIsEvaluator()"
/>
<Button variant="solid" @click="saveEvaluation()">
<Button
v-if="userIsEvaluator()"
variant="solid"
@click="saveEvaluation()"
>
{{ __('Save') }}
</Button>
</div>
@@ -106,11 +120,13 @@
type="checkbox"
v-model="certificate.published"
:label="__('Published')"
:disabled="!userIsEvaluator()"
/>
<Link
v-model="certificate.template"
:label="__('Template')"
doctype="Print Format"
:disabled="!userIsEvaluator()"
:filters="{
doc_type: 'LMS Certificate',
}"
@@ -118,14 +134,20 @@
<FormControl
type="date"
v-model="certificate.issue_date"
:disabled="!userIsEvaluator()"
:label="__('Issue Date')"
/>
<FormControl
type="date"
v-model="certificate.expiry_date"
:disabled="!userIsEvaluator()"
:label="__('Expiry Date')"
/>
<Button variant="solid" @click="saveCertificate()">
<Button
v-if="userIsEvaluator()"
variant="solid"
@click="saveCertificate()"
>
{{ __('Save') }}
</Button>
</div>
@@ -163,6 +185,7 @@ import Rating from '@/components/Controls/Rating.vue'
import Link from '@/components/Controls/Link.vue'
const show = defineModel()
const user = inject('$user')
const dayjs = inject('$dayjs')
const tabIndex = ref(0)
const showCertification = ref(false)
@@ -175,9 +198,18 @@ const props = defineProps({
})
const evaluation = reactive({})
const certificate = reactive({})
watch(user, () => {
if (userIsEvaluator()) {
defaultTemplate.reload()
}
})
const userIsEvaluator = () => {
return user.data && user.data.name == props.event.evaluator
}
const defaultTemplate = createResource({
url: 'frappe.client.get_value',
makeParams(values) {
@@ -190,7 +222,6 @@ const defaultTemplate = createResource({
},
}
},
auto: true,
onSuccess(data) {
certificate.template = data.value
},

View File

@@ -18,7 +18,7 @@
>
<template #body-content>
<div class="flex flex-col gap-4">
<p>
<p class="text-ink-gray-9">
{{
__(
'Submit your resume to proceed with your application for this position. Upon submission, it will be shared with the job poster.'
@@ -29,6 +29,7 @@
<FileUploader
:fileTypes="['.pdf']"
:validateFile="validateFile"
:uploadArgs="{ private: 1 }"
@success="
(file) => {
resume = file
@@ -51,7 +52,7 @@
<FileText class="h-5 w-5 stroke-1.5 text-ink-gray-7" />
</div>
<div class="flex flex-col">
<span>
<span class="text-ink-gray-9">
{{ resume.file_name }}
</span>
<span class="text-sm text-ink-gray-4 mt-1">
@@ -95,7 +96,7 @@ const jobApplication = createResource({
doc: {
doctype: 'LMS Job Application',
user: user.data?.name,
resume: resume.value?.file_name,
resume: resume.value?.file_url,
job: props.job,
},
}

View File

@@ -126,7 +126,7 @@ import {
Button,
toast,
} from 'frappe-ui'
import { computed, watch, reactive, ref, inject } from 'vue'
import { watch, reactive, ref, inject } from 'vue'
import Link from '@/components/Controls/Link.vue'
import { useOnboarding } from 'frappe-ui/frappe'
@@ -141,6 +141,7 @@ const existingQuestion = reactive({
question: '',
marks: 1,
})
const question = reactive({
question: '',
type: 'Choices',

View File

@@ -1,13 +1,13 @@
<template>
<div class="border rounded-md w-1/3 mx-auto my-32">
<div class="border-b px-5 py-3 font-medium">
<div class="border-b px-5 py-3 font-medium text-ink-gray-9">
<span
class="inline-flex items-center before:bg-surface-red-5 before:w-2 before:h-2 before:rounded-md before:mr-2"
></span>
{{ __('Not Permitted') }}
</div>
<div v-if="user.data" class="px-5 py-3">
<div>
<div class="text-ink-gray-7">
{{ __('You do not have permission to access this page.') }}
</div>
<router-link
@@ -21,7 +21,7 @@
</router-link>
</div>
<div class="px-5 py-3">
<div>
<div class="text-ink-gray-7">
{{ __('Please login to access this page.') }}
</div>
<Button @click="redirectToLogin()" class="mt-4">

View File

@@ -1,13 +1,13 @@
<template>
<div class="text-base border rounded-md w-1/3 mx-auto my-32">
<div class="border-b px-5 py-3 font-medium">
<div class="border-b px-5 py-3 font-medium text-ink-gray-9">
<span
class="inline-flex items-center before:bg-surface-red-5 before:w-2 before:h-2 before:rounded-md before:mr-2"
></span>
{{ __(title) }}
</div>
<div class="px-5 py-3">
<div class="mb-4 leading-6">
<div class="mb-4 leading-6 text-ink-gray-7">
{{ __(text) }}
</div>
<Button variant="solid" class="w-full" @click="redirect()">

View File

@@ -1,5 +1,5 @@
<template>
<div class="text-lg font-semibold mb-4">
<div class="text-lg font-semibold mb-4 text-ink-gray-9">
{{ __('My Notes') }}
</div>
<TextEditor

View File

@@ -55,8 +55,8 @@
<div v-if="quiz.data.duration" class="flex flex-col space-x-1 my-4">
<div class="mb-2">
<span class=""> {{ __('Time') }}: </span>
<span class="font-semibold">
<span class="text-ink-gray-9"> {{ __('Time') }}: </span>
<span class="font-semibold text-ink-gray-9">
{{ formatTimer(timer) }}
</span>
</div>
@@ -165,14 +165,14 @@
</div>
</div>
<span
class="ml-2"
class="ml-2 text-ink-gray-9"
v-html="questionDetails.data[`option_${index}`]"
>
</span>
</label>
<div
v-if="questionDetails.data[`explanation_${index}`]"
class="mt-2 text-xs"
class="mt-2 text-xs text-ink-gray-7"
v-show="showAnswers.length"
>
{{ questionDetails.data[`explanation_${index}`] }}
@@ -260,7 +260,7 @@
)
}}
</div>
<div v-else>
<div v-else class="text-ink-gray-7">
{{
__(
'You got {0}% correct answers with a score of {1} out of {2}'

View File

@@ -69,9 +69,12 @@ const update = () => {
let imageFields = ['favicon', 'banner_image']
props.fields.forEach((f) => {
if (imageFields.includes(f.name)) {
fieldsToSave[f.name] = f.value ? f.value.file_url : null
fieldsToSave[f.name] =
branding.data[f.name] && branding.data[f.name].file_url
? branding.data[f.name].file_url
: null
} else {
fieldsToSave[f.name] = f.value
fieldsToSave[f.name] = branding.data[f.name]
}
})

View File

@@ -2,7 +2,9 @@
<Dialog v-model="show" :options="{ size: '5xl' }">
<template #body>
<div class="flex h-[calc(100vh_-_8rem)]">
<div class="flex w-52 shrink-0 flex-col bg-surface-gray-2 p-2">
<div
class="flex w-52 shrink-0 flex-col bg-surface-gray-2 p-2 overflow-y-auto"
>
<h1 class="mb-3 px-2 pt-2 text-lg font-semibold text-ink-gray-9">
{{ __('Settings') }}
</h1>

View File

@@ -1,6 +1,6 @@
<template>
<div v-if="heatmap.data">
<div class="text-lg font-semibold mb-2">
<div class="text-lg font-semibold mb-2 text-ink-gray-9">
{{ heatmap.data.total_activities }}
{{
heatmap.data.total_activities > 1 ? __('activities') : __('activity')

View File

@@ -40,7 +40,7 @@
</div>
<div v-if="batch.data.courses.length">
<div class="flex items-center mt-10">
<div class="text-2xl font-semibold">
<div class="text-2xl font-semibold text-ink-gray-9">
{{ __('Courses') }}
</div>
</div>

View File

@@ -20,7 +20,7 @@
<div class="text-ink-gray-5">
{{ __('Payment for ') }} {{ type }}:
</div>
<div class="leading-5">
<div class="leading-5 text-ink-gray-9">
{{ orderSummary.data.title }}
</div>
</div>
@@ -31,7 +31,7 @@
<div class="text-ink-gray-5">
{{ __('Original Amount') }}
</div>
<div class="">
<div class="text-ink-gray-9">
{{ orderSummary.data.original_amount_formatted }}
</div>
</div>
@@ -42,17 +42,17 @@
<div class="text-ink-gray-5">
{{ __('GST Amount') }}
</div>
<div>
<div class="text-ink-gray-9">
{{ orderSummary.data.gst_amount_formatted }}
</div>
</div>
<div
class="flex items-center justify-between border-t border-outline-gray-3 pt-4 mt-2"
>
<div class="text-lg font-semibold">
<div class="text-lg font-semibold text-ink-gray-9">
{{ __('Total') }}
</div>
<div class="text-lg font-semibold">
<div class="text-lg font-semibold text-ink-gray-9">
{{ orderSummary.data.total_amount_formatted }}
</div>
</div>
@@ -60,7 +60,7 @@
<div class="flex-1 lg:mr-10">
<div class="mb-5">
<div class="text-lg font-semibold">
<div class="text-lg font-semibold text-ink-gray-9">
{{ __('Address') }}
</div>
</div>

View File

@@ -144,7 +144,12 @@ watch(
)
watch(course, () => {
if (!isInstructor() && !course.data?.published && !course.data?.upcoming) {
if (
!isInstructor() &&
!user.data?.is_moderator &&
!course.data?.published &&
!course.data?.upcoming
) {
router.push({
name: 'Courses',
})

View File

@@ -21,7 +21,7 @@
</header>
<div class="mt-5 mb-5">
<div class="px-5 md:px-10 pb-5 mb-5 space-y-5 border-b">
<div class="text-lg font-semibold mb-4">
<div class="text-lg font-semibold mb-4 text-ink-gray-9">
{{ __('Details') }}
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
@@ -138,7 +138,7 @@
</div>
<div class="px-5 md:px-10 pb-5 mb-5 space-y-5 border-b">
<div class="text-lg font-semibold">
<div class="text-lg font-semibold text-ink-gray-9">
{{ __('Settings') }}
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
@@ -178,7 +178,7 @@
</div>
<div class="px-5 md:px-10 pb-5 mb-5 space-y-5 border-b">
<div class="text-lg font-semibold">
<div class="text-lg font-semibold text-ink-gray-9">
{{ __('About the Course') }}
</div>
<FormControl
@@ -234,7 +234,7 @@
</div>
<div class="px-5 md:px-10 pb-5 space-y-5 border-b">
<div class="text-lg font-semibold mt-5">
<div class="text-lg font-semibold mt-5 text-ink-gray-9">
{{ __('Pricing and Certification') }}
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-5">
@@ -260,12 +260,14 @@
v-if="course.paid_course || course.paid_certificate"
v-model="course.course_price"
:label="__('Amount')"
:required="course.paid_course || course.paid_certificate"
/>
<Link
v-if="course.paid_certificate"
doctype="Course Evaluator"
v-model="course.evaluator"
:label="__('Evaluator')"
:required="course.paid_certificate"
:onCreate="
(value, close) => openSettings('Evaluators', close)
"
@@ -278,11 +280,13 @@
v-model="course.currency"
:filters="{ enabled: 1 }"
:label="__('Currency')"
:required="course.paid_course || course.paid_certificate"
/>
<FormControl
v-if="course.paid_certificate"
v-model="course.timezone"
:label="__('Timezone')"
:required="course.paid_certificate"
:placeholder="__('e.g. IST, UTC, GMT...')"
/>
</div>
@@ -290,7 +294,7 @@
</div>
<div class="px-5 md:px-10 pb-5 space-y-5">
<div class="text-lg font-semibold mt-5">
<div class="text-lg font-semibold mt-5 text-ink-gray-9">
{{ __('Meta Tags') }}
</div>
<div class="space-y-5">
@@ -325,7 +329,6 @@
<script setup>
import {
Breadcrumbs,
call,
TextEditor,
Button,
createResource,

View File

@@ -2,7 +2,7 @@
<div>
<div v-if="createdCourses.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">
{{ __('Courses Created') }}
</span>
<router-link

View File

@@ -5,27 +5,28 @@
<Breadcrumbs :items="[{ label: __('Home'), route: { name: 'Home' } }]" />
</header> -->
<div class="w-full px-5 pt-5 pb-10">
<div class="flex items-center justify-between">
<div class="space-y-2">
<div class="space-y-2">
<div class="flex items-center justify-between">
<div class="text-xl font-bold text-ink-gray-9">
{{ __('Hey') }}, {{ user.data?.full_name }} 👋
</div>
<div class="text-lg text-ink-gray-6">
{{ subtitle }}
<div>
<TabButtons v-if="isAdmin" v-model="currentTab" :buttons="tabs" />
<div
v-else
@click="showStreakModal = true"
class="bg-surface-amber-2 px-2 py-1 rounded-md cursor-pointer"
>
<span> 🔥 </span>
<span class="text-ink-gray-9">
{{ streakInfo.data?.current_streak }}
</span>
</div>
</div>
</div>
<div>
<TabButtons v-if="isAdmin" v-model="currentTab" :buttons="tabs" />
<div
v-else
@click="showStreakModal = true"
class="bg-surface-amber-2 px-2 py-1 rounded-md cursor-pointer"
>
<span> 🔥 </span>
<span>
{{ streakInfo.data?.current_streak }}
</span>
</div>
<div class="text-lg text-ink-gray-6 leading-6">
{{ subtitle }}
</div>
</div>

View File

@@ -20,7 +20,7 @@
}}
{{ __(' you are on a') }}
</div>
<div class="font-semibold text-xl">
<div class="font-semibold text-xl text-ink-gray-9">
{{ streakInfo.data?.current_streak }} {{ __('day streak') }}
</div>
</div>
@@ -33,7 +33,7 @@
<div class="text-ink-gray-6">
{{ __('Current Streak') }}
</div>
<div class="font-semibold text-lg">
<div class="font-semibold text-lg text-ink-gray-9">
{{ streakInfo.data?.current_streak }} {{ __('days') }}
</div>
</div>
@@ -41,7 +41,7 @@
<div class="text-ink-gray-6">
{{ __('Longest Streak') }}
</div>
<div class="font-semibold text-lg">
<div class="font-semibold text-lg text-ink-gray-9">
{{ streakInfo.data?.longest_streak }} {{ __('days') }}
</div>
</div>

View File

@@ -0,0 +1,318 @@
<template>
<div class="">
<header
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
>
<Breadcrumbs
class="h-7"
:items="[
{ label: __('Jobs'), route: { name: 'Jobs' } },
{
label: applications.data?.[0]?.job_title,
route: { name: 'JobDetail', params: { job: props.job } },
},
{ label: __('Applications') },
]"
/>
</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>
</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"
>
<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"
>
<img
v-if="row.user_image"
:src="row.user_image"
:alt="row.full_name"
class="w-8 h-8 rounded-full object-cover"
/>
<div
v-else
class="w-8 h-8 rounded-full bg-surface-gray-3 flex items-center justify-center"
>
<FeatherIcon name="user" class="w-4 h-4 text-ink-gray-6" />
</div>
<span class="text-sm font-medium">{{ item }}</span>
</div>
<div
v-else-if="column.key === 'actions'"
class="flex justify-center"
>
<Dropdown :options="getActionOptions(row)">
<Button variant="ghost" size="sm">
<FeatherIcon name="more-horizontal" class="w-4 h-4" />
</Button>
</Dropdown>
</div>
<div v-else class="text-sm">
{{ item }}
</div>
</ListRowItem>
</ListRow>
</ListRows>
</ListView>
<EmptyState v-else type="Applications" />
</div>
<Dialog
v-model="showEmailModal"
:options="{
title: __('Send Email to {0}').format(selectedApplicant?.full_name),
size: 'lg',
actions: [
{
label: __('Send'),
variant: 'solid',
onClick: (close) => sendEmail(close),
},
],
}"
>
<template #body-content>
<div class="space-y-4">
<FormControl
v-model="emailForm.subject"
:label="__('Subject')"
:placeholder="__('Enter email subject')"
required
/>
<FormControl
v-model="emailForm.replyTo"
:label="__('Reply To')"
:placeholder="__('Enter reply to email')"
/>
<div>
<div class="text-sm text-ink-gray-5 mb-1">
{{ __('Message') }}
</div>
<TextEditor
:content="emailForm.message"
@change="(val) => (emailForm.message = val)"
:editable="true"
:fixedMenu="true"
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem]"
/>
</div>
</div>
</template>
</Dialog>
</div>
</template>
<script setup>
import {
Button,
Breadcrumbs,
Dialog,
Dropdown,
FeatherIcon,
FormControl,
TextEditor,
ListView,
ListHeader,
ListHeaderItem,
ListRows,
ListRow,
ListRowItem,
createResource,
createListResource,
usePageMeta,
toast,
} from 'frappe-ui'
import { inject, ref, computed, reactive } from 'vue'
import { sessionStore } from '../stores/session'
import EmptyState from '@/components/EmptyState.vue'
const dayjs = inject('$dayjs')
const { brand } = sessionStore()
const showEmailModal = ref(false)
const selectedApplicant = ref(null)
const emailForm = reactive({
subject: '',
message: '',
replyTo: '',
})
const props = defineProps({
job: {
type: String,
required: true,
},
})
const applications = createListResource({
doctype: 'LMS Job Application',
fields: [
'name',
'user.user_image as user_image',
'user.full_name as full_name',
'user.email as email',
'creation',
'resume',
'job.job_title as job_title',
],
filters: {
job: props.job,
},
auto: true,
})
const emailResource = createResource({
url: 'frappe.core.doctype.communication.email.make',
makeParams(values) {
return {
recipients: selectedApplicant.value.email,
cc: emailForm.replyTo,
subject: emailForm.subject,
content: emailForm.message,
doctype: 'LMS Job Application',
name: selectedApplicant.value.name,
send_email: 1,
now: true,
}
},
})
const openEmailModal = (applicant) => {
selectedApplicant.value = applicant
emailForm.subject = `Job Application for ${applications.data?.[0]?.job_title} - ${applicant.full_name}`
emailForm.replyTo = ''
emailForm.message = ''
showEmailModal.value = true
}
const sendEmail = (close) => {
emailResource.submit(
{},
{
validate() {
if (!emailForm.subject) {
return __('Subject is required')
}
if (!emailForm.message) {
return __('Message is required')
}
},
onSuccess: () => {
toast.success(__('Email sent successfully'))
close()
},
onError: (err) => {
toast.error(err.messages?.[0] || err)
},
}
)
}
const downloadResume = (resumeUrl) => {
window.open(resumeUrl, '_blank')
}
const getActionOptions = (row) => {
const options = []
if (row.resume) {
options.push({
label: __('View Resume'),
icon: 'download',
onClick: () => downloadResume(row.resume),
})
}
options.push({
label: __('Send Email'),
icon: 'mail',
onClick: () => openEmailModal(row),
})
return options
}
const applicationColumns = computed(() => {
return [
{
label: __('Full Name'),
key: 'full_name',
width: 2,
icon: 'user',
},
{
label: __('Email'),
key: 'email',
width: 2,
icon: 'at-sign',
},
{
label: __('Applied On'),
key: 'applied_date',
width: 1,
icon: 'calendar',
},
{
label: '',
key: 'actions',
width: 1,
},
]
})
const applicantRows = computed(() => {
if (!applications.data) return []
return applications.data.map((application) => ({
...application,
full_name: application.full_name,
applied_date: dayjs(application.creation).format('MMM DD, YYYY'),
}))
})
usePageMeta(() => {
return {
title: `Applications - ${applications.data?.[0]?.job_title}`,
icon: brand.favicon,
}
})
</script>

View File

@@ -20,6 +20,17 @@
v-if="user.data?.name && !readOnlyMode"
class="flex items-center space-x-2"
>
<router-link
v-if="canManageJob && applicationCount.data > 0"
:to="{
name: 'JobApplications',
params: { job: job.data?.name },
}"
>
<Button variant="subtle">
{{ __('View Applications') }}
</Button>
</router-link>
<router-link
v-if="user.data.name == job.data?.owner"
:to="{
@@ -146,7 +157,7 @@ import {
createResource,
usePageMeta,
} from 'frappe-ui'
import { inject, ref } from 'vue'
import { inject, ref, computed } from 'vue'
import { sessionStore } from '../stores/session'
import JobApplicationModal from '@/components/Modals/JobApplicationModal.vue'
import {
@@ -159,6 +170,7 @@ import {
FileText,
ClipboardType,
BriefcaseBusiness,
Users,
} from 'lucide-vue-next'
const user = inject('$user')
@@ -226,6 +238,13 @@ const redirectToWebsite = (url) => {
window.open(url, '_blank')
}
const canManageJob = computed(() => {
if (!user.data?.name || !job.data) return false
return (
user.data.name === job.data.owner || user.data.roles?.includes('Moderator')
)
})
usePageMeta(() => {
return {
title: job.data?.job_title,

View File

@@ -10,7 +10,7 @@
</header>
<div class="py-5">
<div class="container border-b mb-4 pb-5">
<div class="text-lg font-semibold mb-4">
<div class="text-lg font-semibold mb-4 text-ink-gray-9">
{{ __('Job Details') }}
</div>
<div class="grid grid-cols-2 gap-5">
@@ -59,7 +59,7 @@
</div>
</div>
<div class="container border-b mb-4 pb-5">
<div class="text-lg font-semibold mb-4">
<div class="text-lg font-semibold mb-4 text-ink-gray-9">
{{ __('Company Details') }}
</div>
<div class="grid grid-cols-2 gap-5">
@@ -158,7 +158,7 @@ import { computed, onMounted, reactive, inject } from 'vue'
import { FileText, X } from 'lucide-vue-next'
import { sessionStore } from '@/stores/session'
import { useRouter } from 'vue-router'
import { getFileSize, validateFile } from '@/utils'
import { escapeHTML, getFileSize, validateFile } from '@/utils'
const user = inject('$user')
const router = useRouter()
@@ -248,6 +248,7 @@ onMounted(() => {
})
const saveJob = () => {
validateJobFields()
if (jobDetail.data) {
editJobDetails()
} else {
@@ -293,6 +294,14 @@ const editJobDetails = () => {
)
}
const validateJobFields = () => {
Object.keys(job).forEach((key) => {
if (key != 'description' && typeof job[key] === 'string') {
job[key] = escapeHTML(job[key])
}
})
}
const saveImage = (file) => {
job.image = file
}

View File

@@ -142,7 +142,6 @@ const renderEditor = (holder) => {
return new EditorJS({
holder: holder,
tools: getEditorTools(true),
autofocus: true,
defaultBlock: 'markdown',
onChange: async (api, event) => {
enablePlyr()

View File

@@ -124,19 +124,13 @@ const props = defineProps({
onMounted(() => {
if ($user.data) profile.reload()
setActiveTab()
})
const profile = createResource({
url: 'frappe.client.get',
makeParams(values) {
return {
doctype: 'User',
filters: {
username: props.username,
},
}
url: 'lms.lms.api.get_profile_details',
params: {
username: props.username,
},
})
@@ -194,17 +188,18 @@ const isSessionUser = () => {
return $user.data?.email === profile.data?.email
}
const hasHigherAccess = () => {
return $user.data?.is_evaluator || $user.data?.is_moderator
}
const getTabButtons = () => {
let buttons = [{ label: 'About' }, { label: 'Certificates' }]
if ($user.data?.is_moderator) buttons.push({ label: 'Roles' })
if (
isSessionUser() &&
($user.data?.is_evaluator || $user.data?.is_moderator)
) {
if (hasHigherAccess()) {
buttons.push({ label: 'Slots' })
buttons.push({ label: 'Schedule' })
}
return buttons
}

View File

@@ -36,7 +36,7 @@
</Calendar>
</div>
</div>
<Event v-model="showEvent" :event="currentEvent" />
<Event v-if="showEvent" v-model="showEvent" :event="currentEvent" />
</template>
<script setup>
import { Calendar, createListResource, Button } from 'frappe-ui'
@@ -57,7 +57,7 @@ const props = defineProps({
const evaluations = createListResource({
doctype: 'LMS Certificate Request',
filters: {
evaluator: user.data?.name,
evaluator: props.profile.data?.name,
status: ['!=', 'Cancelled'],
},
fields: [

View File

@@ -43,18 +43,22 @@
:options="days"
v-model="slot.day"
@focusout.stop="update(slot.name, 'day', slot.day)"
:disabled="!isSessionUser()"
/>
<FormControl
type="time"
v-model="slot.start_time"
@focusout.stop="update(slot.name, 'start_time', slot.start_time)"
:disabled="!isSessionUser()"
/>
<FormControl
type="time"
v-model="slot.end_time"
@focusout.stop="update(slot.name, 'end_time', slot.end_time)"
:disabled="!isSessionUser()"
/>
<X
v-if="isSessionUser()"
@click="deleteRow(slot.name)"
class="w-6 h-auto stroke-1.5 text-red-900 rounded-md cursor-pointer p-1 bg-surface-red-2 hidden group-hover:block"
/>
@@ -69,20 +73,23 @@
:options="days"
v-model="newSlot.day"
@focusout.stop="add()"
:disabled="!isSessionUser()"
/>
<FormControl
type="time"
v-model="newSlot.start_time"
@focusout.stop="add()"
:disabled="!isSessionUser()"
/>
<FormControl
type="time"
v-model="newSlot.end_time"
@focusout.stop="add()"
:disabled="!isSessionUser()"
/>
</div>
<Button @click="showSlotsTemplate = 1">
<Button v-if="isSessionUser()" @click="showSlotsTemplate = 1">
<template #prefix>
<Plus class="w-4 h-4 stroke-1.5 text-ink-gray-7" />
</template>
@@ -98,6 +105,7 @@
type="date"
:label="__('From')"
v-model="from"
:disabled="!isSessionUser()"
@blur="
() => {
updateUnavailability.submit({
@@ -111,6 +119,7 @@
type="date"
:label="__('To')"
v-model="to"
:disabled="!isSessionUser()"
@blur="
() => {
updateUnavailability.submit({
@@ -122,7 +131,7 @@
/>
</div>
</div>
<div>
<div v-if="isSessionUser()">
<h2 class="mb-4 text-lg font-semibold text-ink-gray-9">
{{ __('My calendar') }}
</h2>
@@ -157,11 +166,19 @@ const props = defineProps({
})
onMounted(() => {
if (user.data?.name !== props.profile.data?.name) {
if (user.data?.name !== props.profile.data?.name && !hasHigherAccess()) {
window.location.href = `/user/${props.profile.data?.username}`
}
})
const hasHigherAccess = () => {
return user.data?.is_evaluator || user.data?.is_moderator
}
const isSessionUser = () => {
return user.data?.email === props.profile.data?.email
}
const showSlotsTemplate = ref(0)
const from = ref(null)
const to = ref(null)

View File

@@ -21,7 +21,7 @@
</div>
<div class="grid grid-cols-2 h-[calc(100vh_-_3rem)]">
<div class="border-r py-5 px-8 h-full">
<div class="font-semibold mb-2">
<div class="font-semibold mb-2 text-ink-gray-9">
{{ __('Problem Statement') }}
</div>
<div
@@ -31,7 +31,7 @@
</div>
<div>
<div class="flex items-center justify-between p-2 bg-surface-gray-2">
<div class="font-semibold">
<div class="font-semibold text-ink-gray-9">
{{ exercise.doc?.language }}
</div>
<div class="space-x-2">
@@ -89,7 +89,9 @@
class="py-3"
>
<div class="flex items-center mb-3">
<span class=""> {{ __('Test {0}').format(index + 1) }} - </span>
<span class="text-ink-gray-9">
{{ __('Test {0}').format(index + 1) }} -
</span>
<span
class="font-semibold ml-2 mr-1"
:class="
@@ -112,13 +114,13 @@
<div class="text-xs text-ink-gray-7">
{{ __('Input') }}
</div>
<div>{{ testCase.input }}</div>
<div class="text-ink-gray-9">{{ testCase.input }}</div>
</div>
<div class="space-y-2">
<div class="text-xs text-ink-gray-7">
{{ __('Your Output') }}
</div>
<div>
<div class="text-ink-gray-9">
{{ testCase.output }}
</div>
</div>
@@ -126,7 +128,9 @@
<div class="text-xs text-ink-gray-7">
{{ __('Expected Output') }}
</div>
<div>{{ testCase.expected_output }}</div>
<div class="text-ink-gray-9">
{{ testCase.expected_output }}
</div>
</div>
</div>
</div>
@@ -153,6 +157,7 @@ import { Play, X, Check, Settings } from 'lucide-vue-next'
import { sessionStore } from '@/stores/session'
import { useRouter } from 'vue-router'
import { openSettings } from '@/utils'
import { useSettings } from '@/stores/settings'
const user = inject<any>('$user')
const code = ref<string | null>('')
@@ -162,7 +167,8 @@ const errorMessage = ref<string | null>(null)
const testCaseSection = ref<HTMLElement | null>(null)
const testCases = ref<TestCase[]>([])
const boilerplate = ref<string>('')
const { brand, livecodeURL } = sessionStore()
const { brand } = sessionStore()
const { livecodeURL } = useSettings()
const router = useRouter()
const fromLesson = ref(false)
const falconURL = ref<string>('https://falcon.frappe.io/')

View File

@@ -25,17 +25,17 @@
@click="openForm(program.name)"
class="border rounded-md p-3 hover:border-outline-gray-3 cursor-pointer space-y-2"
>
<div class="text-lg font-semibold">
<div class="text-lg font-semibold text-ink-gray-9">
{{ program.name }}
</div>
<div class="flex items-center space-x-1">
<div class="flex items-center space-x-1 text-ink-gray-7">
<BookOpen class="h-4 w-4 stroke-1.5 mr-1" />
<span>
{{ program.course_count }}
{{ program.course_count == 1 ? __('Course') : __('Courses') }}
</span>
</div>
<div class="flex items-center space-x-1">
<div class="flex items-center space-x-1 text-ink-gray-7">
<User class="h-4 w-4 stroke-1.5 mr-1" />
<span>
{{ program.member_count || 0 }}

View File

@@ -3,7 +3,7 @@
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
>
<Breadcrumbs :items="breadcrumbs" />
<div v-if="!readOnlyMode" class="space-x-2">
<div v-if="!readOnlyMode" class="flex items-center space-x-2">
<Badge v-if="quizDetails.isDirty" theme="orange">
{{ __('Not Saved') }}
</Badge>
@@ -254,11 +254,7 @@ const props = defineProps({
const questions = ref([])
onMounted(() => {
if (
props.quizID == 'new' &&
!user.data?.is_moderator &&
!user.data?.is_instructor
) {
if (!user.data?.is_moderator && !user.data?.is_instructor) {
router.push({ name: 'Courses' })
}
if (props.quizID !== 'new') {

View File

@@ -56,8 +56,8 @@
<span class="font-semibold"> {{ __('Question') }}: </span>
<span class="leading-5" v-html="row.question"> </span>
</div>
<div class="">
<span class="font-semibold"> {{ __('Answer') }} </span>
<div class="text-ink-gray-9">
<span class="font-semibold"> {{ __('Answer') }}: </span>
<span class="leading-5" v-html="row.answer"></span>
</div>
<div class="grid grid-cols-2 gap-5">

View File

@@ -5,7 +5,7 @@
<Breadcrumbs :items="breadcrumbs" />
</header>
<div v-if="submissions.data?.length" class="md:w-3/4 md:mx-auto py-5 mx-5">
<div class="text-xl font-semibold mb-5">
<div class="text-xl font-semibold mb-5 text-ink-gray-9">
{{ submissions.data[0].quiz_title }}
</div>
<ListView
@@ -40,7 +40,7 @@
</Button>
</div>
</div>
<EmptyState v-else />
<EmptyState v-else type="Quiz Submissions" />
</template>
<script setup>
import {

View File

@@ -112,6 +112,12 @@ const routes = [
component: () => import('@/pages/JobDetail.vue'),
props: true,
},
{
path: '/job-openings/:job/applications',
name: 'JobApplications',
component: () => import('@/pages/JobApplications.vue'),
props: true,
},
{
path: '/courses/:courseName/edit',
name: 'CourseForm',

View File

@@ -54,16 +54,6 @@ export const sessionStore = defineStore('lms-session', () => {
},
})
const livecodeURL = createResource({
url: 'frappe.client.get_single_value',
params: {
doctype: 'LMS Settings',
field: 'livecode_url',
},
cache: 'livecodeURL',
auto: user.value ? true : false,
})
return {
user,
isLoggedIn,
@@ -71,6 +61,5 @@ export const sessionStore = defineStore('lms-session', () => {
logout,
brand,
branding,
livecodeURL,
}
})

View File

@@ -41,6 +41,13 @@ export const useSettings = defineStore('settings', () => {
auto: false,
})
const livecodeURL = createResource({
url: 'lms.lms.api.get_lms_setting',
params: { field: 'livecode_url' },
auto: true,
cache: ['livecodeURL'],
})
return {
isSettingsOpen,
activeTab,
@@ -49,5 +56,6 @@ export const useSettings = defineStore('settings', () => {
contactUsEmail,
contactUsURL,
sidebarSettings,
livecodeURL,
}
})