feat: batch announcements
This commit is contained in:
59
frontend/src/components/Annoucements.vue
Normal file
59
frontend/src/components/Annoucements.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div v-if="communications.data">
|
||||
<div v-for="comm in communications.data">
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center">
|
||||
<Avatar :label="comm.sender_full_name" size="lg" />
|
||||
<div class="ml-2">
|
||||
{{ comm.sender_full_name }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
{{ timeAgo(comm.communication_date) }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="prose prose-sm bg-gray-50 !min-w-full px-4 py-2 rounded-md"
|
||||
v-html="comm.content"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { createListResource, Avatar } from 'frappe-ui'
|
||||
import { timeAgo } from '@/utils'
|
||||
|
||||
const props = defineProps({
|
||||
batch: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const communications = createListResource({
|
||||
doctype: 'Communication',
|
||||
fields: [
|
||||
'subject',
|
||||
'content',
|
||||
'recipients',
|
||||
'cc',
|
||||
'communication_date',
|
||||
'sender',
|
||||
'sender_full_name',
|
||||
],
|
||||
filters: {
|
||||
reference_doctype: 'LMS Batch',
|
||||
reference_name: props.batch,
|
||||
},
|
||||
orderBy: 'communication_date desc',
|
||||
auto: true,
|
||||
cache: ['batch', props.batch],
|
||||
})
|
||||
</script>
|
||||
<style>
|
||||
.prose-sm p {
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -39,31 +39,11 @@ import UserDropdown from '@/components/UserDropdown.vue'
|
||||
import CollapseSidebar from '@/components/Icons/CollapseSidebar.vue'
|
||||
import SidebarLink from '@/components/SidebarLink.vue'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { BookOpen, Users, TrendingUp, Briefcase } from 'lucide-vue-next'
|
||||
import { ref, watch } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { getSidebarLinks } from '../utils'
|
||||
|
||||
const links = getSidebarLinks()
|
||||
|
||||
const links = [
|
||||
{
|
||||
label: 'Courses',
|
||||
icon: BookOpen,
|
||||
to: 'Courses',
|
||||
},
|
||||
{
|
||||
label: 'Batches',
|
||||
icon: Users,
|
||||
to: 'Batches',
|
||||
},
|
||||
{
|
||||
label: 'Statistics',
|
||||
icon: TrendingUp,
|
||||
to: 'Statistics',
|
||||
},
|
||||
{
|
||||
label: 'Jobs',
|
||||
icon: Briefcase,
|
||||
to: 'Jobs',
|
||||
},
|
||||
]
|
||||
const getSidebarFromStorage = () => {
|
||||
return useStorage('sidebar_is_collapsed', false)
|
||||
}
|
||||
|
||||
@@ -84,9 +84,9 @@ import {
|
||||
Avatar,
|
||||
Button,
|
||||
} from 'frappe-ui'
|
||||
import { Settings, Trash2, Plus } from 'lucide-vue-next'
|
||||
import { Trash2, Plus } from 'lucide-vue-next'
|
||||
import { ref } from 'vue'
|
||||
import StudentModal from '@/components/StudentModal.vue'
|
||||
import StudentModal from '@/components/Modals/StudentModal.vue'
|
||||
|
||||
const showStudentModal = ref(false)
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ import { Star } from 'lucide-vue-next'
|
||||
import { createResource, Button } from 'frappe-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import ReviewModal from '@/components/ReviewModal.vue'
|
||||
import ReviewModal from '@/components/Modals/ReviewModal.vue'
|
||||
|
||||
const props = defineProps({
|
||||
courseName: {
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
import { createListResource, Button } from 'frappe-ui'
|
||||
import { Plus, Clock, Calendar, Video, Monitor } from 'lucide-vue-next'
|
||||
import { inject } from 'vue'
|
||||
import LiveClassModal from '@/components/LiveClassModal.vue'
|
||||
import LiveClassModal from '@/components/Modals/LiveClassModal.vue'
|
||||
import { ref } from 'vue'
|
||||
import { formatTime } from '@/utils/'
|
||||
|
||||
|
||||
40
frontend/src/components/MobileLayout.vue
Normal file
40
frontend/src/components/MobileLayout.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="flex h-full flex-col">
|
||||
<div class="h-full overflow-auto" id="scrollContainer">
|
||||
<slot />
|
||||
</div>
|
||||
<div
|
||||
class="grid grid-cols-5 border-t border-gray-300 standalone:pb-4"
|
||||
:style="{ gridTemplateColumns: `repeat(${tabs.length}, minmax(0, 1fr))` }"
|
||||
>
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.label"
|
||||
class="flex flex-col items-center justify-center py-3 transition active:scale-95"
|
||||
@click="handleClick(tab)"
|
||||
>
|
||||
<component
|
||||
:is="tab.icon"
|
||||
class="h-6 w-6"
|
||||
:class="[isActive(tab) ? 'text-gray-900' : 'text-gray-600']"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { scrollTo } from '@/utils/scrollContainer'
|
||||
import { getSidebarLinks } from '../utils'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const tabs = getSidebarLinks()
|
||||
|
||||
let isActive = computed((tab) => {
|
||||
return router.currentRoute.value.name === tab.to
|
||||
})
|
||||
|
||||
const handleClick = (tab) => {
|
||||
router.push({ name: tab.to })
|
||||
}
|
||||
</script>
|
||||
117
frontend/src/components/Modals/AnnouncementModal.vue
Normal file
117
frontend/src/components/Modals/AnnouncementModal.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-model="show"
|
||||
:options="{
|
||||
title: __('Make an Announcement'),
|
||||
size: 'xl',
|
||||
actions: [
|
||||
{
|
||||
label: 'Submit',
|
||||
variant: 'solid',
|
||||
onClick: (close) => makeAnnouncement(close),
|
||||
},
|
||||
],
|
||||
}"
|
||||
>
|
||||
<template #body-content>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="">
|
||||
<div class="mb-1.5 text-sm text-gray-600">
|
||||
{{ __('Subject') }}
|
||||
</div>
|
||||
<Input type="text" v-model="announcement.subject" />
|
||||
</div>
|
||||
<div class="">
|
||||
<div class="mb-1.5 text-sm text-gray-600">
|
||||
{{ __('Reply To') }}
|
||||
</div>
|
||||
<Input type="text" v-model="announcement.replyTo" />
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="mb-1.5 text-sm text-gray-600">
|
||||
{{ __('Announcement') }}
|
||||
</div>
|
||||
<TextEditor
|
||||
:bubbleMenu="true"
|
||||
@change="(val) => (announcement.announcement = val)"
|
||||
editorClass="prose-sm py-2 px-2 min-h-[200px] border-gray-300 hover:border-gray-400 rounded-md bg-gray-200"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Dialog, Input, TextEditor, createResource } from 'frappe-ui'
|
||||
import { reactive } from 'vue'
|
||||
import { createToast } from '@/utils/'
|
||||
|
||||
const show = defineModel()
|
||||
|
||||
const props = defineProps({
|
||||
batch: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
students: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const announcement = reactive({
|
||||
subject: '',
|
||||
replyTo: '',
|
||||
announcement: '',
|
||||
})
|
||||
|
||||
const announcementResource = createResource({
|
||||
url: 'frappe.core.doctype.communication.email.make',
|
||||
makeParams(values) {
|
||||
return {
|
||||
recipients: props.students.join(', '),
|
||||
cc: announcement.replyTo,
|
||||
subject: announcement.subject,
|
||||
content: announcement.announcement,
|
||||
doctype: 'LMS Batch',
|
||||
name: props.batch,
|
||||
send_email: 1,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const makeAnnouncement = (close) => {
|
||||
announcementResource.submit(
|
||||
{},
|
||||
{
|
||||
validate() {
|
||||
if (!props.students.length) {
|
||||
return 'No students in this batch'
|
||||
}
|
||||
if (!announcement.subject) {
|
||||
return 'Subject is required'
|
||||
}
|
||||
},
|
||||
onSuccess() {
|
||||
close()
|
||||
createToast({
|
||||
title: 'Success',
|
||||
text: 'Announcement has been sent successfully',
|
||||
icon: 'Check',
|
||||
iconClasses: 'bg-green-600 text-white rounded-md p-px',
|
||||
})
|
||||
},
|
||||
onError(err) {
|
||||
createToast({
|
||||
title: 'Error',
|
||||
text: err.messages?.[0] || err,
|
||||
icon: 'x',
|
||||
iconClasses: 'bg-red-600 text-white rounded-md p-px',
|
||||
position: 'top-center',
|
||||
timeout: 10,
|
||||
})
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
</script>
|
||||
@@ -130,7 +130,6 @@ let liveClass = reactive({
|
||||
batch: props.batch,
|
||||
host: user.data.name,
|
||||
})
|
||||
console.log(liveClass)
|
||||
|
||||
const getTimezoneOptions = () => {
|
||||
return getTimezones().map((timezone) => {
|
||||
@@ -52,7 +52,7 @@ import { Calendar, Clock, UserCog2 } from 'lucide-vue-next'
|
||||
import { inject, ref } from 'vue'
|
||||
import { formatTime } from '../utils'
|
||||
import { Button, createResource } from 'frappe-ui'
|
||||
import EvaluationModal from '@/components/EvaluationModal.vue'
|
||||
import EvaluationModal from '@/components/Modals/EvaluationModal.vue'
|
||||
|
||||
const dayjs = inject('$dayjs')
|
||||
const user = inject('$user')
|
||||
|
||||
Reference in New Issue
Block a user