Refactor Admin Panel with more features (#384)
Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
@@ -146,6 +146,14 @@ export default {
|
||||
"ring-offset": "focus:ring-offset-gray-200",
|
||||
text: "text-gray-500 hover:text-white",
|
||||
}
|
||||
} else if (this.color === "outline-red") {
|
||||
return {
|
||||
main: "bg-white border border-gray-300 text-red-700",
|
||||
hover: "hover:bg-gray-100 hover:text-red-500",
|
||||
ring: "focus:ring-gray-500",
|
||||
"ring-offset": "focus:ring-offset-gray-200",
|
||||
text: "text-gray-500",
|
||||
}
|
||||
} else if (this.color === "red") {
|
||||
return {
|
||||
main: "bg-red-600",
|
||||
|
||||
23
client/components/pages/admin/AdminCard.vue
Normal file
23
client/components/pages/admin/AdminCard.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div class="w-full bg-white border border-gray-200 rounded-lg shadow flex flex-col">
|
||||
<div class="w-full flex border-b px-4 py-2">
|
||||
<Icon
|
||||
:name="props.icon"
|
||||
class="w-6 h-6 text-nt-blue"
|
||||
/>
|
||||
<h3 class="text-md font-semibold ml-2">
|
||||
{{ props.title }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="p-4 flex-grow">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
title: { type: String, required: true },
|
||||
icon: { type: String, required: true }
|
||||
})
|
||||
</script>
|
||||
71
client/components/pages/admin/CancelSubscription.vue
Normal file
71
client/components/pages/admin/CancelSubscription.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<AdminCard
|
||||
title="Cancel subscription"
|
||||
icon="heroicons:trash-16-solid"
|
||||
>
|
||||
<form
|
||||
class="space-y-6 flex flex-col h-full justify-between"
|
||||
@submit.prevent="askCancel"
|
||||
>
|
||||
<p class="text-xs text-gray-500">
|
||||
Ideally customers should cancel subscription themselves via the UI. If
|
||||
you cancel the subscription for them, please provide a reason.
|
||||
</p>
|
||||
<div>
|
||||
<text-input
|
||||
name="cancellation_reason"
|
||||
:form="form"
|
||||
label="Cancellation reason"
|
||||
native-type="reason"
|
||||
:required="true"
|
||||
help="Cancellation reason"
|
||||
/>
|
||||
|
||||
<v-button
|
||||
:loading="loading"
|
||||
type="success"
|
||||
class="w-full"
|
||||
color="outline-red"
|
||||
>
|
||||
<Icon
|
||||
class="inline w-4 h-4 text-red-600"
|
||||
name="heroicons:exclamation-triangle-16-solid"
|
||||
/>
|
||||
Cancel subscription now
|
||||
</v-button>
|
||||
</div>
|
||||
</form>
|
||||
</AdminCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
user: { type: Object, required: true }
|
||||
})
|
||||
|
||||
const alert = useAlert()
|
||||
let loading = ref(false)
|
||||
const form = useForm({
|
||||
user_id: props.user.id,
|
||||
cancellation_reason: ''
|
||||
})
|
||||
|
||||
const askCancel = () => {
|
||||
alert.confirm('Are you sure? This will cancel the subscription for this user.', cancelSubscription)
|
||||
}
|
||||
|
||||
const cancelSubscription = () => {
|
||||
loading = true
|
||||
form
|
||||
.patch('/moderator/cancellation-subscription')
|
||||
.then(async (data) => {
|
||||
loading = false
|
||||
alert.success(data.message)
|
||||
})
|
||||
.catch((error) => {
|
||||
alert.error(error.data.message)
|
||||
loading = false
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
52
client/components/pages/admin/DiscountOnSubscription.vue
Normal file
52
client/components/pages/admin/DiscountOnSubscription.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<AdminCard
|
||||
title="Apply discount"
|
||||
icon="heroicons:tag-20-solid"
|
||||
>
|
||||
<form
|
||||
class="space-y-6 flex flex-col justify-between"
|
||||
@submit.prevent="applyDiscount"
|
||||
>
|
||||
<p class="text-xs text-gray-500">
|
||||
This is only for students, academics and NGOs. Make sure to verify
|
||||
their status before applying discount (student/university email, NGO
|
||||
website, proof of non-profit, etc). They need to create their
|
||||
subscriptions before you can apply the 40% discount.
|
||||
</p>
|
||||
<v-button
|
||||
:loading="loading"
|
||||
type="success"
|
||||
class="w-full"
|
||||
color="white"
|
||||
>
|
||||
Apply Discount
|
||||
</v-button>
|
||||
</form>
|
||||
</AdminCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
user: { type: Object, required: true }
|
||||
})
|
||||
|
||||
let loading = ref(false)
|
||||
const form = useForm({
|
||||
user_id: props.user.id
|
||||
})
|
||||
|
||||
const applyDiscount = () => {
|
||||
loading = true
|
||||
form
|
||||
.patch('/moderator/apply-discount')
|
||||
.then(async (data) => {
|
||||
loading = false
|
||||
useAlert().success(data.message)
|
||||
})
|
||||
.catch((error) => {
|
||||
useAlert().error(error.data.message)
|
||||
loading = false
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
63
client/components/pages/admin/ExtendTrial.vue
Normal file
63
client/components/pages/admin/ExtendTrial.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<AdminCard
|
||||
title="Extend trial"
|
||||
icon="heroicons:calendar-16-solid"
|
||||
>
|
||||
<form
|
||||
class="space-y-6 flex flex-col justify-between"
|
||||
@submit.prevent="extendTrial"
|
||||
>
|
||||
<p class="text-xs text-gray-500">
|
||||
You can extend the trial of subscribers that are still in the trial
|
||||
period. Usually, you should not offer more than 7 days of trial, but
|
||||
you can add up to 14 days if needed.
|
||||
</p>
|
||||
<div>
|
||||
<text-input
|
||||
name="number_of_day"
|
||||
:form="form"
|
||||
label="Number of days"
|
||||
native-type="day"
|
||||
:required="true"
|
||||
help="Number Of Days"
|
||||
placeholder="7"
|
||||
/>
|
||||
<v-button
|
||||
:loading="loading"
|
||||
type="success"
|
||||
class="w-full"
|
||||
color="white"
|
||||
>
|
||||
Apply Extend Trial
|
||||
</v-button>
|
||||
</div>
|
||||
</form>
|
||||
</AdminCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
user: { type: Object, required: true }
|
||||
})
|
||||
|
||||
let loading = ref(false)
|
||||
const form = useForm({
|
||||
user_id: props.user.id,
|
||||
number_of_day: ''
|
||||
})
|
||||
|
||||
const extendTrial = () => {
|
||||
loading = true
|
||||
form
|
||||
.patch('/moderator/extend-trial')
|
||||
.then(async (data) => {
|
||||
loading = false
|
||||
useAlert().success(data.message)
|
||||
})
|
||||
.catch((error) => {
|
||||
useAlert().error(error.data.message)
|
||||
loading = false
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
53
client/components/pages/admin/ImpersonateUser.vue
Normal file
53
client/components/pages/admin/ImpersonateUser.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<UButton
|
||||
size="sm"
|
||||
color="white"
|
||||
icon="i-heroicons-eye-16-solid"
|
||||
@click="impersonate"
|
||||
>
|
||||
Impersonate User
|
||||
</UButton>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
user: { type: Object, required: true }
|
||||
})
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const formsStore = useFormsStore()
|
||||
const workspacesStore = useWorkspacesStore()
|
||||
|
||||
let loading = ref(false)
|
||||
|
||||
const impersonate = () => {
|
||||
loading = true
|
||||
authStore.startImpersonating()
|
||||
opnFetch(`/moderator/impersonate/${props.user.id}`).then(async (data) => {
|
||||
loading = false
|
||||
|
||||
// Save the token.
|
||||
authStore.setToken(data.token, false)
|
||||
|
||||
// Fetch the user.
|
||||
const userData = await opnFetch('user')
|
||||
authStore.setUser(userData)
|
||||
|
||||
// Redirect to the dashboard.
|
||||
formsStore.set([])
|
||||
workspacesStore.set([])
|
||||
|
||||
const workspaces = await fetchAllWorkspaces()
|
||||
workspacesStore.set(workspaces.data.value)
|
||||
formsStore.startLoading()
|
||||
formsStore.loadAll(workspacesStore.currentId)
|
||||
|
||||
useAlert().success(`Impersonating ${authStore.user.name}`)
|
||||
useRouter().push({ name: 'home' })
|
||||
})
|
||||
.catch((error) => {
|
||||
useAlert().error(error.data.message)
|
||||
loading = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user