A227b new admin features (#388)

* wip: adminfeatures

* wip: admin features

* wip: admin features, password reset, deleted forms

* fix linting

* fix pinting

* fix bug

* fix bug

* remove testing

* fixes on deleted forms, removed unused functions

* fix pint

* admin  feature updated

* fix linting warning

* fix workspace subscription tag

* Final touches

* Clean console.log

* Added admin logs

* Fix linting

---------

Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
Favour Olayinka
2024-05-06 13:12:05 +01:00
committed by GitHub
parent 6d50bba76b
commit 80cdce9502
18 changed files with 831 additions and 34 deletions

View File

@@ -1,17 +1,24 @@
<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>
<collapse
v-model="show"
class="p-2 w-full"
>
<template #title>
<div class="w-full flex 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>
</template>
<div class="p-4 flex-grow">
<slot />
</div>
</collapse>
</div>
</template>
@@ -20,4 +27,6 @@ const props = defineProps({
title: { type: String, required: true },
icon: { type: String, required: true }
})
const show = ref(true)
</script>

View File

@@ -0,0 +1,84 @@
<template>
<AdminCard
v-if="props.user.stripe_id"
title="Billing email"
icon="heroicons:envelope-16-solid"
>
<p class="text-xs text-gray-500">
You can update the billing email of the subscriber.
</p>
<div
v-if="loading"
class="text-gray-600 dark:text-gray-400"
>
<Loader class="h-6 w-6 mx-auto m-10" />
</div>
<form
v-else
class="mt-6 space-y-6 flex flex-col justify-between"
@submit.prevent="updateUserBillingEmail"
>
<div>
<text-input
name="billing_email"
:form="form"
label="Billing email"
native-type="email"
:required="true"
help="Billing email"
placeholder="Billing email"
:disabled="!userCreated"
/>
<v-button
:loading="loading"
type="success"
class="w-full"
color="white"
:disabled="!userCreated"
>
Update billing email
</v-button>
</div>
</form>
</AdminCard>
</template>
<script setup>
const props = defineProps({
user: { type: Object, required: true }
})
const loadingBillingEmail = ref(false)
const loading = ref(false)
const userCreated = ref(false)
const form = useForm({
billing_email: '',
user_id: props.user.id
})
onMounted(() => {
if (!props.user.stripe_id) return
loadingBillingEmail.value = true
opnFetch("/moderator/billing/" + props.user.id + "/email",).then(data => {
loadingBillingEmail.value = false
userCreated.value = true
form.billing_email = data.billing_email
}).catch(error => {
loadingBillingEmail.value = false
userCreated.value = false
})
})
const updateUserBillingEmail = () => {
loading.value = true
form.patch("/moderator/billing/email")
.then(async (data) => {
loading.value = false
useAlert().success(data.message)
})
.catch((error) => {
useAlert().error(error.data.message)
loading.value = false
})
}
</script>

View File

@@ -1,5 +1,6 @@
<template>
<AdminCard
v-if="props.user.stripe_id"
title="Cancel subscription"
icon="heroicons:trash-16-solid"
>
@@ -55,6 +56,7 @@ const askCancel = () => {
}
const cancelSubscription = () => {
if (!props.user.stripe_id) return
loading = true
form
.patch('/moderator/cancellation-subscription')

View File

@@ -0,0 +1,113 @@
<template>
<AdminCard
title="Deleted forms"
icon="heroicons:trash-16-solid"
>
<UTable
:loading="loading"
:loading-state="{ icon: 'i-heroicons-arrow-path-20-solid', label: 'Loading...' }"
:progress="{ color: 'primary', animation: 'carousel' }"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'No items.' }"
:columns="columns"
:rows="rows"
class="-mx-6"
>
<template #actions-data="{ row }">
<VButton
:loading="restoringForm"
native-type="button"
size="small"
color="white"
@click.prevent="restoreForm(row.slug)"
>
Restore
</VButton>
</template>
</UTable>
<div
v-if="forms?.length > pageCount"
class="flex justify-end px-3 py-3.5 border-t border-gray-200 dark:border-gray-700">
<UPagination
v-model="page"
:page-count="pageCount"
:total="forms.length"
/>
</div>
</AdminCard>
</template>
<script setup>
const props = defineProps({
user: { type: Object, required: true }
})
const loading = ref(true)
const restoringForm = ref(false)
const forms = ref([])
const page = ref(1)
const pageCount = 5
const rows = computed(() => {
return forms.value.slice((page.value - 1) * pageCount, (page.value) * pageCount)
})
onMounted(() => {
getDeletedForms()
})
const getDeletedForms = () => {
loading.value = true
opnFetch("/moderator/forms/" + props.user.id + "/deleted-forms",).then(data => {
loading.value = false
forms.value = data.forms
}).catch(error => {
useAlert().error(error.message)
loading.value = false
})
}
const restoreForm = (slug) => {
return useAlert().confirm(
"Are you sure you want to restore this form?",
() => {
restoringForm.value = true
opnFetch("/moderator/forms/" + slug + "/restore", {
method: 'PATCH',
}).then(data => {
restoringForm.value = false
useAlert().success(data.message)
getDeletedForms()
}).catch(error => {
restoringForm.value = false
useAlert().error(error.data.message)
})
})
}
const columns = [{
key: 'id',
label: 'ID'
}, {
key: 'slug',
label: 'Slug',
sortable: true
}, {
key: 'title',
label: 'Title',
sortable: true
}, {
key: 'created_by',
label: 'Created by',
sortable: true
}, {
key: 'deleted_at',
label: 'Deleted at',
sortable: true,
}, {
key: 'actions',
label: 'Restore',
sortable: false,
}]
</script>

View File

@@ -1,5 +1,6 @@
<template>
<AdminCard
v-if="props.user.stripe_id"
title="Apply discount"
icon="heroicons:tag-20-solid"
>
@@ -36,6 +37,7 @@ const form = useForm({
})
const applyDiscount = () => {
if (!props.user.stripe_id) return
loading = true
form
.patch('/moderator/apply-discount')

View File

@@ -1,5 +1,6 @@
<template>
<AdminCard
v-if="props.user.stripe_id"
title="Extend trial"
icon="heroicons:calendar-16-solid"
>
@@ -47,6 +48,7 @@ const form = useForm({
})
const extendTrial = () => {
if (!props.user.stripe_id) return
loading = true
form
.patch('/moderator/extend-trial')

View File

@@ -0,0 +1,40 @@
<template>
<UButton
size="sm"
color="white"
icon="i-heroicons-key-16-solid"
:loading="loading"
@click="resetPassword"
>
Reset Password
</UButton>
</template>
<script setup>
const props = defineProps({
user: { type: Object, required: true }
})
const loading = ref(false)
const form = useForm({
user_id: props.user.id
})
const resetPassword = ()=>{
return useAlert().confirm(
"Are you sure you want to send a password reset email?",
() => {
loading.value = true
form
.patch('/moderator/send-password-reset-email')
.then(async (data) => {
loading.value = false
useAlert().success(data.message)
})
.catch((error) => {
useAlert().error(error.data.message)
loading.value = false
})
})
}
</script>

View File

@@ -0,0 +1,106 @@
<template>
<AdminCard
v-if="props.user.stripe_id"
title="Payments"
icon="heroicons:currency-dollar-16-solid"
>
<UTable
:loading="loading"
:loading-state="{ icon: 'i-heroicons-arrow-path-20-solid', label: 'Loading...' }"
:progress="{ color: 'primary', animation: 'carousel' }"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'No items.' }"
:columns="columns"
:rows="rows"
class="-mx-6"
>
<template #id-data="{ row }">
<a
:href="'https://dashboard.stripe.com/invoices/' + row.id"
target="_blank"
class="text-xs select-all bg-purple-50 border-purple-200 text-purple-500 rounded-md px-2 py-1 border"
>
<Icon
name="bx:bxl-stripe"
class="h-4 w-4 inline-block"
/>
{{ row.id }}
</a>
</template>
<template #amount_paid-data="{ row }">
<span class="font-semibold">${{ parseFloat(row.amount_paid / 100).toFixed(2) }}</span>
</template>
<template #status-data="{ row }">
<span
class="text-xs select-all rounded-md px-2 py-1 border"
:class="row.status == 'paid' ? 'bg-green-50 border-green-200 text-green-500' : 'bg-yellow-50 border-yellow-200 text-yellow-500'"
>
{{ row.status }}
</span>
</template>
</UTable>
<div
v-if="payments?.length > pageCount"
class="flex justify-end px-3 py-3.5 border-t border-gray-200 dark:border-gray-700"
>
<UPagination
v-model="page"
:page-count="pageCount"
:total="payments?.length"
/>
</div>
</AdminCard>
</template>
<script setup>
const props = defineProps({
user: {type: Object, required: true}
})
const loading = ref(true)
const payments = ref([])
const page = ref(1)
const pageCount = 5
const rows = computed(() => {
return payments.value.slice((page.value - 1) * pageCount, (page.value) * pageCount)
})
onMounted(() => {
getPayments()
})
const getPayments = () => {
if (!props.user.stripe_id) return
loading.value = true
opnFetch("/moderator/billing/" + props.user.id + "/payments",).then(data => {
loading.value = false
payments.value = data.payments
}).catch(error => {
useAlert().error(error.data.message)
loading.value = false
})
}
const columns = [{
key: 'id',
label: 'ID'
}, {
key: 'amount_paid',
label: 'Amount paid',
sortable: true
}, {
key: 'name',
label: 'Name',
sortable: true
}, {
key: 'status',
label: 'Status',
sortable: true
}, {
key: 'creation_date',
label: 'Creation date',
sortable: true
}]
</script>

View File

@@ -0,0 +1,107 @@
<template>
<AdminCard
v-if="props.user.stripe_id"
title="Subscriptions"
icon="heroicons:credit-card-16-solid"
>
<UTable
:loading="loading"
:loading-state="{ icon: 'i-heroicons-arrow-path-20-solid', label: 'Loading...' }"
:progress="{ color: 'primary', animation: 'carousel' }"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'No items.' }"
:columns="columns"
:rows="rows"
class="-mx-6"
>
<template #stripe_id-data="{ row }">
<a
:href="'https://dashboard.stripe.com/subscriptions/' + row.stripe_id"
target="_blank"
class="text-xs select-all bg-purple-50 border-purple-200 text-purple-500 rounded-md px-2 py-1 border"
>
<Icon
name="bx:bxl-stripe"
class="h-4 w-4 inline-block"
/>
{{ row.stripe_id }}
</a>
</template>
<template #status-data="{ row }">
<span
class="text-xs select-all rounded-md px-2 py-1 border"
:class="row.status == 'active' ? 'bg-green-50 border-green-200 text-green-500' : 'bg-yellow-50 border-yellow-200 text-yellow-500'"
>
{{ row.status }}
</span>
</template>
</UTable>
<div
v-if="subscriptions?.length > pageCount"
class="flex justify-end px-3 py-3.5 border-t border-gray-200 dark:border-gray-700"
>
<UPagination
v-model="page"
:page-count="pageCount"
:total="subscriptions.length"
/>
</div>
</AdminCard>
</template>
<script setup>
const props = defineProps({
user: { type: Object, required: true }
})
const loading = ref(true)
const subscriptions = ref([])
const page = ref(1)
const pageCount = 5
const rows = computed(() => {
return subscriptions.value.slice((page.value - 1) * pageCount, (page.value) * pageCount)
})
onMounted(() => {
getSubscriptions()
})
const getSubscriptions = () => {
if (!props.user.stripe_id) return
loading.value = true
opnFetch("/moderator/billing/" + props.user.id + "/subscriptions",).then(data => {
loading.value = false
subscriptions.value = data.subscriptions
}).catch(error => {
useAlert().error(error.data.message)
loading.value = false
})
}
const columns = [{
key: 'id',
label: 'ID'
}, {
key: 'stripe_id',
label: 'Stripe ID'
}, {
key: 'name',
label: 'Name',
sortable: true
}, {
key: 'creation_date',
label: 'Creation date',
sortable: true
}, {
key: 'plan',
label: 'Plan',
sortable: true,
direction: 'desc'
}, {
key: 'status',
label: 'Status'
}]
</script>

View File

@@ -0,0 +1,79 @@
<template>
<AdminCard
title="Workspaces"
icon="heroicons:globe-alt"
>
<UTable
:loading-state="{ icon: 'i-heroicons-arrow-path-20-solid', label: 'Loading...' }"
:progress="{ color: 'primary', animation: 'carousel' }"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'No items.' }"
:columns="columns"
:rows="rows"
class="-mx-6"
>
<template #plan-data="{ row }">
<span
class="text-xs select-all rounded-md px-2 py-1 border"
:class="userPlanStyles(row.plan)"
>
{{ row.plan }}
</span>
</template>
</UTable>
<div
v-if="workspaces?.length > pageCount"
class="flex justify-end px-3 py-3.5 border-t border-gray-200 dark:border-gray-700">
<UPagination
v-model="page"
:page-count="pageCount"
:total="workspaces?.length"
/>
</div>
</AdminCard>
</template>
<script setup>
const props = defineProps({
user: { type: Object, required: true }
})
const workspaces = ref([])
const page = ref(1)
const pageCount = 2
const rows = computed(() => {
return props.user.workspaces.slice((page.value - 1) * pageCount, (page.value) * pageCount)
})
const columns = [{
key: 'id',
label: 'ID'
}, {
key: 'name',
label: 'Name',
sortable: true
}, {
key: 'plan',
label: 'Plan',
sortable: true
}, {
key: 'forms_count',
label: '# of forms',
sortable: true
}]
function userPlanStyles(plan) {
switch (plan) {
case 'pro':
return 'capitalize text-xs select-all bg-green-50 rounded-md px-2 py-1 border border-green-200 text-green-500'
case 'enterprise':
return 'capitalize text-xs select-all bg-blue-50 rounded-md px-2 py-1 border border-blue-200 text-blue-500'
default:
return 'capitalize text-xs select-all bg-gray-50 rounded-md px-2 py-1 border'
}
}
</script>

View File

@@ -44,7 +44,6 @@ export default {
script.setAttribute('defer', 'defer')
document.head.appendChild(script)
script.addEventListener('load', () => {
console.log('resizeing')
window.iFrameResize(
{
log: false,