feat: batch billing

This commit is contained in:
Jannat Patel
2024-01-23 15:33:31 +05:30
parent b07940951c
commit 9671c4d63f
154 changed files with 112825 additions and 51286 deletions

View File

@@ -14,11 +14,11 @@
"lucide-vue-next": "^0.309.0",
"markdown-it": "^14.0.0",
"pinia": "^2.0.33",
"tailwindcss": "^3.2.7",
"socket.io-client": "^4.7.2",
"tailwindcss": "^3.2.7",
"vue": "^3.2.25",
"vue-router": "^4.0.12",
"vue-chartjs": "^5.0.0"
"vue-chartjs": "^5.0.0",
"vue-router": "^4.0.12"
},
"devDependencies": {
"@vitejs/plugin-vue": "^2.0.0",

View File

@@ -1,5 +1,5 @@
<template>
<div v-if="communications.data">
<div v-if="communications.data?.length">
<div v-for="comm in communications.data">
<div class="mb-8">
<div class="flex items-center justify-between mb-2">
@@ -20,6 +20,9 @@
</div>
</div>
</div>
<div v-else class="text-sm italic text-gray-600">
{{ __('No announcements') }}
</div>
</template>
<script setup>
import { createListResource, Avatar } from 'frappe-ui'

View File

@@ -50,15 +50,23 @@
</span>
</Button>
</router-link>
<Button
<router-link
:to="{
name: 'Billing',
params: {
type: 'batch',
name: batch.data.name,
},
}"
v-else-if="batch.data.paid_batch"
class="w-full mt-4"
variant="solid"
>
<span>
{{ __('Register Now') }}
</span>
</Button>
<Button class="w-full mt-4" variant="solid">
<span>
{{ __('Register Now') }}
</span>
</Button>
</router-link>
<Button v-if="user?.data?.is_moderator" class="w-full mt-2">
<span>
{{ __('Edit') }}

View File

@@ -77,6 +77,7 @@ const valuePropPassed = computed(() => 'value' in attrs)
const value = computed({
get: () => (valuePropPassed.value ? attrs.value : props.modelValue),
set: (val) => {
console.log(val?.value, valuePropPassed.value)
return (
val?.value &&
emit(valuePropPassed.value ? 'change' : 'update:modelValue', val?.value)

View File

@@ -27,6 +27,22 @@
</span>
</Button>
</router-link>
<router-link
v-else-if="course.data.paid_course"
:to="{
name: 'Billing',
params: {
type: 'course',
name: course.data.name,
},
}"
>
<Button variant="solid" class="w-full mb-3">
<span>
{{ __('Buy this course') }}
</span>
</Button>
</router-link>
<Button
v-else
@click="enrollStudent()"

View File

@@ -0,0 +1,44 @@
<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">
<span
class="inline-flex items-center before:bg-red-600 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">
{{ __(text) }}
</div>
<Button variant="solid" class="w-full" @click="redirect()">
{{ __(buttonLabel) }}
</Button>
</div>
</div>
</template>
<script setup>
import { Button } from 'frappe-ui'
const props = defineProps({
title: {
type: String,
default: 'Not Permitted',
},
text: {
type: String,
default: 'You are not permitted to access this page.',
},
buttonLabel: {
type: String,
default: 'Login',
},
buttonLink: {
type: String,
default: '/login',
},
})
const redirect = () => {
window.location.href = props.buttonLink
}
</script>

View File

@@ -6,7 +6,7 @@
<div class="text-lg font-semibold mb-4">
{{ __('Upcoming Evaluations') }}
</div>
<div v-if="upcoming_evals.data">
<div v-if="upcoming_evals.data?.length">
<div class="grid grid-cols-2 gap-4">
<div v-for="evl in upcoming_evals.data">
<div class="border rounded-md p-3">

View File

@@ -120,7 +120,7 @@
/>
</div>
</div>
<div v-else class="h-screen">
<div v-else-if="!user.data?.name" class="h-screen">
<div class="text-base border rounded-md w-1/3 mx-auto my-32">
<div class="border-b px-5 py-3 font-medium">
<span
@@ -238,44 +238,41 @@ const isStudent = computed(() => {
})
const tabIndex = ref(0)
const tabs = []
if (isStudent.value) {
tabs.push({
label: 'Dashboard',
icon: LayoutDashboard,
const tabs = computed(() => {
let batchTabs = []
if (isStudent.value) {
batchTabs.push({
label: 'Dashboard',
icon: LayoutDashboard,
})
}
if (user.data?.is_moderator) {
batchTabs.push({
label: 'Students',
icon: Contact2,
})
batchTabs.push({
label: 'Assessments',
icon: BookOpenCheck,
})
}
batchTabs.push({
label: 'Live Class',
icon: Laptop,
})
}
if (user.data?.is_moderator) {
tabs.push({
label: 'Students',
icon: Contact2,
batchTabs.push({
label: 'Courses',
icon: BookOpen,
})
tabs.push({
label: 'Assessments',
icon: BookOpenCheck,
batchTabs.push({
label: 'Announcements',
icon: Mail,
})
}
tabs.push({
label: 'Live Class',
icon: Laptop,
})
tabs.push({
label: 'Courses',
icon: BookOpen,
})
tabs.push({
label: 'Announcements',
icon: Mail,
})
tabs.push({
label: 'Discussions',
icon: MessageCircle,
batchTabs.push({
label: 'Discussions',
icon: MessageCircle,
})
return batchTabs
})
const courses = createResource({

View File

@@ -78,12 +78,7 @@
</div>
</template>
<script setup>
import {
Breadcrumbs,
createDocumentResource,
createListResource,
createResource,
} from 'frappe-ui'
import { Breadcrumbs, createResource } from 'frappe-ui'
import { BookOpen, Calendar, Clock } from 'lucide-vue-next'
import { formatTime } from '../utils'
import { computed, inject } from 'vue'
@@ -110,7 +105,7 @@ const batch = createResource({
},
auto: true,
onSuccess(data) {
if (data.students?.includes(user.data.name)) {
if (data.students?.includes(user.data?.name)) {
router.push({
name: 'Batch',
params: {

View File

@@ -1,14 +1,64 @@
<template>
<div class="text-base h-screen">
<div v-if="access.data?.access" class="mt-20 w-1/2 mx-auto">
<div
v-if="access.data?.access && orderSummary.data"
class="mt-10 w-1/2 mx-auto"
>
<div class="text-3xl font-bold">
{{ __('Billing Details') }}
</div>
<div class="text-gray-600 mt-1">
{{ __('Enter the billing information to complete the payment.') }}
</div>
<div class="border rounded-md p-8 mt-10">
<div class="border rounded-md p-5 mt-5">
<div class="text-xl font-semibold">
{{ __('Summary') }}
</div>
<div class="text-gray-600 mt-1">
{{ __('Review the details of your purchase.') }}
</div>
<div class="mt-5">
<div class="flex items-center justify-between">
<div>
{{ orderSummary.data.title }}
</div>
<div
:class="{
'font-semibold text-xl': !orderSummary.data.gst_applied,
}"
>
{{
orderSummary.data.gst_applied
? orderSummary.data.original_amount_formatted
: orderSummary.data.total_amount_formatted
}}
</div>
</div>
<div
v-if="orderSummary.data.gst_applied"
class="flex items-center justify-between mt-2"
>
<div>
{{ __('GST Amount') }}
</div>
<div>
{{ orderSummary.data.gst_amount_formatted }}
</div>
</div>
<div
v-if="orderSummary.data.gst_applied"
class="flex items-center justify-between mt-2"
>
<div>
{{ __('Total Amount') }}
</div>
<div class="font-semibold text-2xl">
{{ orderSummary.data.total_amount_formatted }}
</div>
</div>
</div>
<div class="text-xl font-semibold mt-10">
{{ __('Address') }}
</div>
<div class="text-gray-600 mt-1">
@@ -26,13 +76,13 @@
<div class="mb-1.5 text-sm text-gray-700">
{{ __('Address Line 1') }}
</div>
<Input type="text" v-model="billingDetails.address_line_1" />
<Input type="text" v-model="billingDetails.address_line1" />
</div>
<div class="mt-4">
<div class="mb-1.5 text-sm text-gray-700">
{{ __('Address Line 2') }}
</div>
<Input type="text" v-model="billingDetails.address_line_2" />
<Input type="text" v-model="billingDetails.address_line2" />
</div>
<div class="mt-4">
<div class="mb-1.5 text-sm text-gray-700">
@@ -52,7 +102,11 @@
<div class="mb-1.5 text-sm text-gray-700">
{{ __('Country') }}
</div>
<Input type="text" v-model="billingDetails.country" />
<Link
doctype="Country"
:value="billingDetails.country"
@change="(option) => changeCurrency(option)"
/>
</div>
<div class="mt-4">
<div class="mb-1.5 text-sm text-gray-700">
@@ -70,7 +124,11 @@
<div class="mb-1.5 text-sm text-gray-700">
{{ __('Source') }}
</div>
<Input type="text" v-model="billingDetails.source" />
<Link
doctype="LMS Source"
:value="billingDetails.source"
@change="(option) => (billingDetails.source = option)"
/>
</div>
<div v-if="billingDetails.country == 'India'" class="mt-4">
<div class="mb-1.5 text-sm text-gray-700">
@@ -86,62 +144,46 @@
</div>
</div>
</div>
<Button variant="solid" class="mt-8">
<Button variant="solid" class="mt-8" @click="generatePaymentLink()">
{{ __('Proceed to Payment') }}
</Button>
</div>
</div>
<div v-else>
<div class="text-base border rounded-md w-1/3 mx-auto my-32">
<div class="border-b px-5 py-3 font-medium">
<span
class="inline-flex items-center before:bg-red-600 before:w-2 before:h-2 before:rounded-md before:mr-2"
></span>
{{ __('Not Permitted') }}
</div>
<div class="px-5 py-3">
<div class="mb-4 leading-6">
{{ access.data?.message }}
</div>
<Button
v-if="!user.data.name"
variant="solid"
class="w-full"
@click="redirectToLogin()"
>
{{ __('Login') }}
</Button>
<router-link
v-else-if="type == 'course'"
:to="{
name: 'Courses',
}"
>
<Button variant="solid">
{{ __('Checkout Courses') }}
</Button>
</router-link>
<router-link
v-else-if="type == 'batch'"
:to="{
name: 'Batches',
}"
>
<Button varian="solid">
{{ __('Checkout Batches') }}
</Button>
</router-link>
</div>
</div>
<div v-else-if="access.data?.message">
<NotPermitted
:text="access.data.message"
:buttonLabel="
type == 'course' ? 'Checkout Courses' : 'Checkout Batches'
"
:buttonLink="type == 'course' ? '/courses' : '/batches'"
/>
</div>
<div v-else-if="!user.data?.name">
<NotPermitted
text="Please login to access this page."
:buttonLink="`/login?redirect-to=/billing/${type}/${name}`"
/>
</div>
</div>
</template>
<script setup>
import { Input, Button, createResource } from 'frappe-ui'
import { reactive, inject, onMounted, ref } from 'vue'
import Link from '@/components/Controls/Link.vue'
import NotPermitted from '@/components/NotPermitted.vue'
import { createToast } from '@/utils/'
const user = inject('$user')
onMounted(() => {
const script = document.createElement('script')
script.src = `https://checkout.razorpay.com/v1/checkout.js`
document.body.appendChild(script)
if (user.data?.name) {
access.submit()
}
})
const props = defineProps({
type: {
type: String,
@@ -159,24 +201,189 @@ const access = createResource({
type: props.type,
name: props.name,
},
auto: true,
onSuccess(data) {
orderSummary.submit()
setBillingDetails(data.address)
},
})
const billingDetails = reactive({
billing_name: '',
address_line_1: '',
address_line_2: '',
city: '',
state: '',
pincode: '',
country: '',
phone: '',
source: '',
gstin: '',
pan: '',
const orderSummary = createResource({
url: 'lms.lms.utils.get_order_summary',
makeParams(values) {
return {
doctype: props.type == 'course' ? 'LMS Course' : 'LMS Batch',
docname: props.name,
country: billingDetails.country,
}
},
onError(err) {
showError(err)
},
})
const redirectToLogin = () => {
window.location.href = `/login?redirect-to=/billing/${props.type}/${props.name}`
const billingDetails = reactive({})
const setBillingDetails = (data) => {
billingDetails.billing_name = data.billing_name || ''
billingDetails.address_line1 = data.address_line1 || ''
billingDetails.address_line2 = data.address_line2 || ''
billingDetails.city = data.city || ''
billingDetails.state = data.state || ''
billingDetails.country = data.country || ''
billingDetails.pincode = data.pincode || ''
billingDetails.phone = data.phone || ''
billingDetails.source = data.source || ''
billingDetails.gstin = data.gstin || ''
billingDetails.pan = data.pan || ''
}
const paymentOptions = createResource({
url: 'lms.lms.utils.get_payment_options',
makeParams(values) {
return {
doctype: props.type == 'course' ? 'LMS Course' : 'LMS Batch',
docname: props.name,
phone: billingDetails.phone,
country: billingDetails.country,
}
},
})
const generatePaymentLink = () => {
paymentOptions.submit(
{},
{
validate(params) {
return validateAddress()
},
onSuccess(data) {
data.handler = (response) => {
let doctype = props.type == 'course' ? 'LMS Course' : 'LMS Batch'
let docname = props.name
handleSuccess(response, doctype, docname, data.order_id)
}
let rzp1 = new Razorpay(data)
rzp1.open()
},
onError(err) {
showError(err)
},
}
)
}
const paymentResource = createResource({
url: 'lms.lms.utils.verify_payment',
makeParams(values) {
return {
response: values.response,
doctype: props.type == 'course' ? 'LMS Course' : 'LMS Batch',
docname: props.name,
address: billingDetails,
order_id: values.orderId,
}
},
})
const handleSuccess = (response, doctype, docname, orderId) => {
paymentResource.submit(
{
response: response,
orderId: orderId,
},
{
onSuccess(data) {
createToast({
title: 'Success',
text: 'Payment Successful',
icon: 'check',
iconClasses: 'bg-green-600 text-white rounded-md p-px',
})
setTimeout(() => {
window.location.href = data
}, 3000)
},
}
)
}
const validateAddress = () => {
let mandatoryFields = [
'billing_name',
'address_line1',
'city',
'pincode',
'country',
'phone',
'source',
]
for (let field of mandatoryFields) {
if (!billingDetails[field])
return (
'Please enter a valid ' +
field
.replaceAll('_', ' ')
.toLowerCase()
.replace(/\b\w/g, (s) => s.toUpperCase())
)
}
if (billingDetails.gstin && !billingDetails.pan)
return 'Please enter a valid pan number.'
if (billingDetails.country == 'India' && !billingDetails.state)
return 'Please enter a valid state with correct spelling and the first letter capitalized.'
const states = [
'Andhra Pradesh',
'Arunachal Pradesh',
'Assam',
'Bihar',
'Chhattisgarh',
'Goa',
'Gujarat',
'Haryana',
'Himachal Pradesh',
'Jharkhand',
'Karnataka',
'Kerala',
'Madhya Pradesh',
'Maharashtra',
'Manipur',
'Meghalaya',
'Mizoram',
'Nagaland',
'Odisha',
'Punjab',
'Rajasthan',
'Sikkim',
'Tamil Nadu',
'Telangana',
'Tripura',
'Uttar Pradesh',
'Uttarakhand',
'West Bengal',
]
if (
billingDetails.country == 'India' &&
!states.includes(billingDetails.state)
)
return 'Please enter a valid state with correct spelling and the first letter capitalized.'
}
const showError = (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,
})
}
const changeCurrency = (country) => {
billingDetails.country = country
orderSummary.reload()
}
</script>

File diff suppressed because it is too large Load Diff