chore: merged conflicts
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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()">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}'
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
318
frontend/src/pages/JobApplications.vue
Normal file
318
frontend/src/pages/JobApplications.vue
Normal 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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -142,7 +142,6 @@ const renderEditor = (holder) => {
|
||||
return new EditorJS({
|
||||
holder: holder,
|
||||
tools: getEditorTools(true),
|
||||
autofocus: true,
|
||||
defaultBlock: 'markdown',
|
||||
onChange: async (api, event) => {
|
||||
enablePlyr()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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/')
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user