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

@@ -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>