Merge pull request #1726 from pateljannat/payment-settings-refactor

refactor: payment settings
This commit is contained in:
Jannat Patel
2025-09-23 11:37:03 +05:30
committed by GitHub
17 changed files with 3652 additions and 4268 deletions

View File

@@ -86,7 +86,11 @@ declare module 'vue' {
Notes: typeof import('./src/components/Notes/Notes.vue')['default'] Notes: typeof import('./src/components/Notes/Notes.vue')['default']
NotPermitted: typeof import('./src/components/NotPermitted.vue')['default'] NotPermitted: typeof import('./src/components/NotPermitted.vue')['default']
PageModal: typeof import('./src/components/Modals/PageModal.vue')['default'] PageModal: typeof import('./src/components/Modals/PageModal.vue')['default']
PaymentGateway: typeof import('./src/components/Settings/PaymentGateway.vue')['default']
PaymentGatewayDetails: typeof import('./src/components/Settings/PaymentGatewayDetails.vue')['default']
PaymentGateways: typeof import('./src/components/Settings/PaymentGateways.vue')['default']
PaymentSettings: typeof import('./src/components/Settings/PaymentSettings.vue')['default'] PaymentSettings: typeof import('./src/components/Settings/PaymentSettings.vue')['default']
PaymentTransactions: typeof import('./src/components/Settings/PaymentTransactions.vue')['default']
Play: typeof import('./src/components/Icons/Play.vue')['default'] Play: typeof import('./src/components/Icons/Play.vue')['default']
ProgressBar: typeof import('./src/components/ProgressBar.vue')['default'] ProgressBar: typeof import('./src/components/ProgressBar.vue')['default']
Question: typeof import('./src/components/Modals/Question.vue')['default'] Question: typeof import('./src/components/Modals/Question.vue')['default']
@@ -105,6 +109,8 @@ declare module 'vue' {
StudentHeatmap: typeof import('./src/components/StudentHeatmap.vue')['default'] StudentHeatmap: typeof import('./src/components/StudentHeatmap.vue')['default']
StudentModal: typeof import('./src/components/Modals/StudentModal.vue')['default'] StudentModal: typeof import('./src/components/Modals/StudentModal.vue')['default']
Tags: typeof import('./src/components/Tags.vue')['default'] Tags: typeof import('./src/components/Tags.vue')['default']
TransactionDetails: typeof import('./src/components/Settings/TransactionDetails.vue')['default']
Transactions: typeof import('./src/components/Settings/Transactions.vue')['default']
UnsplashImageBrowser: typeof import('./src/components/UnsplashImageBrowser.vue')['default'] UnsplashImageBrowser: typeof import('./src/components/UnsplashImageBrowser.vue')['default']
UpcomingEvaluations: typeof import('./src/components/UpcomingEvaluations.vue')['default'] UpcomingEvaluations: typeof import('./src/components/UpcomingEvaluations.vue')['default']
Uploader: typeof import('./src/components/Controls/Uploader.vue')['default'] Uploader: typeof import('./src/components/Controls/Uploader.vue')['default']

View File

@@ -54,6 +54,7 @@
"@vitejs/plugin-vue": "^5.0.3", "@vitejs/plugin-vue": "^5.0.3",
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.2",
"postcss": "^8.4.5", "postcss": "^8.4.5",
"vite": "^5.0.11" "vite": "^5.0.11",
"vite-plugin-pwa": "^1.0.2"
} }
} }

View File

@@ -141,9 +141,6 @@ const props = defineProps({
type: String, type: String,
default: '', default: '',
}, },
show: {
type: Boolean,
},
}) })
const evaluators = createListResource({ const evaluators = createListResource({

View File

@@ -156,9 +156,6 @@ const props = defineProps({
type: String, type: String,
default: '', default: '',
}, },
show: {
type: Boolean,
},
}) })
const members = createResource({ const members = createResource({
@@ -185,7 +182,6 @@ const openProfile = (username: string) => {
username: username, username: username,
}, },
}) })
console.log(show.value)
} }
const newMember = createResource({ const newMember = createResource({

View File

@@ -0,0 +1,233 @@
<template>
<Dialog
v-model="show"
:options="{
title:
gatewayID === 'new'
? __('New Payment Gateway')
: __('Edit Payment Gateway'),
size: '3xl',
}"
>
<template #body-content>
<SettingFields
v-if="gatewayID != 'new' && paymentGateway.data"
:fields="paymentGateway.data.fields"
:data="paymentGateway.data.data"
class="pt-5 my-0"
/>
<div v-else>
<FormControl
v-model="newGateway"
:label="__('Select Payment Gateway')"
type="select"
:options="allGatewayOptions"
:required="true"
/>
<SettingFields
v-if="newGateway"
:fields="newGatewayFields"
:data="newGatewayData"
class="pt-5 my-0"
/>
</div>
</template>
<template #actions="{ close }">
<div class="pb-5 float-right">
<Button variant="solid" @click="saveSettings(close)">
{{ __('Save') }}
</Button>
</div>
</template>
</Dialog>
</template>
<script setup lang="ts">
import {
Button,
call,
createListResource,
createResource,
Dialog,
FormControl,
} from 'frappe-ui'
import { computed, ref, watch } from 'vue'
import SettingFields from '@/components/Settings/SettingFields.vue'
const show = defineModel<boolean>({ required: true, default: false })
const paymentGateways = defineModel<any>('paymentGateways')
const newGateway = ref(null)
const newGatewayFields = ref([])
const newGatewayData = ref<Record<string, any>>({})
const props = defineProps<{
gatewayID: string | null
}>()
const paymentGateway = createResource({
url: 'lms.lms.api.get_payment_gateway_details',
makeParams(values: any) {
return {
payment_gateway: props.gatewayID,
}
},
transform(data: any) {
arrangeFields(data.fields)
return data
},
})
const allGateways = createListResource({
doctype: 'DocType',
filters: {
module: 'Payment Gateways',
},
fields: ['name', 'issingle'],
})
const gatewayFields = createResource({
url: 'lms.lms.api.get_new_gateway_fields',
makeParams(values: any) {
return {
doctype: values.doctype,
}
},
})
const arrangeFields = (fields: any[]) => {
fields = fields.sort((a, b) => {
if (a.type === 'Upload' && b.type !== 'Upload') {
return 1
} else if (a.type !== 'Upload' && b.type === 'Upload') {
return -1
}
return 0
})
fields.splice(3, 0, {
type: 'Column Break',
})
}
watch(
() => props.gatewayID,
() => {
if (props.gatewayID && props.gatewayID !== 'new') {
paymentGateway.reload()
} else if (props.gatewayID == 'new') {
allGateways.reload()
}
}
)
const getNewGateway = () => {
return allGateways.data?.find((gateway: any) =>
gateway.name.includes(newGateway.value)
)
}
watch(newGateway, () => {
let gatewayDoc = getNewGateway()
gatewayFields.reload({ doctype: gatewayDoc.name }).then(() => {
let fields = gatewayFields.data || []
arrangeFields(fields)
newGatewayFields.value = fields
prepareGatewayData()
})
})
const saveSettings = (close: () => void) => {
if (props.gatewayID === 'new') {
saveNewGateway(close)
} else {
saveExistingGateway(
paymentGateway.data.doctype,
paymentGateway.data.docname,
close
)
}
}
const saveNewGateway = (close: () => void) => {
let gatewayDoc = getNewGateway()
if (gatewayDoc.issingle) {
saveExistingGateway(gatewayDoc.name, gatewayDoc.name, close)
} else {
call('frappe.client.insert', {
doc: {
doctype: gatewayDoc.name,
...newGatewayData.value,
},
}).then((data: any) => {
paymentGateways.value.reload()
close()
})
}
}
const saveExistingGateway = (
doctype: string,
docname: string,
close: () => void
) => {
call('frappe.client.set_value', {
doctype: doctype,
name: docname,
fieldname: getGatewayFields(),
}).then(() => {
paymentGateways.value?.reload()
close()
})
}
const getGatewayFields = () => {
let data =
props.gatewayID == 'new' ? newGatewayData.value : paymentGateway.data.data
return Object.keys(data).reduce((fields: any, key: string) => {
if (data[key] && typeof data[key] === 'object') {
fields[key] = data[key].file_url
} else {
fields[key] = data[key]
}
return fields
}, {})
}
const createGatewayRecord = (gatewayDoc: any, data: any = {}) => {
call('frappe.client.insert', {
doc: {
doctype: 'Payment Gateway',
gateway: newGateway.value,
gateway_controller: gatewayDoc.issingle ? '' : gatewayDoc.name,
gateway_settings: gatewayDoc.issingle ? '' : data.name,
},
}).then(() => {
paymentGateways.value?.reload()
})
}
const allGatewayOptions = computed(() => {
let options: string[] = []
let gatewayList = allGateways.data?.map((gateway: any) => gateway.name) || []
gatewayList.forEach((gateway: any) => {
let gatewayName = gateway.split(' ')[0]
let existingGateways =
paymentGateways.value?.data?.map((pg: any) => pg.name) || []
if (
!options.includes(gatewayName) &&
!existingGateways.includes(gatewayName)
) {
options.push(gatewayName)
}
})
return options.map((gateway: string) => ({ label: gateway, value: gateway }))
})
const prepareGatewayData = () => {
newGatewayData.value = {}
if (newGatewayFields.value.length) {
newGatewayFields.value.forEach((field: any) => {
newGatewayData.value[field.fieldname] = field.default || ''
})
}
}
</script>

View File

@@ -0,0 +1,140 @@
<template>
<div class="flex min-h-0 flex-col text-base">
<div class="flex items-center justify-between mb-5">
<div>
<div class="text-xl font-semibold mb-1 text-ink-gray-9">
{{ __(label) }}
</div>
<div class="text-ink-gray-6 leading-5">
{{ __(description) }}
</div>
</div>
<Button @click="openForm('new')">
<template #prefix>
<Plus class="h-3 w-3 stroke-1.5" />
</template>
{{ __('New') }}
</Button>
</div>
<div v-if="paymentGateways.data?.length" class="overflow-y-scroll">
<ListView
:columns="columns"
:rows="paymentGateways.data"
row-key="name"
:options="{
showTooltip: false,
onRowClick: (row) => {
openForm(row.name)
},
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
>
<ListHeaderItem :item="item" v-for="item in columns">
<template #prefix="{ item }">
<FeatherIcon
v-if="item.icon"
:name="item.icon"
class="h-4 w-4 stroke-1.5"
/>
</template>
</ListHeaderItem>
</ListHeader>
<ListRows>
<ListRow :row="row" v-for="row in paymentGateways.data">
<template #default="{ column, item }">
<ListRowItem :item="row[column.key]" :align="column.align">
<div v-if="column.key == 'enabled'">
<Badge v-if="row[column.key]" theme="green">
{{ __('Enabled') }}
</Badge>
<Badge v-else theme="gray">
{{ __('Disabled') }}
</Badge>
</div>
<div v-else class="leading-5 text-sm">
{{ row[column.key] }}
</div>
</ListRowItem>
</template>
</ListRow>
</ListRows>
<ListSelectBanner>
<template #actions="{ unselectAll, selections }">
<div class="flex gap-2">
<Button
variant="ghost"
@click="removeAccount(selections, unselectAll)"
>
<Trash2 class="h-4 w-4 stroke-1.5" />
</Button>
</div>
</template>
</ListSelectBanner>
</ListView>
</div>
</div>
<PaymentGatewayDetails
v-model="showForm"
:gatewayID="currentGateway"
v-model:paymentGateways="paymentGateways"
/>
</template>
<script setup>
import {
Badge,
Button,
createListResource,
FeatherIcon,
ListView,
ListHeader,
ListHeaderItem,
ListRows,
ListRow,
ListRowItem,
ListSelectBanner,
} from 'frappe-ui'
import { computed, ref } from 'vue'
import { Plus, Trash2 } from 'lucide-vue-next'
import PaymentGatewayDetails from '@/components/Settings/PaymentGatewayDetails.vue'
const showForm = ref(false)
const currentGateway = ref(null)
const props = defineProps({
label: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
})
const paymentGateways = createListResource({
doctype: 'Payment Gateway',
fields: ['name', 'gateway_settings', 'gateway_controller'],
auto: true,
orderBy: 'modified desc',
})
const openForm = (gatewayID) => {
currentGateway.value = gatewayID
showForm.value = true
}
const columns = computed(() => {
return [
{
label: __('Gateway'),
key: 'name',
icon: 'credit-card',
},
]
})
</script>

View File

@@ -1,128 +0,0 @@
<template>
<div class="flex flex-col h-full">
<div class="flex items-center justify-between">
<div class="text-xl font-semibold mb-1 text-ink-gray-9">
{{ label }}
</div>
<!-- <Badge
v-if="isDirty"
:label="__('Not Saved')"
variant="subtle"
theme="orange"
/> -->
</div>
<div class="overflow-y-scroll">
<div class="flex flex-col divide-y">
<SettingFields :fields="fields" :data="data.doc" />
<SettingFields
v-if="paymentGateway.data"
:fields="paymentGateway.data.fields"
:data="paymentGateway.data.data"
class="pt-5 my-0"
/>
</div>
</div>
<div class="flex flex-row-reverse mt-auto">
<Button variant="solid" @click="update">
{{ __('Update') }}
</Button>
</div>
</div>
</template>
<script setup>
import SettingFields from '@/components/Settings/SettingFields.vue'
import { createResource, Badge, Button } from 'frappe-ui'
import { watch } from 'vue'
const props = defineProps({
label: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
data: {
type: Object,
required: true,
},
fields: {
type: Array,
required: true,
},
})
const paymentGateway = createResource({
url: 'lms.lms.api.get_payment_gateway_details',
makeParams(values) {
return {
payment_gateway: props.data.doc.payment_gateway,
}
},
transform(data) {
arrangeFields(data.fields)
return data
},
auto: true,
})
const arrangeFields = (fields) => {
fields = fields.sort((a, b) => {
if (a.type === 'Upload' && b.type !== 'Upload') {
return 1
} else if (a.type !== 'Upload' && b.type === 'Upload') {
return -1
}
return 0
})
fields.splice(3, 0, {
type: 'Column Break',
})
}
const saveSettings = createResource({
url: 'frappe.client.set_value',
makeParams(values) {
let fields = {}
Object.keys(paymentGateway.data.data).forEach((key) => {
if (
paymentGateway.data.data[key] &&
typeof paymentGateway.data.data[key] === 'object'
) {
fields[key] = paymentGateway.data.data[key].file_url
} else {
fields[key] = paymentGateway.data.data[key]
}
})
return {
doctype: paymentGateway.data.doctype,
name: paymentGateway.data.docname,
fieldname: fields,
}
},
auto: false,
onSuccess(data) {
paymentGateway.reload()
},
})
const update = () => {
props.fields.forEach((f) => {
if (f.type != 'Column Break') {
props.data.doc[f.name] = f.value
}
})
props.data.save.submit()
saveSettings.submit()
}
watch(
() => props.data.doc.payment_gateway,
() => {
paymentGateway.reload()
}
)
</script>

View File

@@ -1,8 +1,8 @@
<template> <template>
<div class="flex flex-col justify-between h-full"> <div class="flex flex-col justify-between h-full text-base">
<div> <div>
<div class="flex itemsc-center justify-between"> <div class="flex items-center justify-between">
<div class="text-xl font-semibold leading-none mb-1 text-ink-gray-9"> <div class="text-xl font-semibold leading-none mb-2 text-ink-gray-9">
{{ __(label) }} {{ __(label) }}
</div> </div>
<Badge <Badge
@@ -12,7 +12,7 @@
theme="orange" theme="orange"
/> />
</div> </div>
<div class="text-xs text-ink-gray-5"> <div class="text-ink-gray-6 leading-5">
{{ __(description) }} {{ __(description) }}
</div> </div>
</div> </div>

View File

@@ -30,12 +30,9 @@
</CodeEditor> </CodeEditor>
</div> </div>
<div <div v-else-if="field.type == 'Upload'">
v-else-if="field.type == 'Upload'" <div class="space-y-1 mb-2">
class="grid grid-cols-2 gap-10" <div class="text-sm text-ink-gray-5 font-medium">
>
<div class="space-y-2">
<div class="text-sm text-ink-gray-8 font-medium mb-1">
{{ __(field.label) }} {{ __(field.label) }}
</div> </div>
<div class="text-sm text-ink-gray-5 leading-5"> <div class="text-sm text-ink-gray-5 leading-5">

View File

@@ -37,22 +37,26 @@
<component <component
v-if="activeTab.template" v-if="activeTab.template"
:is="activeTab.template" :is="activeTab.template"
v-model:show="show"
v-bind="{ v-bind="{
label: activeTab.label, label: activeTab.label,
description: activeTab.description, description: activeTab.description,
...(activeTab.label === 'Branding' ...(activeTab.label == 'Branding'
? { fields: activeTab.fields } ? { fields: activeTab.fields }
: {}), : {}),
...(activeTab.label == 'Evaluators' ||
activeTab.label == 'Members' ||
activeTab.label == 'Transactions'
? { 'onUpdate:show': (val) => (show = val), show }
: {}),
}" }"
/> />
<PaymentSettings <!-- <PaymentSettings
v-else-if="activeTab.label === 'Payment Gateway'" v-else-if="activeTab.label === 'Gateways'"
:label="activeTab.label" :label="activeTab.label"
:description="activeTab.description" :description="activeTab.description"
:data="data" :data="data"
:fields="activeTab.fields" :fields="activeTab.fields"
/> /> -->
<SettingDetails <SettingDetails
v-else v-else
:fields="activeTab.fields" :fields="activeTab.fields"
@@ -76,7 +80,8 @@ import Evaluators from '@/components/Settings/Evaluators.vue'
import Categories from '@/components/Settings/Categories.vue' import Categories from '@/components/Settings/Categories.vue'
import EmailTemplates from '@/components/Settings/EmailTemplates.vue' import EmailTemplates from '@/components/Settings/EmailTemplates.vue'
import BrandSettings from '@/components/Settings/BrandSettings.vue' import BrandSettings from '@/components/Settings/BrandSettings.vue'
import PaymentSettings from '@/components/Settings/PaymentSettings.vue' import PaymentGateways from '@/components/Settings/PaymentGateways.vue'
import Transactions from '@/components/Settings/Transactions.vue'
import ZoomSettings from '@/components/Settings/ZoomSettings.vue' import ZoomSettings from '@/components/Settings/ZoomSettings.vue'
import Badges from '@/components/Settings/Badges.vue' import Badges from '@/components/Settings/Badges.vue'
@@ -159,14 +164,13 @@ const tabsStructure = computed(() => {
], ],
}, },
{ {
label: 'Settings', label: 'Payment',
hideLabel: true, hideLabel: false,
items: [ items: [
{ {
label: 'Payment Gateway', label: 'Configuration',
icon: 'DollarSign', icon: 'CreditCard',
description: description: 'Manage all your payment related settings and defaults',
'Configure the payment gateway and other payment related settings',
fields: [ fields: [
{ {
label: 'Default Currency', label: 'Default Currency',
@@ -200,6 +204,18 @@ const tabsStructure = computed(() => {
}, },
], ],
}, },
{
label: 'Gateways',
icon: 'DollarSign',
template: markRaw(PaymentGateways),
description: 'Add and manage all your payment gateways',
},
{
label: 'Transactions',
icon: 'Landmark',
template: markRaw(Transactions),
description: 'View all your payment transactions',
},
], ],
}, },
{ {
@@ -275,7 +291,7 @@ const tabsStructure = computed(() => {
name: 'favicon', name: 'favicon',
type: 'Upload', type: 'Upload',
description: description:
'Appears in the browser tab next to the page title, bookmarks, and shortcuts to help users quickly identify the application.', 'Appears in the browser tab next to the page title to help users quickly identify the application.',
}, },
], ],
}, },

View File

@@ -0,0 +1,152 @@
<template>
<Dialog
v-model="show"
:options="{
title: __('Transaction Details'),
size: '3xl',
}"
>
<template #body-content>
<div v-if="transactionData" class="text-base">
<div class="grid grid-cols-3 gap-5 mt-5">
<FormControl
:label="__('Payment Received')"
type="checkbox"
v-model="transactionData.payment_received"
/>
<FormControl
:label="__('Payment For Certificate')"
type="checkbox"
v-model="transactionData.payment_for_certificate"
/>
</div>
<div class="grid grid-cols-3 gap-5 mt-5">
<Link
:label="__('Member')"
doctype="User"
v-model="transactionData.member"
/>
<FormControl
:label="__('Billing Name')"
v-model="transactionData.billing_name"
/>
<Link
:label="__('Source')"
v-model="transactionData.source"
doctype="LMS Source"
/>
</div>
<div class="font-semibold mt-10">
{{ __('Payment Details') }}
</div>
<div class="grid grid-cols-3 gap-5 mt-5">
<Link
:label="__('Payment For Document Type')"
v-model="transactionData.payment_for_document_type"
doctype="DocType"
/>
<Link
:label="__('Payment For Document')"
v-model="transactionData.payment_for_document"
:doctype="transactionData.payment_for_document_type"
/>
<Link
:label="__('Address')"
v-model="transactionData.address"
doctype="Address"
/>
</div>
<div class="grid grid-cols-3 gap-5 mt-5">
<Link
:label="__('Currency')"
v-model="transactionData.currency"
doctype="Currency"
/>
<FormControl :label="__('Amount')" v-model="transactionData.amount" />
<FormControl
:label="__('Order ID')"
v-model="transactionData.order_id"
/>
</div>
<div class="grid grid-cols-3 gap-5 mt-5">
<FormControl :label="__('GSTIN')" v-model="transactionData.gstin" />
<FormControl :label="__('PAN')" v-model="transactionData.pan" />
<FormControl
:label="__('Payment ID')"
v-model="transactionData.payment_id"
/>
</div>
</div>
</template>
<template #actions="{ close }">
<div class="space-x-2 pb-5 float-right">
<Button @click="openDetails(close)">
{{ __('Open the ') }}
{{
transaction.payment_for_document_type == 'LMS Course'
? __('Course')
: __('Batch')
}}
</Button>
<Button variant="solid" @click="saveTransaction(close)">
{{ __('Save') }}
</Button>
</div>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { Dialog, FormControl, Button } from 'frappe-ui'
import { useRouter } from 'vue-router'
import { ref, watch } from 'vue'
import Link from '@/components/Controls/Link.vue'
const show = defineModel<boolean>({ required: true, default: false })
const transactions = defineModel<any>('transactions')
const router = useRouter()
const showModal = defineModel('show')
const transactionData = ref<{ [key: string]: any } | null>(null)
const props = defineProps<{
transaction: { [key: string]: any } | null
}>()
watch(
() => props.transaction,
(newVal) => {
transactionData.value = newVal ? { ...newVal } : null
},
{ immediate: true }
)
const saveTransaction = (close: () => void) => {
transactions.value.setValue
.submit({
...transactionData.value,
})
.then(() => {
close()
})
}
const openDetails = (close: Function) => {
if (props.transaction) {
const docType = props.transaction.payment_for_document_type
const docName = props.transaction.payment_for_document
if (docType && docName) {
router.push({
name: docType == 'LMS Course' ? 'CourseDetail' : 'BatchDetail',
params: {
[docType == 'LMS Course' ? 'courseName' : 'batchName']: docName,
},
})
}
}
close()
showModal.value = false
}
</script>

View File

@@ -0,0 +1,241 @@
<template>
<div class="flex min-h-0 flex-col text-base">
<div class="mb-5">
<div class="text-xl font-semibold mb-1 text-ink-gray-9">
{{ __(label) }}
</div>
<div class="text-ink-gray-6 leading-5">
{{ __(description) }}
</div>
</div>
<div class="flex items-center space-x-5 mb-4">
<FormControl
v-model="billingName"
:placeholder="__('Filter by Billing Name')"
/>
<Link
v-model="member"
doctype="User"
:placeholder="__('Filter by Member')"
/>
<FormControl
v-model="paymentReceived"
type="checkbox"
:label="__('Payment Received')"
/>
<FormControl
v-model="paymentForCertificate"
type="checkbox"
:label="__('Payment for Certificate')"
/>
</div>
<div v-if="transactions.data?.length" class="overflow-y-scroll">
<ListView
:columns="columns"
:rows="transactions.data"
row-key="name"
:options="{
showTooltip: false,
selectable: false,
onRowClick: (row: { [key: string]: any }) => {
openForm(row)
},
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
>
<ListHeaderItem :item="item" v-for="item in columns">
<template #prefix="{ item }">
<FeatherIcon
v-if="item.icon"
:name="item.icon"
class="h-4 w-4 stroke-1.5"
/>
</template>
</ListHeaderItem>
</ListHeader>
<ListRows>
<ListRow :row="row" v-for="row in transactions.data">
<template #default="{ column, item }">
<ListRowItem :item="row[column.key]" :align="column.align">
<FormControl
v-if="
['payment_received', 'payment_for_certificate'].includes(
column.key
)
"
type="checkbox"
v-model="row[column.key]"
:disabled="true"
/>
<div v-else-if="column.key == 'amount'">
{{ getCurrencySymbol(row['currency']) }} {{ row[column.key] }}
</div>
<div v-else class="leading-5 text-sm">
{{ row[column.key] }}
</div>
</ListRowItem>
</template>
</ListRow>
</ListRows>
</ListView>
<div
v-if="transactions.data.length && transactions.hasNextPage"
class="flex justify-center mt-4"
>
<Button @click="transactions.next()">
<template #prefix>
<RefreshCw class="h-3 w-3 stroke-1.5" />
</template>
{{ __('Load More') }}
</Button>
</div>
</div>
</div>
<TransactionDetails
v-model="showForm"
:transaction="currentTransaction"
v-model:transactions="transactions"
v-model:show="show"
/>
</template>
<script setup lang="ts">
import {
Button,
createListResource,
ListView,
ListHeader,
ListHeaderItem,
FeatherIcon,
ListRows,
ListRow,
ListRowItem,
FormControl,
} from 'frappe-ui'
import { computed, ref, watch } from 'vue'
import { RefreshCw } from 'lucide-vue-next'
import TransactionDetails from './TransactionDetails.vue'
import Link from '@/components/Controls/Link.vue'
const showForm = ref(false)
const currentTransaction = ref<{ [key: string]: any } | null>(null)
const show = defineModel('show')
const billingName = ref(null)
const paymentReceived = ref(false)
const paymentForCertificate = ref(false)
const member = ref(null)
const props = defineProps({
label: {
type: String,
required: true,
},
description: {
type: String,
default: '',
},
})
const transactions = createListResource({
doctype: 'LMS Payment',
fields: [
'name',
'member',
'billing_name',
'source',
'payment_for_document_type',
'payment_for_document',
'payment_received',
'payment_for_certificate',
'currency',
'amount',
'order_id',
'payment_id',
'gstin',
'pan',
'address',
],
auto: true,
orderBy: 'modified desc',
})
watch(
[billingName, member, paymentReceived, paymentForCertificate],
([
newBillingName,
newMember,
newPaymentReceived,
newPaymentForCertificate,
]) => {
transactions.update({
filters: [
newBillingName ? [['billing_name', 'like', `%${newBillingName}%`]] : [],
newMember ? [['member', '=', newMember]] : [],
newPaymentReceived
? [['payment_received', '=', newPaymentReceived]]
: [],
newPaymentForCertificate
? [['payment_for_certificate', '=', newPaymentForCertificate]]
: [],
].flat(),
})
transactions.reload()
},
{ immediate: true }
)
const openForm = (transaction: { [key: string]: any }) => {
currentTransaction.value = transaction
showForm.value = true
}
const getCurrencySymbol = (currency: string) => {
const currencySymbols: Record<string, string> = {
USD: '$',
EUR: '€',
GBP: '£',
INR: '₹',
AED: 'د.إ',
CHF: 'Fr',
JPY: '¥',
AUD: '$',
}
return currencySymbols[currency] || currency
}
const columns = computed(() => {
return [
{
label: __('Billing Name'),
icon: 'user',
key: 'billing_name',
width: '30%',
},
{
label: __('Amount'),
icon: 'dollar-sign',
key: 'amount',
width: '20%',
align: 'right',
},
{
label: __('Payment Received'),
icon: 'check-circle',
key: 'payment_received',
width: '25%',
align: 'center',
},
{
label: __('Payment for Certificate'),
icon: 'award',
key: 'payment_for_certificate',
width: '25%',
align: 'center',
},
]
})
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ import re
import shutil import shutil
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import zipfile import zipfile
from dataclasses import fields
from xml.dom.minidom import parseString from xml.dom.minidom import parseString
import frappe import frappe
@@ -823,7 +824,6 @@ def get_count(doctype, filters):
@frappe.whitelist() @frappe.whitelist()
def get_payment_gateway_details(payment_gateway): def get_payment_gateway_details(payment_gateway):
fields = []
gateway = frappe.get_doc("Payment Gateway", payment_gateway) gateway = frappe.get_doc("Payment Gateway", payment_gateway)
if gateway.gateway_controller is None: if gateway.gateway_controller is None:
@@ -843,15 +843,30 @@ def get_payment_gateway_details(payment_gateway):
except Exception: except Exception:
frappe.throw(_("{0} Settings not found").format(payment_gateway)) frappe.throw(_("{0} Settings not found").format(payment_gateway))
gateway_fields = get_transformed_fields(meta, data)
return {
"fields": gateway_fields,
"data": data,
"doctype": doctype,
"docname": docname,
}
def get_transformed_fields(meta, data=None):
transformed_fields = []
for row in meta: for row in meta:
if row.fieldtype not in ["Column Break", "Section Break"]: if row.fieldtype not in ["Column Break", "Section Break"]:
if row.fieldtype in ["Attach", "Attach Image"]: if row.fieldtype in ["Attach", "Attach Image"]:
fieldtype = "Upload" fieldtype = "Upload"
data[row.fieldname] = get_file_info(data.get(row.fieldname)) if data and data.get(row.fieldname):
data[row.fieldname] = get_file_info(data.get(row.fieldname))
elif row.fieldtype == "Check":
fieldtype = "checkbox"
else: else:
fieldtype = row.fieldtype fieldtype = row.fieldtype
fields.append( transformed_fields.append(
{ {
"label": row.label, "label": row.label,
"name": row.fieldname, "name": row.fieldname,
@@ -859,12 +874,19 @@ def get_payment_gateway_details(payment_gateway):
} }
) )
return { return transformed_fields
"fields": fields,
"data": data,
"doctype": doctype, @frappe.whitelist()
"docname": docname, def get_new_gateway_fields(doctype):
} try:
meta = frappe.get_meta(doctype).fields
except Exception:
frappe.throw(_("{0} not found").format(doctype))
transformed_fields = get_transformed_fields(meta)
return transformed_fields
def update_course_statistics(): def update_course_statistics():

View File

@@ -147,6 +147,7 @@
"default": "0", "default": "0",
"fieldname": "payment_for_certificate", "fieldname": "payment_for_certificate",
"fieldtype": "Check", "fieldtype": "Check",
"in_standard_filter": 1,
"label": "Payment for Certificate" "label": "Payment for Certificate"
} }
], ],
@@ -161,7 +162,7 @@
"link_fieldname": "payment" "link_fieldname": "payment"
} }
], ],
"modified": "2025-08-19 10:33:15.457678", "modified": "2025-09-23 11:04:00.462274",
"modified_by": "sayali@frappe.io", "modified_by": "sayali@frappe.io",
"module": "LMS", "module": "LMS",
"name": "LMS Payment", "name": "LMS Payment",

View File

@@ -27,8 +27,7 @@
"devDependencies": { "devDependencies": {
"cypress": "^14.5.2", "cypress": "^14.5.2",
"cypress-file-upload": "^5.0.8", "cypress-file-upload": "^5.0.8",
"cypress-real-events": "^1.14.0", "cypress-real-events": "^1.14.0"
"vite-plugin-pwa": "^1.0.2"
}, },
"dependencies": { "dependencies": {
"pre-commit": "^1.2.2" "pre-commit": "^1.2.2"

3723
yarn.lock

File diff suppressed because it is too large Load Diff