feat: add new member
This commit is contained in:
@@ -1,55 +1,83 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="text-base p-4">
|
<div class="text-base p-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div class="font-semibold mb-1">
|
<div class="font-semibold mb-1">
|
||||||
{{ __(label) }}
|
{{ __(label) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-gray-600">
|
<div class="text-xs text-gray-600">
|
||||||
{{ __(description) }}
|
{{ __(description) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<FormControl v-model="search" :placeholder="__('Search')" type="text" :debounce="300"/>
|
<div class="flex item-center space-x-2">
|
||||||
</div>
|
<FormControl
|
||||||
<div class="my-4">
|
v-model="search"
|
||||||
|
:placeholder="__('Search')"
|
||||||
|
type="text"
|
||||||
|
:debounce="300"
|
||||||
|
/>
|
||||||
|
<Button @click="() => (showForm = true)">
|
||||||
|
<template #icon>
|
||||||
|
<component :is="icons['Plus']" class="h-3 w-3 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="my-4">
|
||||||
|
<!-- Form to add new member -->
|
||||||
|
<div v-if="showForm" class="flex items-center space-x-2 mb-4">
|
||||||
|
<FormControl
|
||||||
|
v-model="member.email"
|
||||||
|
:placeholder="__('Email')"
|
||||||
|
type="email"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="member.first_name"
|
||||||
|
:placeholder="__('First Name')"
|
||||||
|
type="test"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
<Button @click="addMember()" variant="subtle">
|
||||||
|
{{ __('Add') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Form to add new member -->
|
<!-- Member list -->
|
||||||
<div v-if="showForm" class="flex items-center space-x-2 mb-4">
|
<div
|
||||||
<FormControl v-model="member.email" :placeholder="__('Email')" type="email" class="w-full"/>
|
v-for="member in memberList"
|
||||||
<FormControl v-model="member.first_name" :placeholder="__('First Name')" type="test" class="w-full"/>
|
class="grid grid-cols-5 grid-flow-row py-2 cursor-pointer"
|
||||||
<Button @click="addMember" variant="subtle">
|
>
|
||||||
{{ __('Add') }}
|
<div
|
||||||
</Button>
|
@click="openProfile(member.username)"
|
||||||
</div>
|
class="flex items-center space-x-2 col-span-2"
|
||||||
|
>
|
||||||
<!-- Member list -->
|
<Avatar
|
||||||
<div v-for="member in memberList" class="grid grid-cols-5 grid-flow-row py-2 cursor-pointer">
|
:image="member.user_image"
|
||||||
<div @click="openProfile(member.username)" class="flex items-center space-x-2 col-span-2">
|
:label="member.full_name"
|
||||||
<Avatar :image="member.user_image" :label="member.full_name" size="sm" />
|
size="sm"
|
||||||
<div>
|
/>
|
||||||
{{ member.full_name }}
|
<div>
|
||||||
</div>
|
{{ member.full_name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-700 col-span-2">
|
</div>
|
||||||
{{ member.name }}
|
<div class="text-sm text-gray-700 col-span-2">
|
||||||
</div>
|
{{ member.name }}
|
||||||
<div class="text-sm text-gray-700 justify-self-end">
|
</div>
|
||||||
{{ getRole(member.role) }}
|
<div class="text-sm text-gray-700 justify-self-end">
|
||||||
</div>
|
{{ getRole(member.role) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="hasNextPage" class="flex justify-center">
|
</div>
|
||||||
<Button variant="solid" @click="members.reload()">
|
<div v-if="hasNextPage" class="flex justify-center">
|
||||||
<template #prefix>
|
<Button variant="solid" @click="members.reload()">
|
||||||
<component
|
<template #prefix>
|
||||||
:is="icons['RefreshCw']"
|
<component :is="icons['RefreshCw']" class="h-3 w-3 stroke-1.5" />
|
||||||
class="h-3 w-3 stroke-1.5"
|
</template>
|
||||||
/>
|
{{ __('Load More') }}
|
||||||
</template>
|
</Button>
|
||||||
{{ __('Load More') }}
|
</div>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { createResource, Avatar, Button, FormControl } from 'frappe-ui'
|
import { createResource, Avatar, Button, FormControl } from 'frappe-ui'
|
||||||
@@ -58,72 +86,98 @@ import { useRouter } from 'vue-router'
|
|||||||
import { ref, watch, reactive } from 'vue'
|
import { ref, watch, reactive } from 'vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const show = defineModel("show")
|
const show = defineModel('show')
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const start = ref(0)
|
const start = ref(0)
|
||||||
const memberList = ref([])
|
const memberList = ref([])
|
||||||
const hasNextPage = ref(false)
|
const hasNextPage = ref(false)
|
||||||
const showForm = ref(true)
|
const showForm = ref(false)
|
||||||
|
|
||||||
const member = reactive({
|
const member = reactive({
|
||||||
email: '',
|
email: '',
|
||||||
first_name: '',
|
first_name: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
show: {
|
show: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const members = createResource({
|
const members = createResource({
|
||||||
url: "lms.lms.api.get_members",
|
url: 'lms.lms.api.get_members',
|
||||||
makeParams: () => {
|
makeParams: () => {
|
||||||
return {
|
return {
|
||||||
search: search.value,
|
search: search.value,
|
||||||
start: start.value
|
start: start.value,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
memberList.value = memberList.value.concat(data)
|
memberList.value = memberList.value.concat(data)
|
||||||
start.value = start.value + 20
|
start.value = start.value + 20
|
||||||
hasNextPage.value = data.length === 20
|
hasNextPage.value = data.length === 20
|
||||||
},
|
},
|
||||||
auto: true,
|
auto: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const openProfile = (username) => {
|
const openProfile = (username) => {
|
||||||
show.value = false
|
show.value = false
|
||||||
router.push({
|
router.push({
|
||||||
name: 'Profile',
|
name: 'Profile',
|
||||||
params: {
|
params: {
|
||||||
username: username,
|
username: username,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newMember = createResource({
|
||||||
|
url: 'frappe.client.insert',
|
||||||
|
makeParams(values) {
|
||||||
|
return {
|
||||||
|
doc: {
|
||||||
|
doctype: 'User',
|
||||||
|
first_name: member.first_name,
|
||||||
|
email: member.email,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
auto: false,
|
||||||
|
onSuccess(data) {
|
||||||
|
show.value = false
|
||||||
|
router.push({
|
||||||
|
name: 'Profile',
|
||||||
|
params: {
|
||||||
|
username: data.username,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const addMember = () => {
|
||||||
|
newMember.reload()
|
||||||
|
}
|
||||||
|
|
||||||
watch(search, () => {
|
watch(search, () => {
|
||||||
memberList.value = []
|
memberList.value = []
|
||||||
start.value = 0
|
start.value = 0
|
||||||
members.reload()
|
members.reload()
|
||||||
})
|
})
|
||||||
|
|
||||||
const getRole = (role) => {
|
const getRole = (role) => {
|
||||||
const map = {
|
const map = {
|
||||||
'LMS Student': 'Student',
|
'LMS Student': 'Student',
|
||||||
'Course Creator': 'Instructor',
|
'Course Creator': 'Instructor',
|
||||||
'Moderator': 'Moderator',
|
Moderator: 'Moderator',
|
||||||
'Batch Evaluator': 'Evaluator'
|
'Batch Evaluator': 'Evaluator',
|
||||||
}
|
}
|
||||||
return map[role]
|
return map[role]
|
||||||
}
|
}
|
||||||
|
</script>
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -28,13 +28,22 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="activeTab && data.doc" class="flex flex-1 flex-col overflow-y-auto">
|
<div
|
||||||
<Members v-if="activeTab.label === 'Members'" :label="activeTab.label" :description="activeTab.description" v-model:show="show"/>
|
v-if="activeTab && data.doc"
|
||||||
|
class="flex flex-1 flex-col overflow-y-auto"
|
||||||
|
>
|
||||||
|
<Members
|
||||||
|
v-if="activeTab.label === 'Members'"
|
||||||
|
:label="activeTab.label"
|
||||||
|
:description="activeTab.description"
|
||||||
|
v-model:show="show"
|
||||||
|
/>
|
||||||
<SettingDetails
|
<SettingDetails
|
||||||
v-else
|
v-else
|
||||||
:fields="activeTab.fields"
|
:fields="activeTab.fields"
|
||||||
:data="data"
|
|
||||||
:label="activeTab.label"
|
:label="activeTab.label"
|
||||||
|
:description="activeTab.description"
|
||||||
|
:data="data"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -66,9 +75,16 @@ const tabs = computed(() => {
|
|||||||
label: 'Settings',
|
label: 'Settings',
|
||||||
hideLabel: true,
|
hideLabel: true,
|
||||||
items: [
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Members',
|
||||||
|
description: 'Manage the members of your learning system',
|
||||||
|
icon: 'UserRoundPlus',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Payment Gateway',
|
label: 'Payment Gateway',
|
||||||
icon: 'DollarSign',
|
icon: 'DollarSign',
|
||||||
|
description:
|
||||||
|
'Configure the payment gateway and other payment related settings',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: 'Razorpay Key',
|
label: 'Razorpay Key',
|
||||||
@@ -115,6 +131,7 @@ const tabs = computed(() => {
|
|||||||
{
|
{
|
||||||
label: 'Sidebar',
|
label: 'Sidebar',
|
||||||
icon: 'PanelLeftIcon',
|
icon: 'PanelLeftIcon',
|
||||||
|
description: 'Customize the sidebar as per your needs',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: 'Courses',
|
label: 'Courses',
|
||||||
@@ -160,6 +177,7 @@ const tabs = computed(() => {
|
|||||||
{
|
{
|
||||||
label: 'Email Templates',
|
label: 'Email Templates',
|
||||||
icon: 'MailPlus',
|
icon: 'MailPlus',
|
||||||
|
description: 'Create email templates with the content you want',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: 'Batch Confirmation Template',
|
label: 'Batch Confirmation Template',
|
||||||
@@ -190,9 +208,11 @@ const tabs = computed(() => {
|
|||||||
{
|
{
|
||||||
label: 'Signup',
|
label: 'Signup',
|
||||||
icon: 'LogIn',
|
icon: 'LogIn',
|
||||||
|
description:
|
||||||
|
'Customize the signup page to inform users about your terms and policies',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: 'Show terms of use on signup page',
|
label: 'Show terms of use on signup',
|
||||||
name: 'terms_of_use',
|
name: 'terms_of_use',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
},
|
},
|
||||||
@@ -203,15 +223,7 @@ const tabs = computed(() => {
|
|||||||
doctype: 'Web Page',
|
doctype: 'Web Page',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Ask user category during signup',
|
label: 'Show privacy policy on signup',
|
||||||
name: 'user_category',
|
|
||||||
type: 'checkbox',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'Column Break',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Show privacy policy on signup page',
|
|
||||||
name: 'privacy_policy',
|
name: 'privacy_policy',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
},
|
},
|
||||||
@@ -225,7 +237,7 @@ const tabs = computed(() => {
|
|||||||
type: 'Column Break',
|
type: 'Column Break',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Show cookie policy on signup page',
|
label: 'Show cookie policy on signup',
|
||||||
name: 'cookie_policy',
|
name: 'cookie_policy',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
},
|
},
|
||||||
@@ -235,21 +247,15 @@ const tabs = computed(() => {
|
|||||||
type: 'Link',
|
type: 'Link',
|
||||||
doctype: 'Web Page',
|
doctype: 'Web Page',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Ask user category during signup',
|
||||||
|
name: 'user_category',
|
||||||
|
type: 'checkbox',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: 'Settings',
|
|
||||||
hideLabel: true,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
label: 'Members',
|
|
||||||
description: 'Manage the members of your learning system',
|
|
||||||
icon: "UserRoundPlus",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
return _tabs.map((tab) => {
|
return _tabs.map((tab) => {
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col justify-between h-full p-8">
|
<div class="flex flex-col justify-between h-full p-4">
|
||||||
<div class="flex space-x-8">
|
<div>
|
||||||
|
<div class="font-semibold mb-1">
|
||||||
|
{{ __(label) }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-600">
|
||||||
|
{{ __(description) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex space-x-8 my-5">
|
||||||
<div v-for="(column, index) in columns" :key="index">
|
<div v-for="(column, index) in columns" :key="index">
|
||||||
<div class="flex flex-col space-y-4 w-60">
|
<div class="flex flex-col space-y-4 w-60">
|
||||||
<div v-for="field in column">
|
<div v-for="field in column">
|
||||||
@@ -43,6 +51,13 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const columns = computed(() => {
|
const columns = computed(() => {
|
||||||
|
|||||||
@@ -563,19 +563,22 @@ def get_categories(doctype, filters):
|
|||||||
|
|
||||||
return categoryOptions
|
return categoryOptions
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_members(start=0, search=""):
|
def get_members(start=0, search=""):
|
||||||
filters = {
|
filters = {"enabled": 1, "name": ["not in", ["Administrator", "Guest"]]}
|
||||||
"enabled": 1,
|
|
||||||
"name": ["not in", ["Administrator", "Guest"]]
|
|
||||||
}
|
|
||||||
|
|
||||||
if search:
|
if search:
|
||||||
filters["full_name"] = ["like", f"%{search}%"]
|
filters["full_name"] = ["like", f"%{search}%"]
|
||||||
|
|
||||||
print(filters)
|
print(filters)
|
||||||
members = frappe.get_all("User", filters=filters, fields=["name", "full_name", "user_image", "username"],
|
members = frappe.get_all(
|
||||||
page_length=20, start=start)
|
"User",
|
||||||
|
filters=filters,
|
||||||
|
fields=["name", "full_name", "user_image", "username"],
|
||||||
|
page_length=20,
|
||||||
|
start=start,
|
||||||
|
)
|
||||||
|
|
||||||
for member in members:
|
for member in members:
|
||||||
roles = frappe.get_roles(member.name)
|
roles = frappe.get_roles(member.name)
|
||||||
@@ -589,6 +592,3 @@ def get_members(start=0, search=""):
|
|||||||
member.role = "LMS Student"
|
member.role = "LMS Student"
|
||||||
|
|
||||||
return members
|
return members
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -16,6 +16,9 @@ class CustomUser(User):
|
|||||||
super().validate()
|
super().validate()
|
||||||
self.validate_username_duplicates()
|
self.validate_username_duplicates()
|
||||||
|
|
||||||
|
def after_insert(self):
|
||||||
|
self.add_roles("LMS Student")
|
||||||
|
|
||||||
def validate_username_duplicates(self):
|
def validate_username_duplicates(self):
|
||||||
while not self.username or self.username_exists():
|
while not self.username or self.username_exists():
|
||||||
self.username = append_number_if_name_exists(
|
self.username = append_number_if_name_exists(
|
||||||
|
|||||||
Reference in New Issue
Block a user