refactor: layout of payment fields

This commit is contained in:
Jannat Patel
2025-05-22 21:39:49 +05:30
parent ef7180f23f
commit 5e2de35693
8 changed files with 365 additions and 61 deletions

View File

@@ -47,6 +47,8 @@ declare module 'vue' {
Discussions: typeof import('./src/components/Discussions.vue')['default']
EditCoverImage: typeof import('./src/components/Modals/EditCoverImage.vue')['default']
EditProfile: typeof import('./src/components/Modals/EditProfile.vue')['default']
EmailTemplateModal: typeof import('./src/components/Modals/EmailTemplateModal.vue')['default']
EmailTemplates: typeof import('./src/components/EmailTemplates.vue')['default']
EmptyState: typeof import('./src/components/EmptyState.vue')['default']
EvaluationModal: typeof import('./src/components/Modals/EvaluationModal.vue')['default']
Evaluators: typeof import('./src/components/Evaluators.vue')['default']

View File

@@ -18,11 +18,11 @@
</div>
<div class="overflow-y-auto">
<SettingFields :fields="fields" :data="data.data" />
<div class="flex flex-row-reverse mt-auto">
<Button variant="solid" :loading="saveSettings.loading" @click="update">
{{ __('Update') }}
</Button>
</div>
</div>
<div class="flex flex-row-reverse mt-auto">
<Button variant="solid" :loading="saveSettings.loading" @click="update">
{{ __('Update') }}
</Button>
</div>
</div>
</template>

View File

@@ -49,11 +49,11 @@
<div
v-for="(cat, index) in categories.data"
:key="cat.name"
class="pt-4 pb-1"
class="pt-2"
>
<div
v-if="editing?.name !== cat.name"
class="flex items-center justify-between group"
class="flex items-center justify-between group text-sm"
>
<div @dblclick="allowEdit(cat, index)">
{{ cat.category }}

View File

@@ -0,0 +1,136 @@
<template>
<div class="flex flex-col min-h-0 text-base">
<div class="flex items-center justify-between mb-5">
<div class="flex flex-col space-y-2">
<div class="text-xl font-semibold text-ink-gray-9">
{{ label }}
</div>
<div class="text-xs text-ink-gray-5">
{{ __(description) }}
</div>
</div>
<div class="flex items-center space-x-5">
<Button @click="() => (showForm = true)">
<template #prefix>
<Plus class="h-3 w-3 stroke-1.5" />
</template>
{{ __('New') }}
</Button>
</div>
</div>
<div v-if="emailTemplates.data?.length" class="overflow-y-scroll">
<ListView
:columns="columns"
:rows="emailTemplates.data"
row-key="name"
:options="{
showTooltip: false,
onRowClick: (row) => {
if (readOnlyMode) return
selectedTemplate = row.name
showForm = true
},
}"
>
<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 }">
<component
v-if="item.icon"
:is="item.icon"
class="h-4 w-4 stroke-1.5 ml-4"
/>
</template>
</ListHeaderItem>
</ListHeader>
<ListRows>
<ListRow :row="row" v-for="row in emailTemplates.data">
<template #default="{ column, item }">
<ListRowItem :item="row[column.key]" :align="column.align">
<div v-if="column.key == 'use_html'">
<FormControl
v-model="row[column.key]"
type="checkbox"
:disabled="true"
/>
</div>
<div v-else class="leading-5 text-sm">
{{ row[column.key] }}
</div>
</ListRowItem>
</template>
</ListRow>
</ListRows>
</ListView>
</div>
</div>
<EmailTemplateModal
v-model="showForm"
v-model:emailTemplates="emailTemplates"
:templateID="selectedTemplate"
/>
</template>
<script setup lang="ts">
import {
createListResource,
ListView,
ListHeader,
ListHeaderItem,
ListSelectBanner,
FormControl,
ListRows,
ListRow,
ListRowItem,
Button,
} from 'frappe-ui'
import { computed, ref } from 'vue'
import { Plus, Trash2 } from 'lucide-vue-next'
import EmailTemplateModal from '@/components/Modals/EmailTemplateModal.vue'
const props = defineProps({
label: {
type: String,
required: true,
},
description: {
type: String,
default: '',
},
})
const showForm = ref(false)
const readOnlyMode = window.read_only_mode
const selectedTemplate = ref(null)
const emailTemplates = createListResource({
doctype: 'Email Template',
fields: ['name', 'subject', 'use_html', 'response'],
auto: true,
orderBy: 'modified desc',
cache: 'email-templates',
})
const columns = computed(() => {
return [
{
label: 'Name',
key: 'name',
width: '15rem',
},
{
label: 'Subject',
key: 'subject',
width: '18rem',
},
{
label: 'Use HTML',
key: 'use_html',
width: '10rem',
align: 'right',
},
]
})
</script>

View File

@@ -0,0 +1,158 @@
<template>
<Dialog
v-model="show"
:options="{
title:
templateID == 'new'
? __('New Email Template')
: __('Edit Email Template'),
size: 'lg',
actions: [
{
label: __('Save'),
variant: 'solid',
onClick: ({ close }) => {
saveTemplate(close)
},
},
],
}"
>
<template #body-content>
<div class="space-y-4">
<FormControl
:label="__('Name')"
v-model="template.name"
type="text"
:required="true"
/>
<FormControl
:label="__('Subject')"
v-model="template.subject"
type="text"
:required="true"
/>
<FormControl
:label="__('Use HTML')"
v-model="template.use_html"
type="checkbox"
/>
<div>
<div class="text-xs text-ink-gray-5 mb-2">
{{ __('Response') }}
<span class="text-ink-red-3">*</span>
</div>
<TextEditor
:content="template.response"
@change="(val) => (template.response = val)"
:editable="true"
:fixedMenu="true"
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem] max-h-[18rem] overflow-y-auto"
/>
</div>
</div>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { Dialog, FormControl, TextEditor, toast } from 'frappe-ui'
import { reactive, ref, watch } from 'vue'
import { cleanError } from '@/utils'
const props = defineProps({
templateID: {
type: String,
default: 'new',
},
})
console.log(props.templateID)
const show = defineModel()
const emailTemplates = defineModel('emailTemplates')
const template = reactive({
name: '',
subject: '',
use_html: false,
response: '',
})
const saveTemplate = (close) => {
if (props.templateID == 'new') {
createNewTemplate(close)
} else {
updateTemplate(close)
}
}
const createNewTemplate = (close) => {
emailTemplates.value.insert.submit(
{
__newname: template.value.name,
...template.value,
},
{
onSuccess() {
emailTemplates.value.reload()
close()
refreshForm()
toast.success(__('Email Template created successfully'))
},
onError(err) {
close()
refreshForm()
toast.error(
cleanError(err.messages[0]) || __('Error creating email template')
)
},
}
)
}
const updateTemplate = (close) => {
console.log(show)
emailTemplates.value.setValue.submit({
name: props.templateID,
...template,
}),
{
onSuccess() {
emailTemplates.value.reload()
close()
refreshForm()
toast.success(__('Email Template updated successfully'))
},
onError(err) {
close()
refreshForm()
toast.error(
cleanError(err.messages[0]) || __('Error updating email template')
)
},
}
}
watch(
() => props.templateID,
(val) => {
if (val !== 'new') {
emailTemplates.value?.data.forEach((row) => {
if (row.name === val) {
template.name = row.name
template.subject = row.subject
template.use_html = row.use_html
template.response = row.response
}
})
}
},
{ flush: 'post' }
)
const refreshForm = () => {
template.name = ''
template.subject = ''
template.use_html = false
template.response = ''
}
</script>

View File

@@ -51,8 +51,13 @@
:label="activeTab.label"
:description="activeTab.description"
/>
<EmailTemplates
v-else-if="activeTab.label === 'Email Templates'"
:label="activeTab.label"
:description="activeTab.description"
/>
<PaymentSettings
v-else-if="activeTab.label === 'Payment Gateway'"
v-else-if="activeTab.label === 'Payment'"
:label="activeTab.label"
:description="activeTab.description"
:data="data"
@@ -86,6 +91,7 @@ import SidebarLink from '@/components/SidebarLink.vue'
import Members from '@/components/Members.vue'
import Evaluators from '@/components/Evaluators.vue'
import Categories from '@/components/Categories.vue'
import EmailTemplates from '@/components/EmailTemplates.vue'
import BrandSettings from '@/components/BrandSettings.vue'
import PaymentSettings from '@/components/PaymentSettings.vue'
@@ -122,7 +128,7 @@ const tabsStructure = computed(() => {
label: 'Enable Learning Paths',
name: 'enable_learning_paths',
description:
'This will enforce students to go through programs assigned to them in the correct order.',
'This will ensure students follow the assigned programs in order.',
type: 'checkbox',
},
{
@@ -139,11 +145,26 @@ const tabsStructure = computed(() => {
'If enabled, it sends google calendar invite to the student for evaluations.',
type: 'checkbox',
},
{
type: 'Column Break',
},
{
label: 'Batch Confirmation Template',
name: 'batch_confirmation_template',
doctype: 'Email Template',
type: 'Link',
},
{
label: 'Certification Template',
name: 'certification_template',
doctype: 'Email Template',
type: 'Link',
},
{
label: 'Unsplash Access Key',
name: 'unsplash_access_key',
description:
'Optional. If this is set, students can pick a cover image from the unsplash library for their profile page. https://unsplash.com/documentation#getting-started.',
'Allows users to pick a profile cover image from Unsplash. https://unsplash.com/documentation#getting-started.',
type: 'password',
},
],
@@ -155,11 +176,17 @@ const tabsStructure = computed(() => {
hideLabel: true,
items: [
{
label: 'Payment Gateway',
label: 'Payment',
icon: 'DollarSign',
description:
'Configure the payment gateway and other payment related settings',
fields: [
{
label: 'Default Currency',
name: 'default_currency',
type: 'Link',
doctype: 'Currency',
},
{
label: 'Payment Gateway',
name: 'payment_gateway',
@@ -167,10 +194,7 @@ const tabsStructure = computed(() => {
doctype: 'Payment Gateway',
},
{
label: 'Default Currency',
name: 'default_currency',
type: 'Link',
doctype: 'Currency',
type: 'Column Break',
},
{
label: 'Apply GST for India',
@@ -210,6 +234,11 @@ const tabsStructure = computed(() => {
description: 'Double click to edit the category',
icon: 'Network',
},
{
label: 'Email Templates',
description: 'Manage the email templates for your learning system',
icon: 'MailPlus',
},
],
},
{
@@ -235,28 +264,6 @@ const tabsStructure = computed(() => {
name: 'favicon',
type: 'Upload',
},
{
label: 'Footer Logo',
name: 'footer_logo',
type: 'Upload',
},
{
label: 'Address',
name: 'address',
type: 'textarea',
rows: 2,
},
{
label: 'Footer "Powered By"',
name: 'footer_powered',
type: 'textarea',
rows: 4,
},
{
label: 'Copyright',
name: 'copyright',
type: 'text',
},
],
},
{
@@ -299,24 +306,6 @@ const tabsStructure = computed(() => {
},
],
},
{
label: 'Email Templates',
icon: 'MailPlus',
fields: [
{
label: 'Batch Confirmation Template',
name: 'batch_confirmation_template',
doctype: 'Email Template',
type: 'Link',
},
{
label: 'Certification Template',
name: 'certification_template',
doctype: 'Email Template',
type: 'Link',
},
],
},
{
label: 'Signup',
icon: 'LogIn',

View File

@@ -12,13 +12,13 @@
/> -->
</div>
<div class="overflow-y-scroll">
<div class="flex space-x-4">
<SettingFields :fields="fields" :data="data.doc" class="w-1/2" />
<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="w-1/2"
class="pt-5 my-0"
/>
</div>
</div>
@@ -60,9 +60,27 @@ const paymentGateway = createResource({
payment_gateway: props.data.doc.payment_gateway,
}
},
transform(data) {
arrangeFields(data.fields)
},
auto: true,
})
const arrangeFields = (fields) => {
fields = data.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) {

View File

@@ -6,7 +6,7 @@
<div v-for="(column, index) in columns" :key="index">
<div
class="flex flex-col space-y-5"
:class="columns.length > 1 ? 'w-72' : 'w-full'"
:class="columns.length > 1 ? 'w-[21rem]' : 'w-1/2'"
>
<div v-for="field in column">
<Link
@@ -14,6 +14,7 @@
v-model="data[field.name]"
:doctype="field.doctype"
:label="__(field.label)"
:description="__(field.description)"
/>
<div v-else-if="field.type == 'Code'">
@@ -54,11 +55,11 @@
<div v-else>
<div class="flex items-center text-sm space-x-2">
<div
class="flex items-center justify-center rounded border border-outline-gray-modals bg-white w-[10rem] py-2"
class="flex items-center justify-center rounded border border-outline-gray-1 bg-surface-gray-2 px-20 py-5"
>
<img
:src="data[field.name]?.file_url || data[field.name]"
class="w-[80%] rounded"
class="size-6 rounded"
/>
</div>
<div class="flex flex-col flex-wrap">