feat: implement coupon code manage
ment with billing and transaction integration; bug fixes
This commit is contained in:
@@ -24,15 +24,28 @@
|
||||
:label="__('Discount Amount')"
|
||||
type="number"
|
||||
/>
|
||||
<FormControl v-model="doc.expires_on" :label="__('Expires On')" type="date" />
|
||||
<FormControl v-model="doc.usage_limit" :label="__('Usage Limit')" type="number" />
|
||||
<FormControl
|
||||
v-model="doc.expires_on"
|
||||
:label="__('Expires On')"
|
||||
type="date"
|
||||
:description="__('Leave blank for no expiry')"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="doc.usage_limit"
|
||||
:label="__('Usage Limit')"
|
||||
type="number"
|
||||
:placeholder="__('Unlimited')"
|
||||
/>
|
||||
<Switch v-model="doc.active" :label="__('Active')" />
|
||||
<div class="col-span-2">
|
||||
<div class="text-sm font-medium text-ink-gray-7 mb-2">{{ __('Applicable Items (optional)') }}</div>
|
||||
<div class="text-md font-medium text-ink-gray-7 mb-1 mt-2">{{ __('Select Courses/Batches') }}<span class="text-ink-red-3">*</span></div>
|
||||
<div class="space-y-2">
|
||||
<div v-for="(row, idx) in doc.applicable_items" :key="idx" class="flex gap-2 items-center">
|
||||
<FormControl v-model="row.reference_doctype" :label="__('Type')" type="select" :options="['LMS Course', 'LMS Batch']" />
|
||||
<Link :doctype="row.reference_doctype || 'LMS Course'" :label="__('Item')" :value="row.reference_name" @change="(opt) => (row.reference_name = opt)" />
|
||||
<div v-for="(row, idx) in doc.applicable_items" :key="idx" class="flex gap-2 items-end">
|
||||
<FormControl class="w-28" v-model="row.reference_doctype" :label="__('Type')" type="select" :options="[
|
||||
{ label: 'Course ', value: 'LMS Course' },
|
||||
{ label: 'Batch ', value: 'LMS Batch' }
|
||||
]" />
|
||||
<Link class="min-w-40" :doctype="row.reference_doctype || 'LMS Course'" :label="__('Name')" :value="row.reference_name" @change="(opt) => (row.reference_name = opt)" />
|
||||
<Button variant="subtle" @click="removeRow(idx)">
|
||||
<X class="h-3 w-3" />
|
||||
</Button>
|
||||
@@ -135,7 +148,6 @@ function save() {
|
||||
emit('saved')
|
||||
},
|
||||
onError(err) {
|
||||
console.log('Save error:', err)
|
||||
toast.error(err.messages?.[0] || err.message || err)
|
||||
}
|
||||
})
|
||||
@@ -147,7 +159,6 @@ function save() {
|
||||
emit('saved')
|
||||
},
|
||||
onError(err) {
|
||||
console.log('Insert error:', err)
|
||||
toast.error(err.messages?.[0] || err.message || err)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
<th class="text-left p-2">{{ __('Expires On') }}</th>
|
||||
<th class="text-left p-2">{{ __('Usage') }}</th>
|
||||
<th class="text-left p-2">{{ __('Active') }}</th>
|
||||
<th class="text-right p-2 w-8"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -43,6 +44,13 @@
|
||||
<Badge v-if="row.active" theme="green">{{ __('Enabled') }}</Badge>
|
||||
<Badge v-else theme="gray">{{ __('Disabled') }}</Badge>
|
||||
</td>
|
||||
<td class="p-2 text-right" @click.stop>
|
||||
<Button variant="ghost" @click="confirmDelete(row)">
|
||||
<template #icon>
|
||||
<Trash2 class="h-4 w-4 stroke-1.5 text-ink-red-4" />
|
||||
</template>
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -52,11 +60,14 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Button, Badge, createListResource } from 'frappe-ui'
|
||||
import { ref } from 'vue'
|
||||
import { Plus } from 'lucide-vue-next'
|
||||
import { Button, Badge, createListResource, toast, call } from 'frappe-ui'
|
||||
import { ref, getCurrentInstance } from 'vue'
|
||||
import { Plus, Trash2 } from 'lucide-vue-next'
|
||||
import CouponDetails from '@/components/Settings/CouponDetails.vue'
|
||||
|
||||
const app = getCurrentInstance()
|
||||
const { $dialog } = app.appContext.config.globalProperties
|
||||
|
||||
defineProps({
|
||||
label: String,
|
||||
description: String,
|
||||
@@ -78,6 +89,7 @@ const coupons = createListResource({
|
||||
'times_redeemed',
|
||||
'active',
|
||||
],
|
||||
auto: true,
|
||||
})
|
||||
|
||||
function openForm(id) {
|
||||
@@ -88,5 +100,31 @@ function openForm(id) {
|
||||
function onSaved() {
|
||||
coupons.reload()
|
||||
}
|
||||
|
||||
function confirmDelete(row) {
|
||||
$dialog({
|
||||
title: __('Delete this coupon?'),
|
||||
message: __('This will permanently delete the coupon and the code will no longer work. Are you sure?'),
|
||||
actions: [
|
||||
{
|
||||
label: __('Delete'),
|
||||
theme: 'red',
|
||||
variant: 'solid',
|
||||
onClick({ close }) {
|
||||
trashCoupon(row.name, close)
|
||||
close()
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
function trashCoupon(name, close) {
|
||||
call('frappe.client.delete', { doctype: 'LMS Coupon', name }).then(() => {
|
||||
toast.success(__('Coupon deleted successfully'))
|
||||
coupons.reload()
|
||||
if (typeof close === 'function') close()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -67,43 +67,32 @@
|
||||
/>
|
||||
<FormControl :label="__('Amount')" v-model="transactionData.amount" />
|
||||
<FormControl
|
||||
:label="__('Order ID')"
|
||||
v-model="transactionData.order_id"
|
||||
v-if="transactionData.coupon"
|
||||
:label="__('Coupon Code')"
|
||||
v-model="transactionData.coupon"
|
||||
:disabled="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="transactionData && (transactionData.coupon || transactionData.discount_amount || transactionData.discount_percent)"
|
||||
class="mt-10"
|
||||
>
|
||||
<div class="font-semibold">
|
||||
{{ __('Coupon (if applied)') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-5 mt-5">
|
||||
<Link
|
||||
:label="__('Coupon')"
|
||||
v-model="transactionData.coupon"
|
||||
doctype="LMS Coupon"
|
||||
:disabled="true"
|
||||
/>
|
||||
<FormControl
|
||||
:label="__('Discount Type')"
|
||||
v-model="transactionData.discount_type"
|
||||
:disabled="true"
|
||||
/>
|
||||
<FormControl
|
||||
v-if="transactionData.discount_type === 'Percent'"
|
||||
:label="__('Discount Percent')"
|
||||
v-model="transactionData.discount_percent"
|
||||
:disabled="true"
|
||||
/>
|
||||
<FormControl
|
||||
v-else
|
||||
:label="__('Discount Amount')"
|
||||
v-model="transactionData.discount_amount"
|
||||
:disabled="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-5 mt-5">
|
||||
<FormControl
|
||||
v-if="Number(transactionData.discount_amount)"
|
||||
:label="__('Discount Amount')"
|
||||
v-model="transactionData.discount_amount"
|
||||
:disabled="true"
|
||||
/>
|
||||
<FormControl
|
||||
v-if="Number(transactionData.gst_amount)"
|
||||
:label="__('GST Amount')"
|
||||
v-model="transactionData.gst_amount"
|
||||
:disabled="true"
|
||||
/>
|
||||
<FormControl
|
||||
v-if="Number(transactionData.discount_amount) || Number(transactionData.gst_amount)"
|
||||
:label="__('Total Amount')"
|
||||
v-model="transactionData.total_amount"
|
||||
:disabled="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-5 mt-5">
|
||||
@@ -114,6 +103,13 @@
|
||||
v-model="transactionData.payment_id"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-5 mt-5">
|
||||
<FormControl
|
||||
:label="__('Order ID')"
|
||||
v-model="transactionData.order_id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions="{ close }">
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
:disabled="true"
|
||||
/>
|
||||
<div v-else-if="column.key == 'amount'">
|
||||
{{ getCurrencySymbol(row['currency']) }} {{ row[column.key] }}
|
||||
{{ getCurrencySymbol(row['currency']) }} {{ row['total_amount'] }}
|
||||
</div>
|
||||
<div v-else class="leading-5 text-sm">
|
||||
{{ row[column.key] }}
|
||||
@@ -153,6 +153,7 @@ const transactions = createListResource({
|
||||
'payment_for_certificate',
|
||||
'currency',
|
||||
'amount',
|
||||
'total_amount',
|
||||
'order_id',
|
||||
'payment_id',
|
||||
'gstin',
|
||||
|
||||
@@ -35,6 +35,10 @@
|
||||
{{ orderSummary.data.original_amount_formatted }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="orderSummary.data.discount_amount" class="flex items-center justify-between mt-2">
|
||||
<div class="text-ink-gray-5">{{ __('Discount') }}</div>
|
||||
<div>-{{ orderSummary.data.discount_amount_formatted }}</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="orderSummary.data.gst_applied"
|
||||
class="flex items-center justify-between mt-2"
|
||||
@@ -46,10 +50,6 @@
|
||||
{{ orderSummary.data.gst_amount_formatted }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="orderSummary.data.discount_amount" class="flex items-center justify-between mt-2">
|
||||
<div class="text-ink-gray-5">{{ __('Discount') }}</div>
|
||||
<div>-{{ orderSummary.data.discount_amount_formatted }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center justify-between border-t border-outline-gray-3 pt-4 mt-2"
|
||||
>
|
||||
@@ -63,9 +63,9 @@
|
||||
<div class="mb-5">
|
||||
<div class="flex items-center gap-3 mt-2 flex-wrap md:flex-nowrap" v-if="props.type !== 'certificate'">
|
||||
<span class="text-ink-gray-5 text-xs shrink-0">{{ __('Coupon') }}</span>
|
||||
<FormControl class="flex-1 min-w-0 [&_input]:!bg-[#fefefe]" v-model="couponCode" @input="couponCode = $event.target.value.toUpperCase()"/>
|
||||
<Button @click="applyCouponCode" variant="outline">{{ __('Apply') }}</Button>
|
||||
<Button v-if="appliedCoupon" @click="removeCoupon" variant="subtle">{{ __('Remove') }}</Button>
|
||||
<FormControl class="flex-1 min-w-0 [&_input]:!bg-[#fefefe]" v-model="couponCode" :disabled="appliedCoupon" @input="couponCode = $event.target.value.toUpperCase()"/>
|
||||
<Button v-if="!appliedCoupon" @click="applyCouponCode" variant="outline">{{ __('Apply') }}</Button>
|
||||
<Button v-if="appliedCoupon" @click="removeCoupon" variant="subtle" class="bg-red-200"><X class="h-4.5 w-4.5" /></Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -174,6 +174,7 @@ import { reactive, inject, onMounted, computed, ref } from 'vue'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import NotPermitted from '@/components/NotPermitted.vue'
|
||||
import { X } from 'lucide-vue-next'
|
||||
|
||||
const user = inject('$user')
|
||||
const { brand } = sessionStore()
|
||||
@@ -239,6 +240,7 @@ const applyCoupon = createResource({
|
||||
},
|
||||
onSuccess(data) {
|
||||
orderSummary.data = data
|
||||
console.log('orderSummary.data - ', orderSummary.data)
|
||||
appliedCoupon.value = couponCode.value
|
||||
toast.success(__('Coupon applied'))
|
||||
},
|
||||
@@ -266,18 +268,20 @@ const setBillingDetails = (data) => {
|
||||
const paymentLink = createResource({
|
||||
url: 'lms.lms.payments.get_payment_link',
|
||||
makeParams(values) {
|
||||
return {
|
||||
let data={
|
||||
doctype: props.type == 'batch' ? 'LMS Batch' : 'LMS Course',
|
||||
docname: props.name,
|
||||
title: orderSummary.data.title,
|
||||
amount: orderSummary.data.original_amount,
|
||||
total_amount: orderSummary.data.amount,
|
||||
discount_amount: orderSummary.data.discount_amount || 0,
|
||||
gst_amount: orderSummary.data.gst_applied || 0,
|
||||
currency: orderSummary.data.currency,
|
||||
address: billingDetails,
|
||||
redirect_to: redirectTo.value,
|
||||
payment_for_certificate: props.type == 'certificate',
|
||||
coupon_code: appliedCoupon.value,
|
||||
}
|
||||
return data
|
||||
},
|
||||
})
|
||||
|
||||
@@ -308,6 +312,7 @@ function applyCouponCode() {
|
||||
|
||||
function removeCoupon() {
|
||||
appliedCoupon.value = null
|
||||
couponCode.value = ''
|
||||
orderSummary.reload()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user