deleted frontend code

This commit is contained in:
Čarodej
2022-04-27 08:17:06 +02:00
parent f45c1eb576
commit eb4d5b4cba
142 changed files with 338 additions and 15390 deletions
-35
View File
@@ -6,11 +6,6 @@
<AppSpecification v-if="config.isAdminVueFileManagerBar" :data="data" class="hidden lg:flex" />
<AppSpecification v-if="config.isAdminVueFileManagerBar" :data="data" class="card shadow-card lg:hidden" />
<!--Create metered plan alert-->
<AlertBox v-if="config.subscriptionType === 'metered' && config.isEmptyPlans" color="rose">
As you installed app with metered subscription type, you have to <router-link :to="{ name: 'CreateMeteredPlan' }" class="dark:text-rose-500 text-sm font-bold underline">create your plan</router-link> as soon as possible to prevent new user registration without automatically assigned subscription plan.
</AlertBox>
<!--Cron Alert-->
<AlertBox v-if="!data.app.isRunningCron && !config.isDev" color="rose">
We detect your cron jobs probably doesn't work correctly, please check it, you need it for running app correctly. If you set your cron job, please get back one minute later.
@@ -50,22 +45,6 @@
<chevron-right-icon size="16" class="text-theme vue-feather" />
</router-link>
</div>
<div v-if="config.subscriptionType !== 'none'" class="card mb-4 w-full shadow-card md:mb-0">
<FormLabel icon="dollar">
{{ $t('earnings') }}
</FormLabel>
<b class="-mt-3 mb-0.5 block text-2xl font-extrabold sm:text-3xl">
{{ data.app.earnings }}
</b>
<router-link :to="{ name: 'Invoices' }" class="mt-6 flex items-center">
<span class="mr-2 whitespace-nowrap text-xs font-bold">
{{ $t('show_all_transactions') }}
</span>
<chevron-right-icon size="16" class="text-theme vue-feather" />
</router-link>
</div>
</div>
<!--Upload bandwidth widgets-->
@@ -110,18 +89,6 @@
<WidgetLatestRegistrations />
</div>
<!--Latest transactions widgets-->
<div
v-if="['fixed', 'metered'].includes(this.config.subscriptionType)"
class="card mb-4 shadow-card md:mb-6"
>
<FormLabel icon="dollar">
{{ $t('latest_transactions') }}
</FormLabel>
<WidgetLatestTransactions />
</div>
</div>
<div id="loader" v-if="isLoading">
<Spinner />
@@ -137,7 +104,6 @@ import FormLabel from '../../components/UI/Labels/FormLabel'
import BarChart from '../../components/UI/BarChart/BarChart'
import {mapGetters} from 'vuex'
import axios from 'axios'
import WidgetLatestTransactions from '../../components/Dashboard/Widgets/WidgetLatestTransactions'
import AlertBox from "../../components/UI/Others/AlertBox";
import AppSpecification from "../../components/Dashboard/AppSpecification";
@@ -146,7 +112,6 @@ export default {
components: {
AppSpecification,
AlertBox,
WidgetLatestTransactions,
WidgetLatestRegistrations,
ChevronRightIcon,
FormLabel,
-90
View File
@@ -1,90 +0,0 @@
<template>
<div>
<!--Datatable-->
<DatatableWrapper
v-if="!config.isEmptyTransactions"
class="card overflow-x-auto shadow-card"
api="/api/admin/transactions"
:paginator="true"
:columns="columns"
>
<template slot-scope="{ row }">
<!--Transaction rows-->
<MeteredTransactionRow
v-if="config.subscriptionType === 'metered'"
:row="row"
:user="true"
@showDetail="showTransactionDetail"
/>
<FixedTransactionRow v-if="config.subscriptionType === 'fixed'" :row="row" :user="true" />
<!--Transaction detail-->
<MeteredTransactionDetailRow
v-if="row.data.attributes.metadata && showedTransactionDetailById === row.data.id"
:row="row"
/>
</template>
</DatatableWrapper>
<!--Empty State-->
<div v-if="config.isEmptyTransactions" class="flex items-center justify-center fixed top-0 bottom-0 left-0 right-0">
<div class="text-center">
<img
class="mb-6 inline-block w-28"
src="https://twemoji.maxcdn.com/v/13.1.0/svg/1f9ee.svg"
alt="transaction"
/>
<h1 class="mb-1 text-2xl font-bold">
{{ $t('there_is_nothing') }}
</h1>
<p class="text-sm text-gray-600">
{{ $t('transaction_will_be_here') }}
</p>
</div>
</div>
</div>
</template>
<script>
import FixedTransactionRow from '../../components/Subscription/FixedTransactionRow'
import MeteredTransactionDetailRow from '../../components/Subscription/MeteredTransactionDetailRow'
import MeteredTransactionRow from '../../components/Subscription/MeteredTransactionRow'
import MemberAvatar from '../../components/UI/Others/MemberAvatar'
import DatatableWrapper from '../../components/UI/Table/DatatableWrapper'
import ColorLabel from '../../components/UI/Labels/ColorLabel'
import { mapGetters } from 'vuex'
export default {
name: 'Invoices',
components: {
MeteredTransactionDetailRow,
MeteredTransactionRow,
FixedTransactionRow,
DatatableWrapper,
MemberAvatar,
ColorLabel,
},
computed: {
...mapGetters(['config']),
columns() {
if (config.subscriptionType === 'fixed') {
return this.$store.getters.transactionColumns.filter((column) => !['type'].includes(column.field))
}
return this.$store.getters.transactionColumns
},
},
data() {
return {
showedTransactionDetailById: undefined,
}
},
methods: {
showTransactionDetail(id) {
if (this.showedTransactionDetailById === id) this.showedTransactionDetailById = undefined
else this.showedTransactionDetailById = id
},
},
}
</script>
-122
View File
@@ -1,122 +0,0 @@
<template>
<div>
<div class="card shadow-card">
<DatatableWrapper
@init="isLoading = false"
api="/api/admin/pages"
:paginator="false"
:columns="columns"
class="overflow-x-auto"
>
<template slot-scope="{ row }">
<tr class="whitespace-nowrap border-b border-dashed border-light dark:border-opacity-5">
<td class="py-5 pr-3 md:pr-1">
<router-link
:to="{
name: 'PageEdit',
params: { slug: row.data.attributes.slug },
}"
class="cursor-pointer text-sm font-bold"
tag="div"
>
{{ row.data.attributes.title }}
</router-link>
</td>
<td class="px-3 md:px-1">
<span class="text-sm font-bold">
{{ row.data.attributes.slug }}
</span>
</td>
<td class="px-3 md:px-1">
<span class="text-sm font-bold">
<SwitchInput
@input="
$updateText(
`/admin/pages/${row.data.id}`,
'visibility',
row.data.attributes.visibility
)
"
v-model="row.data.attributes.visibility"
:state="row.data.attributes.visibility"
class="switch"
/>
</span>
</td>
<td class="pl-3 text-right md:pl-1">
<div class="flex w-full justify-end space-x-2">
<router-link
class="flex h-8 w-8 items-center justify-center rounded-md bg-light-background transition-colors hover:bg-green-100 dark:bg-2x-dark-foreground"
:to="{
name: 'PageEdit',
params: {
slug: row.data.attributes.slug,
},
}"
>
<Edit2Icon size="15" class="opacity-75" />
</router-link>
</div>
</td>
</tr>
</template>
</DatatableWrapper>
</div>
</div>
</template>
<script>
import DatatableWrapper from '../../components/UI/Table/DatatableWrapper'
import MobileActionButton from '../../components/UI/Buttons/MobileActionButton'
import EmptyPageContent from '../../components/Others/EmptyPageContent'
import SwitchInput from '../../components/Inputs/SwitchInput'
import MobileHeader from '../../components/Mobile/MobileHeader'
import SectionTitle from '../../components/UI/Labels/SectionTitle'
import ButtonBase from '../../components/UI/Buttons/ButtonBase'
import { Trash2Icon, Edit2Icon } from 'vue-feather-icons'
import ColorLabel from '../../components/UI/Labels/ColorLabel'
import Spinner from '../../components/UI/Others/Spinner'
export default {
name: 'Pages',
components: {
MobileActionButton,
EmptyPageContent,
DatatableWrapper,
SectionTitle,
MobileHeader,
SwitchInput,
Trash2Icon,
ButtonBase,
ColorLabel,
Edit2Icon,
Spinner,
},
data() {
return {
isLoading: true,
columns: [
{
label: this.$t('page'),
field: 'title',
sortable: true,
},
{
label: this.$t('slug'),
field: 'slug',
sortable: true,
},
{
label: this.$t('status'),
field: 'visibility',
sortable: true,
},
{
label: this.$t('action'),
sortable: false,
},
],
}
},
}
</script>
@@ -1,87 +0,0 @@
<template>
<div>
<div v-if="!isLoading && page" class="card shadow-card">
<FormLabel>
{{ page.data.attributes.title }}
</FormLabel>
<AppInputSwitch
:title="$t('visibility')"
:description="$t('admin_pages.form.visibility_help')"
>
<SwitchInput @input="changeStatus" class="switch" :state="page.data.attributes.visibility" />
</AppInputSwitch>
<AppInputText :title="$t('title')">
<input
@input="$updateText('/admin/pages/' + $route.params.slug, 'title', page.data.attributes.title)"
v-model="page.data.attributes.title"
:placeholder="$t('admin_pages.form.title_plac')"
type="text"
class="focus-border-theme input-dark"
/>
</AppInputText>
<AppInputText :title="$t('slug')">
<input v-model="page.data.attributes.slug" type="text" class="focus-border-theme input-dark" disabled />
</AppInputText>
<AppInputText :title="$t('content')" :is-last="true">
<textarea
@input="$updateText('/admin/pages/' + $route.params.slug, 'content', page.data.attributes.content)"
v-model="page.data.attributes.content"
:placeholder="$t('admin_pages.form.content_plac')"
class="focus-border-theme input-dark"
rows="18"
></textarea>
</AppInputText>
</div>
<div id="loader" v-if="isLoading">
<Spinner />
</div>
</div>
</template>
<script>
import AppInputSwitch from '../../../components/Forms/Layouts/AppInputSwitch'
import AppInputText from '../../../components/Forms/Layouts/AppInputText'
import { ValidationProvider, ValidationObserver } from 'vee-validate/dist/vee-validate.full'
import FormLabel from '../../../components/UI/Labels/FormLabel'
import { required } from 'vee-validate/dist/rules'
import SwitchInput from '../../../components/Inputs/SwitchInput'
import MobileHeader from '../../../components/Mobile/MobileHeader'
import SectionTitle from '../../../components/UI/Labels/SectionTitle'
import ButtonBase from '../../../components/UI/Buttons/ButtonBase'
import Spinner from '../../../components/UI/Others/Spinner'
import axios from 'axios'
export default {
name: 'PageEdit',
components: {
AppInputSwitch,
AppInputText,
ValidationProvider,
ValidationObserver,
FormLabel,
SectionTitle,
MobileHeader,
SwitchInput,
ButtonBase,
required,
Spinner,
},
data() {
return {
isLoading: true,
page: undefined,
}
},
methods: {
changeStatus(val) {
this.$updateText('/admin/pages/' + this.$route.params.slug, 'visibility', val)
},
},
created() {
axios.get('/api/admin/pages/' + this.$route.params.slug).then((response) => {
this.page = response.data
this.isLoading = false
})
},
}
</script>
@@ -1,39 +0,0 @@
<template>
<div>
<!--Page Tab links-->
<div class="card z-10 shadow-card" style="padding-bottom: 0; padding-top: 0">
<CardNavigation :pages="pages" class="-mx-1" />
</div>
<!--Page Content-->
<router-view />
</div>
</template>
<script>
import CardNavigation from '../../../components/UI/Others/CardNavigation'
export default {
name: 'PaymentSettings',
components: {
CardNavigation,
},
data() {
return {
pages: [
{
title: this.$t('payments'),
route: 'AppPayments',
},
{
title: this.$t('billings'),
route: 'AppBillings',
},
],
}
},
mounted() {
this.$router.replace({ name: 'AppPayments' })
},
}
</script>
@@ -1,167 +0,0 @@
<template>
<PageTab :is-loading="isLoading">
<div v-if="billingInformation" class="card shadow-card">
<FormLabel>
{{ $t('admin_settings.billings.section_company') }}
</FormLabel>
<AppInputText :title="$t('admin_settings.billings.company_name')">
<input
@input="$updateText('/admin/settings', 'billing_name', billingInformation.billing_name)"
v-model="billingInformation.billing_name"
:placeholder="$t('admin_settings.billings.company_name_plac')"
type="text"
class="focus-border-theme input-dark"
/>
</AppInputText>
<AppInputText :title="$t('admin_settings.billings.vat')" :is-last="true">
<input
@input="$updateText('/admin/settings', 'billing_vat_number', billingInformation.billing_vat_number)"
v-model="billingInformation.billing_vat_number"
:placeholder="$t('admin_settings.billings.vat_plac')"
type="text"
class="focus-border-theme input-dark"
/>
</AppInputText>
</div>
<div v-if="billingInformation" class="card shadow-card">
<FormLabel>
{{ $t('admin_settings.billings.section_billing') }}
</FormLabel>
<AppInputText :title="$t('admin_settings.billings.country')">
<SelectInput
@input="$updateText('/admin/settings', 'billing_country', billingInformation.billing_country)"
v-model="billingInformation.billing_country"
:default="billingInformation.billing_country"
:options="countries"
:placeholder="$t('admin_settings.billings.country_plac')"
/>
</AppInputText>
<AppInputText :title="$t('billing_address')">
<input
@input="$updateText('/admin/settings', 'billing_address', billingInformation.billing_address)"
v-model="billingInformation.billing_address"
:placeholder="$t('admin_settings.billings.address_plac')"
type="text"
class="focus-border-theme input-dark"
/>
</AppInputText>
<div class="flex space-x-4">
<AppInputText :title="$t('billing_city')" class="w-full">
<input
@input="$updateText('/admin/settings', 'billing_city', billingInformation.billing_city)"
v-model="billingInformation.billing_city"
:placeholder="$t('admin_settings.billings.city_plac')"
type="text"
class="focus-border-theme input-dark"
/>
</AppInputText>
<AppInputText :title="$t('admin_settings.billings.postal_code')" class="w-full">
<input
@input="
$updateText(
'/admin/settings',
'billing_postal_code',
billingInformation.billing_postal_code
)
"
v-model="billingInformation.billing_postal_code"
:placeholder="$t('admin_settings.billings.postal_code_plac')"
type="text"
class="focus-border-theme input-dark"
/>
</AppInputText>
</div>
<AppInputText :title="$t('admin_settings.billings.state')">
<input
@input="$updateText('/admin/settings', 'billing_state', billingInformation.billing_state)"
v-model="billingInformation.billing_state"
:placeholder="$t('admin_settings.billings.state_plac')"
type="text"
class="focus-border-theme input-dark"
/>
</AppInputText>
<AppInputText :title="$t('admin_settings.billings.phone_number')" :is-last="true">
<input
@input="
$updateText('/admin/settings', 'billing_phone_number', billingInformation.billing_phone_number)
"
v-model="billingInformation.billing_phone_number"
:placeholder="$t('admin_settings.billings.phone_number_plac')"
type="text"
class="focus-border-theme input-dark"
/>
</AppInputText>
</div>
</PageTab>
</template>
<script>
import AppInputText from '../../../../components/Forms/Layouts/AppInputText'
import { ValidationProvider, ValidationObserver } from 'vee-validate/dist/vee-validate.full'
import PageTabGroup from '../../../../components/Layout/PageTabGroup'
import SelectInput from '../../../../components/Inputs/SelectInput'
import ImageInput from '../../../../components/Inputs/ImageInput'
import FormLabel from '../../../../components/UI/Labels/FormLabel'
import ButtonBase from '../../../../components/UI/Buttons/ButtonBase'
import PageTab from '../../../../components/Layout/PageTab'
import InfoBox from '../../../../components/UI/Others/InfoBox'
import { required } from 'vee-validate/dist/rules'
import axios from 'axios'
import { mapGetters } from 'vuex'
export default {
name: 'AppAppearance',
components: {
ValidationObserver,
ValidationProvider,
AppInputText,
PageTabGroup,
SelectInput,
ImageInput,
ButtonBase,
FormLabel,
required,
PageTab,
InfoBox,
},
computed: {
...mapGetters(['countries']),
},
data() {
return {
isLoading: true,
billingInformation: undefined,
}
},
mounted() {
axios
.get('/api/admin/settings', {
params: {
column: 'billing_phone_number|billing_postal_code|billing_vat_number|billing_address|billing_country|billing_state|billing_city|billing_name',
},
})
.then((response) => {
this.isLoading = false
this.billingInformation = {
billing_phone_number: response.data.billing_phone_number,
billing_postal_code: response.data.billing_postal_code,
billing_vat_number: response.data.billing_vat_number,
billing_address: response.data.billing_address,
billing_country: response.data.billing_country,
billing_state: response.data.billing_state,
billing_city: response.data.billing_city,
billing_name: response.data.billing_name,
}
})
},
}
</script>
@@ -1,664 +0,0 @@
<template>
<PageTab>
<!--Global payment settings-->
<div class="card shadow-card">
<FormLabel icon="dollar">
{{ $t('subscription_payments') }}
</FormLabel>
<AppInputSwitch
:title="$t('allow_subscription_payments')"
:description="$t('allow_subscription_payments_description')"
:is-last="true"
>
<SwitchInput
@input="$updateText('/admin/settings', 'allowed_payments', allowedPayments)"
v-model="allowedPayments"
:state="allowedPayments"
/>
</AppInputSwitch>
</div>
<!--Metered settings-->
<div v-if="config.subscriptionType === 'metered' && allowedPayments" class="card shadow-card">
<FormLabel icon="bar-chart">
{{ $t('metered_billing_settings') }}
</FormLabel>
<AppInputSwitch
:title="$t('allow_registration_bonus')"
:description="$t('allow_registration_bonus_description')"
>
<SwitchInput
@input="$updateText('/admin/settings', 'allowed_registration_bonus', allowedRegistrationBonus)"
v-model="allowedRegistrationBonus"
:state="allowedRegistrationBonus"
/>
</AppInputSwitch>
<AppInputText
v-if="allowedRegistrationBonus"
:title="$t('registration_bonus_amount')"
:description="$t('registration_bonus_amount_description')"
>
<input
@input="$updateText('/admin/settings', 'registration_bonus_amount', registrationBonusAmount)"
v-model="registrationBonusAmount"
:placeholder="$t('registration_bonus_amount_')"
type="number"
class="focus-border-theme input-dark"
/>
</AppInputText>
<AppInputButton
:title="$t('metered_plan')"
:description="$t('metered_plan_description')"
:is-last="true"
>
<router-link
v-if="config.isCreatedMeteredPlan"
:to="{
name: 'PlanMeteredSettings',
params: { id: config.meteredPlanId },
}"
>
<ButtonBase v-if="config.isCreatedMeteredPlan" class="w-full sm:w-auto" button-style="theme">
{{ $t('plan_details') }}
</ButtonBase>
</router-link>
<router-link v-if="!config.isCreatedMeteredPlan" :to="{ name: 'CreateMeteredPlan' }">
<ButtonBase class="w-full sm:w-auto" button-style="theme-solid">
{{ $t('create_plan') }}
</ButtonBase>
</router-link>
</AppInputButton>
</div>
<!--Stripe method configuration-->
<div v-if="allowedPayments" class="card shadow-card">
<img :src="$getPaymentLogo('stripe')" alt="Stripe" class="mb-8 h-8" />
<AppInputSwitch
:title="$t('Allow Stripe Service')"
:description="$t('allow_pay_by_card')"
:is-last="!stripe.allowedService"
>
<SwitchInput
@input="$updateText('/admin/settings', 'allowed_stripe', stripe.allowedService)"
v-model="stripe.allowedService"
:state="stripe.allowedService"
/>
</AppInputSwitch>
<!--Stripe credentials are set up-->
<div v-if="stripe.allowedService">
<AppInputText
:title="$t('webhook_url')"
:description="$t('copy_webhook_note')"
>
<CopyInput size="small" :str="getWebhookEndpoint('stripe')" />
</AppInputText>
<div v-if="stripe.isConfigured">
<AppInputText
@input="$updateText('/admin/settings', 'stripe_payment_description', stripe.paymentDescription)"
:title="$t('payment_description')"
:description="$t('payment_description_note')"
>
<textarea
rows="2"
@input="
$updateText(
'/admin/settings',
'stripe_payment_description',
stripe.paymentDescription,
true
)
"
v-model="stripe.paymentDescription"
:placeholder="
$t('additional_info_about_payment_method_')
"
type="text"
class="focus-border-theme input-dark"
/>
</AppInputText>
<div
@click="stripe.isVisibleCredentialsForm = !stripe.isVisibleCredentialsForm"
class="flex cursor-pointer items-center"
:class="{ 'mb-4': stripe.isVisibleCredentialsForm }"
>
<edit2-icon size="14" class="vue-feather text-theme mr-2.5" />
<b class="text-sm">{{ $t('update_your_credentials') }}</b>
</div>
</div>
<!--Set up Stripe credentials-->
<ValidationObserver
v-if="!stripe.isConfigured || stripe.isVisibleCredentialsForm"
@submit.prevent="storeCredentials('stripe')"
ref="stripe"
v-slot="{ invalid }"
tag="form"
class="rounded-xl p-5 shadow-lg"
>
<FormLabel v-if="!stripe.isConfigured" icon="shield">
{{ $t('configure_your_credentials') }}
</FormLabel>
<ValidationProvider
tag="div"
mode="passive"
name="Publishable Key"
rules="required"
v-slot="{ errors }"
>
<AppInputText :title="$t('admin_settings.payments.stripe_pub_key')" :error="errors[0]">
<input
v-model="stripe.credentials.key"
:placeholder="$t('admin_settings.payments.stripe_pub_key_plac')"
type="text"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
</AppInputText>
</ValidationProvider>
<ValidationProvider tag="div" mode="passive" name="Secret Key" rules="required" v-slot="{ errors }">
<AppInputText :title="$t('admin_settings.payments.stripe_sec_key')" :error="errors[0]">
<input
v-model="stripe.credentials.secret"
:placeholder="$t('admin_settings.payments.stripe_sec_key_plac')"
type="text"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
</AppInputText>
</ValidationProvider>
<ValidationProvider
tag="div"
mode="passive"
name="Webhook Secret"
rules="required"
v-slot="{ errors }"
>
<AppInputText :title="$t('Webhook Secret')" :error="errors[0]">
<input
v-model="stripe.credentials.webhook"
:placeholder="$t('paste_webhook_secret')"
type="text"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
</AppInputText>
</ValidationProvider>
<ButtonBase
:disabled="isLoading"
:loading="isLoading"
button-style="theme"
type="submit"
class="w-full"
>
{{ $t('store_credentials') }}
</ButtonBase>
</ValidationObserver>
</div>
</div>
<!--Paystack method configuration-->
<div v-if="allowedPayments" class="card shadow-card">
<img :src="$getPaymentLogo('paystack')" alt="Paystack" class="mb-8 h-7" />
<AppInputSwitch
:title="$t('Allow Paystack Service')"
:description="$t('allow_pay_by_card')"
:is-last="!paystack.allowedService"
>
<SwitchInput
@input="$updateText('/admin/settings', 'allowed_paystack', paystack.allowedService)"
v-model="paystack.allowedService"
:state="paystack.allowedService"
/>
</AppInputSwitch>
<!--Paystack credentials are set up-->
<div v-if="paystack.allowedService">
<AppInputText
:title="$t('webhook_url')"
:description="$t('copy_webhook_note')"
>
<CopyInput size="small" :str="getWebhookEndpoint('paystack')" />
</AppInputText>
<div v-if="paystack.isConfigured">
<AppInputText
@input="
$updateText('/admin/settings', 'paystack_payment_description', paystack.paymentDescription)
"
:title="$t('payment_description')"
:description="$t('payment_description_note')"
>
<textarea
rows="2"
@input="
$updateText(
'/admin/settings',
'paystack_payment_description',
paystack.paymentDescription,
true
)
"
v-model="paystack.paymentDescription"
:placeholder="
$t('additional_info_about_payment_method_')
"
type="text"
class="focus-border-theme input-dark"
/>
</AppInputText>
<div
@click="paystack.isVisibleCredentialsForm = !paystack.isVisibleCredentialsForm"
class="flex cursor-pointer items-center"
:class="{ 'mb-4': paystack.isVisibleCredentialsForm }"
>
<edit2-icon size="14" class="vue-feather text-theme mr-2.5" />
<b class="text-sm">{{ $t('update_your_credentials') }}</b>
</div>
</div>
<!--Set up Paystack credentials-->
<ValidationObserver
v-if="!paystack.isConfigured || paystack.isVisibleCredentialsForm"
@submit.prevent="storeCredentials('paystack')"
ref="paystack"
v-slot="{ invalid }"
tag="form"
class="rounded-xl p-5 shadow-lg"
>
<FormLabel v-if="!paystack.isConfigured" icon="shield">
{{ $t('configure_your_credentials') }}
</FormLabel>
<ValidationProvider
tag="div"
mode="passive"
name="Publishable Key"
rules="required"
v-slot="{ errors }"
>
<AppInputText :title="$t('admin_settings.payments.stripe_pub_key')" :error="errors[0]">
<input
v-model="paystack.credentials.key"
:placeholder="$t('admin_settings.payments.stripe_pub_key_plac')"
type="text"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
</AppInputText>
</ValidationProvider>
<ValidationProvider tag="div" mode="passive" name="Secret Key" rules="required" v-slot="{ errors }">
<AppInputText :title="$t('admin_settings.payments.stripe_sec_key')" :error="errors[0]">
<input
v-model="paystack.credentials.secret"
:placeholder="$t('admin_settings.payments.stripe_sec_key_plac')"
type="text"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
</AppInputText>
</ValidationProvider>
<ButtonBase
:disabled="isLoading"
:loading="isLoading"
button-style="theme"
type="submit"
class="w-full"
>
{{ $t('store_credentials') }}
</ButtonBase>
</ValidationObserver>
</div>
</div>
<!--PayPal method configuration-->
<div v-if="allowedPayments" class="card shadow-card">
<img :src="$getPaymentLogo('paypal')" alt="PayPal" class="mb-8 h-8" />
<AppInputSwitch
:title="$t('Allow PayPal Service')"
:description="$t('allow_pay_by_card')"
:is-last="!paypal.allowedService"
>
<SwitchInput
@input="$updateText('/admin/settings', 'allowed_paypal', paypal.allowedService)"
v-model="paypal.allowedService"
:state="paypal.allowedService"
/>
</AppInputSwitch>
<!--Stripe credentials are set up-->
<div v-if="paypal.allowedService">
<AppInputText
:title="$t('webhook_url')"
:description="$t('copy_webhook_note')"
>
<CopyInput size="small" :str="getWebhookEndpoint('paypal')" />
</AppInputText>
<div v-if="paypal.isConfigured">
<AppInputSwitch :title="$t('Live Mode')" :description="$t('Toggle between live and sandbox mode')">
<SwitchInput
@input="$updateText('/admin/settings', 'paypal_live', config.isPayPalLive)"
v-model="config.isPayPalLive"
:state="config.isPayPalLive"
/>
</AppInputSwitch>
<AppInputText
@input="$updateText('/admin/settings', 'paypal_payment_description', paypal.paymentDescription)"
:title="$t('payment_description')"
:description="$t('payment_description_note')"
>
<textarea
rows="2"
@input="
$updateText(
'/admin/settings',
'paypal_payment_description',
paypal.paymentDescription,
true
)
"
v-model="paypal.paymentDescription"
:placeholder="
$t('additional_info_about_payment_method_')
"
type="text"
class="focus-border-theme input-dark"
/>
</AppInputText>
<div
@click="paypal.isVisibleCredentialsForm = !paypal.isVisibleCredentialsForm"
class="flex cursor-pointer items-center"
:class="{ 'mb-4': paypal.isVisibleCredentialsForm }"
>
<edit2-icon size="14" class="vue-feather text-theme mr-2.5" />
<b class="text-sm">{{ $t('update_your_credentials') }}</b>
</div>
</div>
<!--Set up Paypal credentials-->
<ValidationObserver
v-if="!paypal.isConfigured || paypal.isVisibleCredentialsForm"
@submit.prevent="storeCredentials('paypal')"
ref="paypal"
v-slot="{ invalid }"
tag="form"
class="rounded-xl p-5 shadow-lg"
>
<FormLabel v-if="!paypal.isConfigured" icon="shield">
{{ $t('configure_your_credentials') }}
</FormLabel>
<ValidationProvider>
<AppInputSwitch v-if="! paypal.isConfigured" :title="$t('Live Mode')" :description="$t('Toggle between live and sandbox mode')">
<SwitchInput v-model="paypal.credentials.live" :state="paypal.credentials.live" />
</AppInputSwitch>
</ValidationProvider>
<ValidationProvider
tag="div"
mode="passive"
name="Publishable Key"
rules="required"
v-slot="{ errors }"
>
<AppInputText :title="$t('admin_settings.payments.stripe_pub_key')" :error="errors[0]">
<input
v-model="paypal.credentials.key"
:placeholder="$t('admin_settings.payments.stripe_pub_key_plac')"
type="text"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
</AppInputText>
</ValidationProvider>
<ValidationProvider tag="div" mode="passive" name="Secret Key" rules="required" v-slot="{ errors }">
<AppInputText :title="$t('admin_settings.payments.stripe_sec_key')" :error="errors[0]">
<input
v-model="paypal.credentials.secret"
:placeholder="$t('admin_settings.payments.stripe_sec_key_plac')"
type="text"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
</AppInputText>
</ValidationProvider>
<ValidationProvider tag="div" mode="passive" name="Webhook ID" rules="required" v-slot="{ errors }">
<AppInputText :title="$t('Webhook ID')" :error="errors[0]">
<input
v-model="paypal.credentials.webhook"
:placeholder="$t('Paste your webhook id')"
type="text"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
</AppInputText>
</ValidationProvider>
<ButtonBase
:disabled="isLoading"
:loading="isLoading"
button-style="theme"
type="submit"
class="w-full"
>
{{ $t('store_credentials') }}
</ButtonBase>
</ValidationObserver>
</div>
</div>
</PageTab>
</template>
<script>
import { Edit2Icon, Trash2Icon } from 'vue-feather-icons'
import AppInputButton from '../../../../components/Forms/Layouts/AppInputButton'
import DatatableWrapper from '../../../../components/UI/Table/DatatableWrapper'
import { ValidationProvider, ValidationObserver } from 'vee-validate/dist/vee-validate.full'
import PageTabGroup from '../../../../components/Layout/PageTabGroup'
import SelectInput from '../../../../components/Inputs/SelectInput'
import SwitchInput from '../../../../components/Inputs/SwitchInput'
import ImageInput from '../../../../components/Inputs/ImageInput'
import AppInputSwitch from '../../../../components/Forms/Layouts/AppInputSwitch'
import FormLabel from '../../../../components/UI/Labels/FormLabel'
import ButtonBase from '../../../../components/UI/Buttons/ButtonBase'
import CopyInput from '../../../../components/Inputs/CopyInput'
import AppInputText from '../../../../components/Forms/Layouts/AppInputText'
import PageTab from '../../../../components/Layout/PageTab'
import InfoBox from '../../../../components/UI/Others/InfoBox'
import { required } from 'vee-validate/dist/rules'
import { events } from '../../../../bus'
import { mapGetters } from 'vuex'
import axios from 'axios'
export default {
name: 'AppPayments',
components: {
AppInputButton,
DatatableWrapper,
ValidationObserver,
ValidationProvider,
AppInputSwitch,
AppInputText,
PageTabGroup,
SwitchInput,
SelectInput,
ImageInput,
ButtonBase,
CopyInput,
FormLabel,
Trash2Icon,
Edit2Icon,
required,
PageTab,
InfoBox,
},
computed: {
...mapGetters(['config']),
},
data() {
return {
allowedRegistrationBonus: true,
registrationBonusAmount: undefined,
allowedPayments: false,
isLoading: false,
isError: false,
errorMessage: '',
stripe: {
allowedService: true,
isConfigured: false,
isVisibleCredentialsForm: false,
paymentDescription: undefined,
credentials: {
key: undefined,
secret: undefined,
webhook: undefined,
},
},
paystack: {
allowedService: true,
isConfigured: false,
isVisibleCredentialsForm: false,
paymentDescription: undefined,
credentials: {
key: undefined,
secret: undefined,
},
},
paypal: {
allowedService: true,
isConfigured: false,
isVisibleCredentialsForm: false,
paymentDescription: undefined,
credentials: {
key: undefined,
secret: undefined,
webhook: undefined,
live: undefined,
},
},
columns: [
{
label: this.$t('name'),
field: 'name',
sortable: true,
},
{
label: this.$t('currency'),
field: 'currency',
sortable: true,
},
{
label: this.$t('interval'),
field: 'interval',
sortable: true,
},
{
label: this.$t('subscribers'),
sortable: false,
},
{
label: this.$t('action'),
sortable: false,
},
],
}
},
methods: {
async storeCredentials(service) {
// Validate fields
const isValid = await this.$refs[service].validate()
if (!isValid) return
// Start loading
this.isLoading = true
// Send request to get verify account
axios
.post('/api/admin/settings/payment-service', {
service: service,
key: this[service].credentials.key,
secret: this[service].credentials.secret,
webhook: this[service].credentials.webhook || undefined,
live: this[service].credentials.live,
})
.then(() => {
// Update Credentials
let commitKey = {
stripe: 'SET_STRIPE_CREDENTIALS',
paystack: 'SET_PAYSTACK_CREDENTIALS',
paypal: 'SET_PAYPAL_CREDENTIALS',
}[service]
// Commit credentials
this.$store.commit(commitKey, this[service].credentials)
this[service].allowedService = true
this[service].isConfigured = true
this[service].isVisibleCredentialsForm = false
// Show toaster
events.$emit('toaster', {
type: 'success',
message: this.$t('toaster.credentials_set', {
service: service,
}),
})
})
.catch((error) => {
if (error.response.status === 500) {
this.isError = true
this.errorMessage = error.response.data.message
}
})
.finally(() => (this.isLoading = false))
},
getWebhookEndpoint(service) {
return `${this.config.host}/api/subscriptions/${service}/webhooks`
},
},
created() {
// Set payment description
this.stripe.paymentDescription = this.config.stripe_payment_description
this.paystack.paymentDescription = this.config.paystack_payment_description
this.paypal.paymentDescription = this.config.paypal_payment_description
// Set if service is allowed
this.stripe.allowedService = this.config.isStripe
this.paystack.allowedService = this.config.isPaystack
this.paypal.allowedService = this.config.isPayPal
if (this.config.stripe_public_key) this.stripe.isConfigured = true
if (this.config.paystack_public_key) this.paystack.isConfigured = true
if (this.config.paypal_client_id) this.paypal.isConfigured = true
this.allowedPayments = this.config.allowed_payments
// Set metered
this.allowedRegistrationBonus = this.config.allowed_registration_bonus
this.registrationBonusAmount = this.config.registration_bonus_amount
},
destroyed() {
events.$off('action:confirmed')
},
}
</script>
-300
View File
@@ -1,300 +0,0 @@
<template>
<div>
<!--Plans-->
<div v-if="!config.isEmptyPlans" class="card shadow-card">
<!--Create button-->
<div v-if="!config.isCreatedMeteredPlan || config.subscriptionType === 'fixed'" class="mb-6">
<router-link :to="{ name: createPlanRoute }">
<MobileActionButton icon="plus">
{{ $t('create_plan') }}
</MobileActionButton>
</router-link>
</div>
<!--Datatable-->
<DatatableWrapper
@data="plans = $event"
@init="isLoading = false"
api="/api/subscriptions/admin/plans"
:paginator="true"
:columns="columns"
class="overflow-x-auto"
>
<template slot-scope="{ row }">
<!--Metered subscription-->
<tr
v-if="config.subscriptionType === 'metered'"
class="whitespace-nowrap border-b border-dashed border-light dark:border-opacity-5"
>
<td class="py-5 pr-3 md:pr-1">
<router-link
class="text-sm font-bold"
:to="{
name: 'PlanMeteredSettings',
params: { id: row.data.id },
}"
>
{{ row.data.attributes.name }}
</router-link>
</td>
<td class="px-3 md:px-1">
<ColorLabel :color="$getPlanStatusColor(row.data.attributes.status)">
{{ $t(row.data.attributes.status) }}
</ColorLabel>
</td>
<td class="px-3 md:px-1">
<span class="text-sm font-bold">
{{ row.data.attributes.currency }}
</span>
</td>
<td class="px-3 md:px-1">
<span class="text-sm font-bold capitalize">
{{ $t(row.data.attributes.interval) }}
</span>
</td>
<td class="px-3 md:px-1">
<span class="text-sm font-bold">
{{ row.data.attributes.subscribers }}
</span>
</td>
<td class="pl-3 text-right md:pl-1">
<div class="flex w-full justify-end space-x-2">
<router-link
:to="{
name: 'PlanMeteredSettings',
params: { id: row.data.id },
}"
class="flex h-8 w-8 items-center justify-center rounded-md bg-light-background transition-colors hover:bg-green-100 dark:bg-2x-dark-foreground"
>
<Edit2Icon size="15" class="opacity-75" />
</router-link>
<router-link
v-if="row.data.attributes.status !== 'archived'"
:to="{
name: 'PlanMeteredDelete',
params: { id: row.data.id },
}"
class="flex h-8 w-8 items-center justify-center rounded-md bg-light-background transition-colors hover:bg-red-100 dark:bg-2x-dark-foreground"
>
<Trash2Icon size="15" class="opacity-75" />
</router-link>
</div>
</td>
</tr>
<!--Fixed subscription-->
<tr
v-if="config.subscriptionType === 'fixed'"
class="whitespace-nowrap border-b border-dashed border-light dark:border-opacity-5"
>
<td class="py-5 pr-3 md:pr-1">
<span v-if="row.data.attributes.status === 'archived'" class="ml-6 text-gray-300">-</span>
<SwitchInput
v-if="row.data.attributes.status === 'active'"
@input="
$updateInput(
`/subscriptions/admin/plans/${row.data.id}`,
'visible',
row.data.attributes.visible
)
"
v-model="row.data.attributes.visible"
:state="row.data.attributes.visible"
class="switch"
/>
</td>
<td class="px-3 md:px-1">
<router-link
class="text-sm font-bold"
:to="{
name: 'PlanFixedSettings',
params: { id: row.data.id },
}"
>
{{ row.data.attributes.name }}
</router-link>
</td>
<td class="px-3 md:px-1">
<ColorLabel :color="$getPlanStatusColor(row.data.attributes.status)">
{{ $t(row.data.attributes.status) }}
</ColorLabel>
</td>
<td class="px-3 md:px-1">
<span class="text-sm font-bold">
{{ row.data.attributes.price }}
</span>
</td>
<td class="px-3 md:px-1">
<span class="text-sm font-bold capitalize">
{{ $t(row.data.attributes.interval) }}
</span>
</td>
<td class="px-3 md:px-1">
<span class="text-sm font-bold">
{{ row.data.attributes.subscribers }}
</span>
</td>
<td class="px-3 md:px-1">
<span class="text-sm font-bold">
{{ row.data.attributes.features.max_storage_amount }}
GB
</span>
</td>
<td class="pl-3 text-right md:pl-1">
<div class="flex w-full justify-end space-x-2">
<router-link
class="flex h-8 w-8 items-center justify-center rounded-md bg-light-background transition-colors hover:bg-green-100 dark:bg-2x-dark-foreground"
:to="{
name: 'PlanFixedSettings',
params: { id: row.data.id },
}"
>
<Edit2Icon size="15" class="opacity-75" />
</router-link>
<router-link
class="flex h-8 w-8 items-center justify-center rounded-md bg-light-background transition-colors hover:bg-red-100 dark:bg-2x-dark-foreground"
:to="{
name: 'PlanFixedDelete',
params: { id: row.data.id },
}"
>
<Trash2Icon size="15" class="opacity-75" />
</router-link>
</div>
</td>
</tr>
</template>
</DatatableWrapper>
</div>
<!--Empty State-->
<div v-if="config.isEmptyPlans" class="flex items-center justify-center fixed top-0 bottom-0 left-0 right-0">
<div class="text-center">
<img
class="mb-6 inline-block w-28"
src="https://twemoji.maxcdn.com/v/13.1.0/svg/1f9fe.svg"
alt="transaction"
/>
<h1 class="mb-1 text-2xl font-bold">
{{ $t('there_is_nothing') }}
</h1>
<p class="text-sm text-gray-600">
{{ $t('all_visible_plans_here') }}
</p>
<router-link :to="{ name: createPlanRoute }" class="mt-6 inline-block">
<ButtonBase class="action-confirm" button-style="theme">
{{ $t('create_first_plan') }}
</ButtonBase>
</router-link>
</div>
</div>
</div>
</template>
<script>
import DatatableWrapper from '../../components/UI/Table/DatatableWrapper'
import MobileActionButton from '../../components/UI/Buttons/MobileActionButton'
import SwitchInput from '../../components/Inputs/SwitchInput'
import ButtonBase from '../../components/UI/Buttons/ButtonBase'
import ColorLabel from '../../components/UI/Labels/ColorLabel'
import { Trash2Icon, Edit2Icon } from 'vue-feather-icons'
import { mapGetters } from 'vuex'
export default {
name: 'Plans',
components: {
MobileActionButton,
DatatableWrapper,
SwitchInput,
ColorLabel,
Trash2Icon,
ButtonBase,
Edit2Icon,
},
computed: {
...mapGetters(['config']),
createPlanRoute() {
return {
metered: 'CreateMeteredPlan',
fixed: 'CreateFixedPlan',
}[this.config.subscriptionType]
},
columns() {
return {
metered: [
{
label: this.$t('name'),
field: 'name',
sortable: true,
},
{
label: this.$t('status'),
field: 'status',
sortable: true,
},
{
label: this.$t('currency'),
field: 'currency',
sortable: true,
},
{
label: this.$t('interval'),
field: 'interval',
sortable: true,
},
{
label: this.$t('subscribers'),
sortable: false,
},
{
label: this.$t('action'),
sortable: false,
},
],
fixed: [
{
label: this.$t('visibility'),
field: 'visible',
sortable: true,
},
{
label: this.$t('name'),
field: 'name',
sortable: true,
},
{
label: this.$t('status'),
field: 'status',
sortable: true,
},
{
label: this.$t('price'),
field: 'amount',
sortable: true,
},
{
label: this.$t('interval'),
field: 'interval',
sortable: true,
},
{
label: this.$t('subscribers'),
sortable: false,
},
{
label: this.$t('storage'),
sortable: false,
},
{
label: this.$t('action'),
sortable: false,
},
],
}[this.config.subscriptionType]
},
},
}
</script>
@@ -1,267 +0,0 @@
<template>
<ValidationObserver @submit.prevent="createPlan" ref="createPlan" v-slot="{ invalid }" tag="form">
<div class="card shadow-card">
<FormLabel>
{{ $t('details') }}
</FormLabel>
<!--Name-->
<ValidationProvider tag="div" mode="passive" name="Name" rules="required" v-slot="{ errors }">
<AppInputText :title="$t('name')">
<input
v-model="plan.name"
:placeholder="$t('plan_name')"
type="text"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
</AppInputText>
</ValidationProvider>
<!--Description-->
<ValidationProvider tag="div" mode="passive" name="Description" v-slot="{ errors }">
<AppInputText :title="$t('description_optional')" :is-last="true">
<textarea
v-model="plan.description"
:placeholder="$t('plan_description')"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
maxlength="120"
></textarea>
</AppInputText>
</ValidationProvider>
</div>
<div class="card shadow-card">
<FormLabel>
{{ $t('pricing') }}
</FormLabel>
<div class="justify-items md:flex md:space-x-4">
<!--Price-->
<ValidationProvider
tag="div"
mode="passive"
name="Price"
rules="required"
v-slot="{ errors }"
class="w-full"
>
<AppInputText :title="$t('plan_price')" class="w-full">
<input
v-model="plan.amount"
:placeholder="$t('plan_price')"
type="number"
step="0.01"
min="1"
max="999999999999"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
</AppInputText>
</ValidationProvider>
<!--Currency-->
<ValidationProvider
tag="div"
mode="passive"
name="Currency"
rules="required"
v-slot="{ errors }"
class="w-full"
>
<AppInputText :title="$t('currency')" class="w-full">
<SelectInput
v-model="plan.currency"
:options="currencyList"
:placeholder="$t('select_plan_currency')"
:isError="errors[0]"
/>
</AppInputText>
</ValidationProvider>
</div>
<!--Interval-->
<ValidationProvider tag="div" mode="passive" name="Interval" rules="required" v-slot="{ errors }">
<AppInputText :title="$t('interval')" :is-last="true">
<SelectInput
v-model="plan.interval"
:options="intervalList"
:placeholder="$t('select_billing_interval')"
:isError="errors[0]"
/>
</AppInputText>
</ValidationProvider>
</div>
<div class="card shadow-card">
<FormLabel>
{{ $t('features') }}
</FormLabel>
<!--Storage Capacity-->
<ValidationProvider
tag="div"
mode="passive"
name="Max Storage Capacity"
rules="required"
v-slot="{ errors }"
>
<AppInputText
:title="$t('admin_page_plans.form.storage')"
:description="$t('admin_page_plans.form.storage_helper')"
>
<input
v-model="plan.features.max_storage_amount"
:placeholder="$t('admin_page_plans.form.storage_plac')"
type="number"
min="1"
max="999999999"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
</AppInputText>
</ValidationProvider>
<!--Team Members-->
<ValidationProvider tag="div" mode="passive" name="Max Team Members" rules="required" v-slot="{ errors }">
<AppInputText
:title="$t('team_members')"
:description="$t('zero_for_unlimited_members')"
:is-last="true"
>
<input
v-model="plan.features.max_team_members"
:placeholder="$t('add_max_team_members')"
type="number"
min="1"
max="999999999"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
</AppInputText>
</ValidationProvider>
</div>
<InfoBox v-if="isError" type="error" style="margin-top: 40px">
<p>{{ errorMessage }}</p>
</InfoBox>
<ButtonBase :disabled="isLoading" :loading="isLoading" button-style="theme" type="submit" class="w-full sm:w-auto">
{{ $t('create_plan') }}
</ButtonBase>
</ValidationObserver>
</template>
<script>
import AppInputText from '../../../../components/Forms/Layouts/AppInputText'
import { ValidationProvider, ValidationObserver } from 'vee-validate/dist/vee-validate.full'
import SelectInput from '../../../../components/Inputs/SelectInput'
import ImageInput from '../../../../components/Inputs/ImageInput'
import MobileHeader from '../../../../components/Mobile/MobileHeader'
import FormLabel from '../../../../components/UI/Labels/FormLabel'
import SectionTitle from '../../../../components/UI/Labels/SectionTitle'
import ButtonBase from '../../../../components/UI/Buttons/ButtonBase'
import InfoBox from '../../../../components/UI/Others/InfoBox'
import { required } from 'vee-validate/dist/rules'
import { mapGetters } from 'vuex'
import { events } from '../../../../bus'
import axios from 'axios'
export default {
name: 'CreateFixedPlan',
components: {
ValidationProvider,
ValidationObserver,
SectionTitle,
AppInputText,
MobileHeader,
SelectInput,
ButtonBase,
ImageInput,
FormLabel,
required,
InfoBox,
},
computed: {
...mapGetters(['currencyList', 'intervalList', 'config']),
},
data() {
return {
errorMessage: undefined,
isLoading: false,
isError: false,
plan: {
type: 'fixed',
name: undefined,
description: undefined,
interval: undefined,
amount: undefined,
currency: undefined,
features: {
max_storage_amount: undefined,
max_team_members: undefined,
},
},
}
},
methods: {
async createPlan() {
// Validate fields
const isValid = await this.$refs.createPlan.validate()
if (!isValid) return
// Start loading
this.isLoading = true
axios
.post('/api/subscriptions/admin/plans', this.plan)
.then((response) => {
// Show toaster
events.$emit('toaster', {
type: 'success',
message: this.$t('toaster.plan_created'),
})
// Go to plan page
this.$router.push({
name: 'PlanFixedSettings',
params: { id: response.data.data.id },
})
// Set default state {isEmptyPlans} to false
if (this.config.isEmptyPlans) {
this.$store.commit('REPLACE_CONFIG_VALUE', {
key: 'isEmptyPlans',
value: false,
})
}
})
.catch((error) => {
// Validation errors
if (error.response.status === 422) {
if (error.response.data.errors['max_storage_amount']) {
this.$refs.createPlan.setErrors({
'Max Storage Capacity': this.$t('errors.capacity_digit'),
})
}
}
if (error.response.status === 500 && error.response.data.type) {
events.$emit('alert:open', {
title: error.response.data.title,
message: error.response.data.message,
})
} else if (error.response.status === 500) {
this.isError = true
this.errorMessage = error.response.data.message
}
})
.finally(() => {
this.isLoading = false
})
},
},
}
</script>
@@ -1,351 +0,0 @@
<template>
<ValidationObserver @submit.prevent="createPlan" ref="createPlan" v-slot="{ invalid }" tag="form">
<div class="card shadow-card">
<FormLabel>
{{ $t('details') }}
</FormLabel>
<!--Name-->
<ValidationProvider tag="div" mode="passive" name="Name" rules="required" v-slot="{ errors }">
<AppInputText :title="$t('name')">
<input
v-model="plan.name"
:placeholder="$t('plan_name')"
type="text"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
</AppInputText>
</ValidationProvider>
<!--Description-->
<ValidationProvider tag="div" mode="passive" name="Description" v-slot="{ errors }">
<AppInputText :title="$t('description_optional')">
<textarea
v-model="plan.description"
:placeholder="$t('plan_description')"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
></textarea>
</AppInputText>
</ValidationProvider>
<!--Currency-->
<ValidationProvider tag="div" mode="passive" name="Currency" rules="required" v-slot="{ errors }">
<AppInputText :title="$t('currency')" class="w-full" :is-last="true">
<SelectInput
v-model="plan.currency"
:options="currencyList"
:placeholder="$t('select_plan_currency')"
:isError="errors[0]"
/>
</AppInputText>
</ValidationProvider>
</div>
<div class="card shadow-card">
<FormLabel>
{{ $t('charged_features') }}
</FormLabel>
<!--Bandwidth-->
<div>
<AppInputSwitch
:title="$t('bandwidth_per_gb')"
:description="$t('bandwidth_per_gb_note')"
>
<SwitchInput
v-model="plan.features.bandwidth.active"
class="switch"
:state="plan.features.bandwidth.active"
/>
</AppInputSwitch>
<ValidationProvider
v-if="plan.features.bandwidth.active"
class="-mt-3"
tag="div"
mode="passive"
name="Bandwidth Price"
rules="required"
v-slot="{ errors }"
>
<AppInputText class="w-full">
<input
v-model="plan.features.bandwidth.per_unit"
:placeholder="$t('type_bandwidth_price')"
type="number"
step="0.01"
min="0.01"
max="999999999999"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
</AppInputText>
</ValidationProvider>
</div>
<!--Storage-->
<div>
<AppInputSwitch
:title="$t('storage_per_gb')"
:description="$t('storage_per_gb_note')"
>
<SwitchInput
v-model="plan.features.storage.active"
class="switch"
:state="plan.features.storage.active"
/>
</AppInputSwitch>
</div>
<ValidationProvider
v-if="plan.features.storage.active"
class="-mt-3"
tag="div"
mode="passive"
name="Storage Price"
rules="required"
v-slot="{ errors }"
>
<AppInputText class="w-full">
<input
v-model="plan.features.storage.per_unit"
:placeholder="$t('type_storage_price')"
type="number"
step="0.01"
min="0.01"
max="999999999999"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
</AppInputText>
</ValidationProvider>
<!--Member-->
<div>
<AppInputSwitch
:title="$t('member_per_unit')"
:description="$t('member_per_unit_note')"
>
<SwitchInput
v-model="plan.features.member.active"
class="switch"
:state="plan.features.member.active"
/>
</AppInputSwitch>
</div>
<ValidationProvider
v-if="plan.features.member.active"
class="-mt-3"
tag="div"
mode="passive"
name="Member Price"
rules="required"
v-slot="{ errors }"
>
<AppInputText class="w-full">
<input
v-model="plan.features.member.per_unit"
:placeholder="$t('type_member_price')"
type="number"
step="0.01"
min="0.01"
max="999999999999"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
</AppInputText>
</ValidationProvider>
<!--Flat Fee-->
<div>
<AppInputSwitch
:title="$t('flat_fee_unit_gb')"
:description="$t('flat_fee_unit_gb_note')"
:is-last="!plan.features.flatFee.active"
>
<SwitchInput
v-model="plan.features.flatFee.active"
class="switch"
:state="plan.features.flatFee.active"
/>
</AppInputSwitch>
<ValidationProvider
v-if="plan.features.flatFee.active"
class="-mt-3"
tag="div"
mode="passive"
name="FlatFee Price"
rules="required"
v-slot="{ errors }"
>
<AppInputText class="w-full" :is-last="true">
<input
v-model="plan.features.flatFee.per_unit"
:placeholder="$t('type_flat_fee_price')"
type="number"
step="0.01"
min="0.01"
max="999999999999"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
</AppInputText>
</ValidationProvider>
</div>
</div>
<ButtonBase :disabled="isLoading" :loading="isLoading" button-style="theme" type="submit" class="w-full sm:w-auto">
{{ $t('create_plan') }}
</ButtonBase>
</ValidationObserver>
</template>
<script>
import SwitchInput from '../../../../components/Inputs/SwitchInput'
import AppInputSwitch from '../../../../components/Forms/Layouts/AppInputSwitch'
import AppInputText from '../../../../components/Forms/Layouts/AppInputText'
import {ValidationProvider, ValidationObserver} from 'vee-validate/dist/vee-validate.full'
import SelectInput from '../../../../components/Inputs/SelectInput'
import ImageInput from '../../../../components/Inputs/ImageInput'
import MobileHeader from '../../../../components/Mobile/MobileHeader'
import FormLabel from '../../../../components/UI/Labels/FormLabel'
import SectionTitle from '../../../../components/UI/Labels/SectionTitle'
import ButtonBase from '../../../../components/UI/Buttons/ButtonBase'
import InfoBox from '../../../../components/UI/Others/InfoBox'
import {required} from 'vee-validate/dist/rules'
import {mapGetters} from 'vuex'
import {events} from '../../../../bus'
import axios from 'axios'
export default {
name: 'CreateMeteredPlan',
components: {
ValidationProvider,
ValidationObserver,
AppInputSwitch,
SwitchInput,
SectionTitle,
AppInputText,
MobileHeader,
SelectInput,
ButtonBase,
ImageInput,
FormLabel,
required,
InfoBox,
},
computed: {
...mapGetters(['currencyList', 'intervalList', 'config']),
},
data() {
return {
errorMessage: undefined,
isLoading: false,
isError: false,
plan: {
type: 'fixed',
name: undefined,
description: undefined,
currency: undefined,
features: {
bandwidth: {
active: false,
per_unit: undefined,
first_unit: 1,
aggregate_strategy: 'sum_of_usage',
},
storage: {
active: false,
per_unit: undefined,
first_unit: 1,
aggregate_strategy: 'maximum_usage',
},
member: {
active: false,
per_unit: undefined,
first_unit: 1,
aggregate_strategy: 'maximum_usage',
},
flatFee: {
active: false,
per_unit: undefined,
aggregate_strategy: 'maximum_usage',
},
},
},
}
},
methods: {
async createPlan() {
let tiers = []
Object.entries(this.plan.features).forEach(([key, feature]) => {
if (feature.active) {
tiers.push({
aggregate_strategy: feature.aggregate_strategy,
key: key,
tiers: [
{
per_unit: feature.per_unit,
first_unit: 1,
flat_fee: null,
last_unit: null,
},
],
})
}
})
// Validate fields
const isValid = await this.$refs.createPlan.validate()
if (!isValid) return
// Start loading
this.isLoading = true
axios
.post('/api/subscriptions/admin/plans', {
type: 'metered',
name: this.plan.name,
description: this.plan.description,
currency: this.plan.currency,
meters: tiers,
})
.then((response) => {
events.$emit('toaster', {
type: 'success',
message: this.$t('toaster.plan_created'),
})
// Go to plan page
this.$router.push({
name: 'PlanMeteredSettings',
params: {id: response.data.data.id},
})
// Set default state {isEmptyPlans} to false
if (this.config.isEmptyPlans) {
this.$store.commit('REPLACE_CONFIG_VALUE', {
key: 'isEmptyPlans',
value: false,
})
}
})
.catch((error) => {
events.$emit('toaster', {
type: 'danger',
message: this.$t('popup_error.title'),
})
})
.finally(() => {
this.isLoading = false
})
},
},
}
</script>
@@ -1,67 +0,0 @@
<template>
<div>
<div v-if="plan" class="card sticky top-0 z-10 shadow-card" style="padding-bottom: 0">
<div class="mb-2">
<h1 class="text-lg font-bold sm:text-xl">
{{ plan.attributes.name }}
</h1>
<small class="text-xs font-bold dark:text-gray-500 text-gray-500 sm:text-sm">
{{ plan.attributes.price }} /
{{ $t(`interval.${plan.attributes.interval}`) }}
</small>
</div>
<CardNavigation :pages="pages" class="-mx-1.5" />
</div>
<router-view v-if="!isLoading" :plan="plan" />
<div id="loader" v-if="isLoading">
<Spinner />
</div>
</div>
</template>
<script>
import CardNavigation from '../../../components/UI/Others/CardNavigation'
import Spinner from '../../../components/UI/Others/Spinner'
import axios from 'axios'
export default {
name: 'FixedPlan',
components: {
CardNavigation,
Spinner,
},
data() {
return {
isLoading: true,
plan: undefined,
pages: [
{
title: this.$t('settings'),
route: 'PlanFixedSettings',
},
{
title: this.$t('subscribers'),
route: 'PlanFixedSubscribers',
},
{
title: this.$t('delete_plan'),
route: 'PlanFixedDelete',
},
],
}
},
created() {
axios
.get('/api/subscriptions/admin/plans/' + this.$route.params.id)
.then((response) => {
this.plan = response.data.data
})
.finally(() => {
this.isLoading = false
})
},
}
</script>
@@ -1,78 +0,0 @@
<template>
<div>
<div v-if="plan" class="card sticky top-0 z-10 shadow-card" style="padding-bottom: 0">
<div class="mb-2">
<h1 class="text-lg font-bold sm:text-xl">
{{ plan.attributes.name }}
</h1>
<small class="text-xs font-bold dark:text-gray-500 text-gray-500 sm:text-sm">
{{ $t('x_days_intervals') }}
</small>
</div>
<!--Navigation-->
<CardNavigation :pages="pages" class="-mx-1" />
</div>
<router-view v-if="!isLoading" :plan="plan" />
<div id="loader" v-if="isLoading">
<Spinner />
</div>
</div>
</template>
<script>
import CardNavigation from '../../../components/UI/Others/CardNavigation'
import Spinner from '../../../components/UI/Others/Spinner'
import axios from 'axios'
import { mapGetters } from 'vuex'
export default {
name: 'MeteredPlan',
components: {
CardNavigation,
Spinner,
},
computed: {
...mapGetters(['config']),
pages() {
let pages = [
{
title: this.$t('settings'),
route: 'PlanMeteredSettings',
},
{
title: this.$t('subscribers'),
route: 'PlanMeteredSubscribers',
},
]
if (this.plan && this.plan.attributes.status === 'active') {
pages.push({
title: this.$t('delete_plan'),
route: 'PlanMeteredDelete',
})
}
return pages
},
},
data() {
return {
isLoading: true,
plan: undefined,
}
},
created() {
axios
.get('/api/subscriptions/admin/plans/' + this.$route.params.id)
.then((response) => {
this.plan = response.data.data
})
.finally(() => {
this.isLoading = false
})
},
}
</script>
@@ -1,118 +0,0 @@
<template>
<div class="card shadow-card">
<FormLabel>
{{ $t('delete_plan') }}
</FormLabel>
<ValidationObserver ref="deletePlan" @submit.prevent="deletePlan" v-slot="{ invalid }" tag="form">
<ValidationProvider
tag="div"
v-slot="{ errors }"
mode="passive"
name="Plan name"
:rules="'required|is:' + plan.attributes.name"
>
<AppInputText
:title="
$t('admin_page_user.label_delete_user', {
user: plan.attributes.name,
})
"
:description="$t('admin_page_plans.disclaimer_delete_plan')"
:error="errors[0]"
:is-last="true"
>
<div class="space-y-4 sm:flex sm:space-x-4 sm:space-y-0">
<input
v-model="planName"
:placeholder="$t('type_plan_name')"
type="text"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
<ButtonBase
:loading="isSendingRequest"
:disabled="isSendingRequest"
type="submit"
button-style="danger"
class="w-full sm:w-auto"
>
{{ $t('delete_plan') }}
</ButtonBase>
</div>
</AppInputText>
</ValidationProvider>
</ValidationObserver>
</div>
</template>
<script>
import AppInputText from '../../../../components/Forms/Layouts/AppInputText'
import FormLabel from '../../../../components/UI/Labels/FormLabel'
import InfoBox from '../../../../components/UI/Others/InfoBox'
import { ValidationProvider, ValidationObserver } from 'vee-validate/dist/vee-validate.full'
import ButtonBase from '../../../../components/UI/Buttons/ButtonBase'
import { required, is } from 'vee-validate/dist/rules'
import { events } from '../../../../bus'
import axios from 'axios'
export default {
name: 'PlanDelete',
props: ['plan'],
components: {
ValidationProvider,
ValidationObserver,
AppInputText,
ButtonBase,
FormLabel,
required,
InfoBox,
},
data() {
return {
isSendingRequest: false,
isLoading: false,
planName: '',
}
},
methods: {
async deletePlan() {
// Validate fields
const isValid = await this.$refs.deletePlan.validate()
if (!isValid) return
this.isSendingRequest = true
axios
.post(`/api/subscriptions/admin/plans/${this.$route.params.id}`, {
_method: 'delete',
})
.then(() => {
// If metered subscription, then set isEmptyPlans to true
if (this.$store.getters.config.subscriptionType === 'metered') {
this.$store.commit('REPLACE_CONFIG_VALUE', {
key: 'isEmptyPlans',
value: true,
})
}
events.$emit('toaster', {
type: 'success',
message: this.$t('popup_deleted_plan.title'),
})
this.$router.push({ name: 'Plans' })
})
.catch(() => {
events.$emit('toaster', {
type: 'danger',
message: this.$t('popup_error.title'),
})
})
.finally(() => {
this.isSendingRequest = false
})
},
},
}
</script>
@@ -1,143 +0,0 @@
<template>
<div>
<div class="card shadow-card">
<FormLabel>
{{ $t('details') }}
</FormLabel>
<!--Visible-->
<AppInputSwitch
:title="$t('status')"
:description="$t('admin_page_plans.form.status_help')"
>
<SwitchInput
@input="
$updateInput(
'/subscriptions/admin/plans/' + $route.params.id,
'visible',
plan.attributes.visible
)
"
v-model="plan.attributes.visible"
class="switch"
:state="plan.attributes.visible"
/>
</AppInputSwitch>
<!--Name-->
<AppInputText :title="$t('name')">
<input
@input="
$updateInput('/subscriptions/admin/plans/' + $route.params.id, 'name', plan.attributes.name)
"
v-model="plan.attributes.name"
:placeholder="$t('plan_name')"
type="text"
class="focus-border-theme input-dark"
/>
</AppInputText>
<!--Description-->
<AppInputText :title="$t('description_optional')">
<textarea
@input="
$updateInput(
'/subscriptions/admin/plans/' + $route.params.id,
'description',
plan.attributes.description
)
"
v-model="plan.attributes.description"
:placeholder="$t('plan_description')"
class="focus-border-theme input-dark"
></textarea>
</AppInputText>
<InfoBox style="margin-bottom: 0">
<p>
{{
$t(
'price_change_not_possible_create_new'
)
}}
</p>
</InfoBox>
</div>
<div class="card shadow-card">
<FormLabel>
{{ $t('features') }}
</FormLabel>
<!--Storage Capacity-->
<AppInputText
:title="$t('admin_page_plans.form.storage')"
:description="$t('admin_page_plans.form.storage_helper')"
>
<input
@input="
$updateInput(
`/subscriptions/admin/plans/${$route.params.id}/features`,
'max_storage_amount',
plan.attributes.features.max_storage_amount
)
"
v-model="plan.attributes.features.max_storage_amount"
:placeholder="$t('admin_page_plans.form.storage_plac')"
type="number"
min="1"
max="999999999"
class="focus-border-theme input-dark"
/>
</AppInputText>
<!--Team Members-->
<AppInputText :title="$t('max_team_members')" :description="$t('zero_for_unlimited_members')" is-last="true">
<input
@input="
$updateInput(
`/subscriptions/admin/plans/${$route.params.id}/features`,
'max_team_members',
plan.attributes.features.max_team_members
)
"
v-model="plan.attributes.features.max_team_members"
:placeholder="$t('add_max_team_members')"
type="number"
min="1"
max="999999999"
class="focus-border-theme input-dark"
/>
</AppInputText>
</div>
</div>
</template>
<script>
import SwitchInput from '../../../../components/Inputs/SwitchInput'
import SelectInput from '../../../../components/Inputs/SelectInput'
import AppInputSwitch from '../../../../components/Forms/Layouts/AppInputSwitch'
import FormLabel from '../../../../components/UI/Labels/FormLabel'
import AppInputText from '../../../../components/Forms/Layouts/AppInputText'
import InfoBox from '../../../../components/UI/Others/InfoBox'
export default {
name: 'PlanFixedSettings',
props: ['plan'],
components: {
AppInputSwitch,
AppInputText,
SwitchInput,
SelectInput,
FormLabel,
InfoBox,
},
data() {
return {
visible: undefined,
}
},
created() {
this.visible = this.plan.attributes.visible
},
}
</script>
@@ -1,160 +0,0 @@
<template>
<div>
<div class="card shadow-card">
<FormLabel>
{{ $t('details') }}
</FormLabel>
<!--Name-->
<AppInputText :title="$t('name')">
<input
@input="
$updateInput('/subscriptions/admin/plans/' + $route.params.id, 'name', plan.attributes.name)
"
v-model="plan.attributes.name"
:placeholder="$t('plan_name')"
type="text"
class="focus-border-theme input-dark"
/>
</AppInputText>
<!--Description-->
<AppInputText :title="$t('description_optional')" :is-last="true">
<textarea
@input="
$updateInput(
'/subscriptions/admin/plans/' + $route.params.id,
'description',
plan.attributes.description
)
"
v-model="plan.attributes.description"
:placeholder="$t('plan_description')"
class="focus-border-theme input-dark"
></textarea>
</AppInputText>
</div>
<div class="card shadow-card">
<FormLabel>
{{ $t('charged_features') }}
</FormLabel>
<!--Bandwidth-->
<AppInputText
v-if="plan.attributes.features.bandwidth"
:title="$t('bandwidth_per_gb')"
:description="$t('bandwidth_per_gb_note')"
class="w-full"
>
<input
:value="
formatCurrency(plan.attributes.currency, plan.attributes.features.bandwidth.tiers[0].per_unit)
"
type="text"
class="focus-border-theme input-dark"
disabled
/>
</AppInputText>
<!--Storage-->
<AppInputText
v-if="plan.attributes.features.storage"
:title="$t('storage_per_gb')"
:description="$t('storage_per_gb_note')"
class="w-full"
>
<input
:value="
formatCurrency(plan.attributes.currency, plan.attributes.features.storage.tiers[0].per_unit)
"
type="text"
class="focus-border-theme input-dark"
disabled
/>
</AppInputText>
<!--Member-->
<AppInputText
v-if="plan.attributes.features.member"
:title="$t('member_per_unit')"
:description="$t('member_per_unit_note')"
class="w-full"
>
<input
:value="formatCurrency(plan.attributes.currency, plan.attributes.features.member.tiers[0].per_unit)"
type="text"
class="focus-border-theme input-dark"
disabled
/>
</AppInputText>
<!--Flat Fee-->
<AppInputText
v-if="plan.attributes.features.flatFee"
:title="$t('flat_fee_unit_gb')"
:description="$t('flat_fee_unit_gb_note')"
class="w-full"
>
<input
:value="
formatCurrency(plan.attributes.currency, plan.attributes.features.flatFee.tiers[0].per_unit)
"
type="text"
class="focus-border-theme input-dark"
disabled
/>
</AppInputText>
<InfoBox style="margin-bottom: 0">
<p>
{{
$t(
'price_change_not_possible_create_new'
)
}}
</p>
</InfoBox>
</div>
</div>
</template>
<script>
import SwitchInput from '../../../../components/Inputs/SwitchInput'
import SelectInput from '../../../../components/Inputs/SelectInput'
import AppInputSwitch from '../../../../components/Forms/Layouts/AppInputSwitch'
import FormLabel from '../../../../components/UI/Labels/FormLabel'
import AppInputText from '../../../../components/Forms/Layouts/AppInputText'
import InfoBox from '../../../../components/UI/Others/InfoBox'
export default {
name: 'PlanMeteredSettings',
props: ['plan'],
components: {
AppInputSwitch,
AppInputText,
SwitchInput,
SelectInput,
FormLabel,
InfoBox,
},
data() {
return {
visible: undefined,
}
},
methods: {
formatCurrency(currency, amount) {
// TODO: add user locale
let formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency,
})
return formatter.format(amount)
},
},
created() {
this.visible = this.plan.attributes.visible
},
}
</script>
@@ -1,215 +0,0 @@
<template>
<PageTab :is-loading="isLoading">
<DatatableWrapper
@data="subscribers = $event"
@init="isLoading = false"
:api="`/api/subscriptions/admin/plans/${this.$route.params.id}/subscribers`"
:paginator="true"
:columns="columns"
class="card overflow-x-auto shadow-card"
>
<!--Table data content-->
<template slot-scope="{ row }">
<tr
v-if="config.subscriptionType === 'metered'"
class="whitespace-nowrap border-b border-dashed border-light dark:border-opacity-5"
>
<td class="py-3 pr-3 md:pr-1">
<router-link
class="flex items-center"
:to="{
name: 'UserDetail',
params: {
id: row.data.relationships.user.data.id,
},
}"
>
<MemberAvatar :is-border="false" :size="36" :member="row.data.relationships.user" />
<div class="ml-3 pr-10">
<b
class="max-w-1 block overflow-hidden text-ellipsis whitespace-nowrap text-sm font-bold"
style="max-width: 155px"
>
{{ row.data.relationships.user.data.attributes.name }}
</b>
<span class="block text-xs text-gray-600 dark:text-gray-500">
{{ row.data.relationships.user.data.attributes.email }}
</span>
</div>
</router-link>
</td>
<td class="px-3 md:px-1">
<span class="whitespace-nowrap text-sm font-bold">
{{ row.data.attributes.renews_at }}
</span>
</td>
<td class="pl-3 text-right md:pl-1">
<img
class="inline-block max-h-5"
:src="$getPaymentLogo(row.data.attributes.driver)"
:alt="row.data.attributes.driver"
/>
</td>
</tr>
<tr
v-if="config.subscriptionType === 'fixed'"
class="whitespace-nowrap border-b border-dashed border-light dark:border-opacity-5"
>
<td class="py-3 pr-3 md:pr-1">
<router-link
class="flex items-center"
:to="{
name: 'UserDetail',
params: {
id: row.data.relationships.user.data.id,
},
}"
>
<MemberAvatar :is-border="false" :size="36" :member="row.data.relationships.user" />
<div class="ml-3 pr-10">
<b
class="max-w-1 block overflow-hidden text-ellipsis whitespace-nowrap text-sm font-bold"
style="max-width: 155px"
>
{{ row.data.relationships.user.data.attributes.name }}
</b>
<span class="block text-xs text-gray-600 dark:text-gray-500">
{{ row.data.relationships.user.data.attributes.email }}
</span>
</div>
</router-link>
</td>
<td class="px-3 md:px-1">
<ColorLabel :color="$getSubscriptionStatusColor(row.data.attributes.status)">
{{ $t(row.data.attributes.status) }}
</ColorLabel>
</td>
<td class="px-3 md:px-1">
<span class="text-limit text-sm font-bold capitalize" style="max-width: 160px">
{{ row.data.attributes.name }}
</span>
</td>
<td class="px-3 md:px-1">
<span class="text-sm font-bold">
<!--todo: update renew attribute-->
{{
row.data.attributes.renews_at
? row.data.attributes.renews_at
: row.data.attributes.created_at
}}
</span>
</td>
<td class="px-3 md:px-1">
<span class="text-sm font-bold">
{{ row.data.attributes.ends_at ? row.data.attributes.ends_at : '-' }}
</span>
</td>
<td class="pl-3 text-right md:pl-1">
<img
class="inline-block max-h-5"
:src="$getPaymentLogo(row.data.attributes.driver)"
:alt="row.data.attributes.driver"
/>
</td>
</tr>
</template>
<!--Empty page-->
<template v-slot:empty-page>
<InfoBox style="margin-bottom: 0">
<p>{{ $t('admin_page_plans.subscribers.empty') }}</p>
</InfoBox>
</template>
</DatatableWrapper>
</PageTab>
</template>
<script>
import ColorLabel from '../../../../components/UI/Labels/ColorLabel'
import MemberAvatar from '../../../../components/UI/Others/MemberAvatar'
import DatatableCellImage from '../../../../components/UI/Table/DatatableCellImage'
import { DownloadCloudIcon, Edit2Icon, Trash2Icon } from 'vue-feather-icons'
import DatatableWrapper from '../../../../components/UI/Table/DatatableWrapper'
import PageTabGroup from '../../../../components/Layout/PageTabGroup'
import PageTab from '../../../../components/Layout/PageTab'
import InfoBox from '../../../../components/UI/Others/InfoBox'
import { mapGetters } from 'vuex'
export default {
name: 'PlanSubscribers',
components: {
DatatableCellImage,
DownloadCloudIcon,
DatatableWrapper,
PageTabGroup,
MemberAvatar,
ColorLabel,
Trash2Icon,
Edit2Icon,
PageTab,
InfoBox,
},
computed: {
...mapGetters(['config']),
columns() {
return {
metered: [
{
label: this.$t('user'),
field: 'user_id',
sortable: true,
},
{
label: this.$t('renews_at'),
field: 'created_at',
sortable: true,
},
{
label: this.$t('service'),
field: 'driver',
sortable: true,
},
],
fixed: [
{
label: this.$t('user'),
field: 'user_id',
sortable: true,
},
{
label: this.$t('status'),
field: 'status',
sortable: true,
},
{
label: this.$t('note'),
field: 'plan.name',
sortable: true,
},
{
label: this.$t('renews_at'),
field: 'created_at',
sortable: true,
},
{
label: this.$t('ends_at'),
field: 'ends_at',
sortable: true,
},
{
label: this.$t('service'),
field: 'driver',
sortable: true,
},
],
}[this.config.subscriptionType]
},
},
data() {
return {
subscribers: undefined,
isLoading: true,
}
},
}
</script>
@@ -25,22 +25,10 @@ export default {
title: this.$t('admin_settings.tabs.others'),
route: 'AppOthers',
},
{
title: this.$t('login_and_registration'),
route: 'AppSignInUp',
},
{
title: this.$t('appearance'),
route: 'AppAppearance',
},
{
title: this.$t('Adsense'),
route: 'AppAdsense',
},
{
title: this.$t('homepage'),
route: 'AppIndex',
},
{
title: this.$t('environment'),
route: 'AppEnvironment',
@@ -1,126 +0,0 @@
<template>
<PageTab>
<!--Adsense basic setup-->
<div v-if="adsense" class="card shadow-card">
<FormLabel>
{{ $t('Basic Setup') }}
</FormLabel>
<AppInputSwitch :title="$t('Allow Google Adsense')" :description="$t('Allow ads on app pages.')">
<SwitchInput
@input="$updateText('/admin/settings', 'allowed_adsense', adsense.allowedService)"
v-model="adsense.allowedService"
class="switch"
:state="adsense.allowedService"
/>
</AppInputSwitch>
<AppInputText
:title="$t('Client Id')"
:description="$t('Paste your Adsense Client ID e.g. ca-pub-XXXXXXXXXXXXXXXXX')"
:is-last="true"
>
<input
@input="$updateText('/admin/settings', 'adsense_client_id', adsense.clientId, true)"
v-model="adsense.clientId"
:placeholder="$t('Client Id...')"
type="text"
class="focus-border-theme input-dark"
/>
</AppInputText>
</div>
<!--Adsense places-->
<div v-if="adsense" class="card shadow-card">
<FormLabel>
{{ $t('Ads') }}
</FormLabel>
<AppInputText
:title="$t('File Viewport Banner')"
:description="$t('This banner will be showed above user files')"
>
<textarea
rows="3"
@input="$updateText('/admin/settings', 'adsense_banner_01', adsense.banner01, true)"
v-model="adsense.banner01"
:placeholder="$t('Paste the <ins></ins> tag here...')"
type="text"
class="focus-border-theme input-dark"
/>
</AppInputText>
<AppInputText
:title="$t('Download Page Banner')"
:description="$t('This banner will be showed below file download page')"
>
<textarea
rows="3"
@input="$updateText('/admin/settings', 'adsense_banner_02', adsense.banner02, true)"
v-model="adsense.banner02"
:placeholder="$t('Paste the <ins></ins> tag here...')"
type="text"
class="focus-border-theme input-dark"
/>
</AppInputText>
<AppInputText
:title="$t('Homepage Banner')"
:description="$t('This banner will be showed on the homepage')"
:is-last="true"
>
<textarea
rows="3"
@input="$updateText('/admin/settings', 'adsense_banner_03', adsense.banner03, true)"
v-model="adsense.banner03"
:placeholder="$t('Paste the <ins></ins> tag here...')"
type="text"
class="focus-border-theme input-dark"
/>
</AppInputText>
</div>
</PageTab>
</template>
<script>
import SwitchInput from '../../../../components/Inputs/SwitchInput'
import AppInputButton from '../../../../components/Forms/Layouts/AppInputButton'
import AppInputSwitch from '../../../../components/Forms/Layouts/AppInputSwitch'
import FormLabel from '../../../../components/UI/Labels/FormLabel'
import AppInputText from '../../../../components/Forms/Layouts/AppInputText'
import PageTab from '../../../../components/Layout/PageTab'
import { mapGetters } from 'vuex'
export default {
name: 'Adsense',
components: {
AppInputButton,
AppInputSwitch,
AppInputText,
SwitchInput,
FormLabel,
PageTab,
},
computed: {
...mapGetters(['config']),
},
data() {
return {
adsense: {
allowedService: undefined,
clientId: undefined,
banner01: undefined,
},
}
},
created() {
this.adsense = {
allowedService: this.config.allowedAdsense,
clientId: this.config.adsenseClientId,
banner01: this.config.adsenseBanner01,
banner02: this.config.adsenseBanner02,
banner03: this.config.adsenseBanner03,
}
},
}
</script>
@@ -1,26 +1,5 @@
<template>
<PageTab :is-loading="isLoading">
<!--Broadcasting setup-->
<ValidationObserver
@submit.prevent="broadcastSetupSubmit"
ref="broadcastSetup"
v-slot="{ invalid }"
tag="form"
class="card shadow-card"
>
<BroadcastSetup v-model="broadcast" />
<ButtonBase
:loading="isSendingBroadcastForm"
:disabled="isSendingBroadcastForm"
type="submit"
button-style="theme"
class="mt-6 w-full sm:mt-7 sm:w-auto"
>
{{ $t('save_broadcast_settings') }}
</ButtonBase>
</ValidationObserver>
<!--Storage setup-->
<ValidationObserver
@submit.prevent="storageSetupSubmit"
@@ -76,12 +55,10 @@ import PageTab from '../../../../components/Layout/PageTab'
import MailSetup from '../../../../components/Forms/MailSetup'
import { events } from '../../../../bus'
import axios from 'axios'
import BroadcastSetup from "../../../../components/Forms/BroadcastSetup";
export default {
name: 'AppEnvironment',
components: {
BroadcastSetup,
ValidationObserver,
ValidationProvider,
StorageSetup,
@@ -99,40 +76,9 @@ export default {
isLoading: false,
isSendingEmailForm: false,
isSendingStorageForm: false,
isSendingBroadcastForm: false,
broadcast: undefined,
}
},
methods: {
async broadcastSetupSubmit() {
// Validate fields
const isValid = await this.$refs.broadcastSetup.validate()
if (!isValid) return
// Start loading
this.isSendingBroadcastForm = true
axios
.post('/api/admin/settings/broadcast', { ...this.broadcast })
.then(() => {
events.$emit('toaster', {
type: 'success',
message: this.$t('broadcast_driver_updated'),
})
})
.catch(() => {
events.$emit('toaster', {
type: 'danger',
message: this.$t('popup_error.title'),
})
})
.finally(() => {
this.isSendingBroadcastForm = false
this.broadcast = undefined
})
},
async storageSetupSubmit() {
// Validate fields
const isValid = await this.$refs.storageSetup.validate()
@@ -1,588 +0,0 @@
<template>
<PageTab :is-loading="isLoading">
<PageTabGroup v-if="app">
<div class="form block-form">
<div class="card shadow-card">
<FormLabel>
{{ $t('homepage') }}
</FormLabel>
<AppInputSwitch
:title="$t('allow_homepage')"
:description="$t('allow_homepage_note')"
:is-last="true"
>
<SwitchInput
@input="$updateText('/admin/settings', 'allow_homepage', app.allow_homepage)"
v-model="app.allow_homepage"
class="switch"
:state="app.allow_homepage"
/>
</AppInputSwitch>
</div>
<!--Header-->
<div class="card shadow-card">
<FormLabel>Header Title</FormLabel>
<div class="block-wrapper">
<img src="/assets/images/admin/main-header.jpg" alt="Main Header" class="page-image" />
</div>
<div class="block-wrapper">
<label>Title:</label>
<ValidationProvider
tag="div"
mode="passive"
name="App Title"
rules="required"
v-slot="{ errors }"
>
<input
@input="$updateText('/admin/settings', 'header_title', app.header_title)"
v-model="app.header_title"
type="text"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>Description:</label>
<ValidationProvider
tag="div"
mode="passive"
name="App Description"
rules="required"
v-slot="{ errors }"
>
<textarea
@input="$updateText('/admin/settings', 'header_description', app.header_description)"
rows="2"
v-model="app.header_description"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
></textarea>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
</div>
<!--Features title-->
<div class="card shadow-card">
<FormLabel>Features Title</FormLabel>
<div class="block-wrapper">
<div>
<div class="inline-wrapper">
<div class="switch-label">
<label class="input-label"> Show section: </label>
</div>
<SwitchInput
@input="$updateText('/admin/settings', 'section_features', app.section_features)"
v-model="app.section_features"
class="switch"
:state="app.section_features"
/>
</div>
</div>
</div>
<div v-if="app.section_features">
<div class="block-wrapper">
<img src="/assets/images/admin/main-features.jpg" alt="Main Features" class="page-image" />
</div>
<div class="block-wrapper">
<label>Title:</label>
<ValidationProvider
tag="div"
mode="passive"
name="App Title"
rules="required"
v-slot="{ errors }"
>
<input
@input="$updateText('/admin/settings', 'features_title', app.features_title)"
v-model="app.features_title"
type="text"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>Description:</label>
<ValidationProvider
tag="div"
mode="passive"
name="App Description"
rules="required"
v-slot="{ errors }"
>
<textarea
@input="
$updateText('/admin/settings', 'features_description', app.features_description)
"
rows="2"
v-model="app.features_description"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
></textarea>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
</div>
</div>
<!--Feature boxes-->
<div class="card shadow-card">
<FormLabel>Feature Boxes</FormLabel>
<div class="block-wrapper">
<div>
<div class="inline-wrapper">
<div class="switch-label">
<label class="input-label"> Show section: </label>
</div>
<SwitchInput
@input="
$updateText(
'/admin/settings',
'section_feature_boxes',
app.section_feature_boxes
)
"
v-model="app.section_feature_boxes"
class="switch"
:state="app.section_feature_boxes"
/>
</div>
</div>
</div>
<div v-if="app.section_feature_boxes">
<div class="block-wrapper">
<img src="/assets/images/admin/feature-boxes.jpg" alt="Main Features" class="page-image" />
</div>
<div class="block-wrapper">
<label>First Box Title:</label>
<ValidationProvider
tag="div"
mode="passive"
name="Feature Title 1"
rules="required"
v-slot="{ errors }"
>
<input
@input="$updateText('/admin/settings', 'feature_title_1', app.feature_title_1)"
v-model="app.feature_title_1"
type="text"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>First Box Description:</label>
<ValidationProvider
tag="div"
mode="passive"
name="Feature Description 1"
rules="required"
v-slot="{ errors }"
>
<textarea
@input="
$updateText(
'/admin/settings',
'feature_description_1',
app.feature_description_1
)
"
rows="2"
v-model="app.feature_description_1"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
></textarea>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>Second Box Title:</label>
<ValidationProvider
tag="div"
mode="passive"
name="Feature Title 2"
rules="required"
v-slot="{ errors }"
>
<input
@input="$updateText('/admin/settings', 'feature_title_2', app.feature_title_2)"
v-model="app.feature_title_2"
type="text"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>Second Box Description:</label>
<ValidationProvider
tag="div"
mode="passive"
name="Feature Description 2"
rules="required"
v-slot="{ errors }"
>
<textarea
@input="
$updateText(
'/admin/settings',
'feature_description_2',
app.feature_description_2
)
"
rows="2"
v-model="app.feature_description_2"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
></textarea>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>Third Box Title:</label>
<ValidationProvider
tag="div"
mode="passive"
name="Feature Title 3"
rules="required"
v-slot="{ errors }"
>
<input
@input="$updateText('/admin/settings', 'feature_title_3', app.feature_title_3)"
v-model="app.feature_title_3"
type="text"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>Third Box Description:</label>
<ValidationProvider
tag="div"
mode="passive"
name="Feature Description 3"
rules="required"
v-slot="{ errors }"
>
<textarea
@input="
$updateText(
'/admin/settings',
'feature_description_3',
app.feature_description_3
)
"
rows="2"
v-model="app.feature_description_3"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
></textarea>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
</div>
</div>
<!--Pricing Content-->
<div v-if="config.isSaaS" class="card shadow-card">
<FormLabel>Pricing Content</FormLabel>
<div class="block-wrapper">
<div>
<div class="inline-wrapper">
<div class="switch-label">
<label class="input-label"> Show section: </label>
</div>
<SwitchInput
@input="
$updateText(
'/admin/settings',
'section_pricing_content',
app.section_pricing_content
)
"
v-model="app.section_pricing_content"
class="switch"
:state="app.section_pricing_content"
/>
</div>
</div>
</div>
<div v-if="app.section_pricing_content">
<div class="block-wrapper">
<img
src="/assets/images/admin/pricing-content.jpg"
alt="Main Features"
class="page-image"
/>
</div>
<div class="block-wrapper">
<label>Title:</label>
<ValidationProvider
tag="div"
mode="passive"
name="App Title"
rules="required"
v-slot="{ errors }"
>
<input
@input="$updateText('/admin/settings', 'pricing_title', app.pricing_title)"
v-model="app.pricing_title"
type="text"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>Description:</label>
<ValidationProvider
tag="div"
mode="passive"
name="App Description"
rules="required"
v-slot="{ errors }"
>
<textarea
@input="
$updateText('/admin/settings', 'pricing_description', app.pricing_description)
"
rows="2"
v-model="app.pricing_description"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
></textarea>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
</div>
</div>
<!--Get Started-->
<div class="card shadow-card">
<FormLabel>Get Started Content</FormLabel>
<div class="block-wrapper">
<div>
<div class="inline-wrapper">
<div class="switch-label">
<label class="input-label"> Show section: </label>
</div>
<SwitchInput
@input="
$updateText('/admin/settings', 'section_get_started', app.section_get_started)
"
v-model="app.section_get_started"
class="switch"
:state="app.section_get_started"
/>
</div>
</div>
</div>
<div v-if="app.section_get_started">
<div class="block-wrapper">
<img
src="/assets/images/admin/get-started-content.jpg"
alt="Main Features"
class="page-image"
/>
</div>
<div class="block-wrapper">
<label>Title:</label>
<ValidationProvider
tag="div"
mode="passive"
name="App Title"
rules="required"
v-slot="{ errors }"
>
<input
@input="$updateText('/admin/settings', 'get_started_title', app.get_started_title)"
v-model="app.get_started_title"
type="text"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
<div class="block-wrapper">
<label>Description:</label>
<ValidationProvider
tag="div"
mode="passive"
name="App Description"
rules="required"
v-slot="{ errors }"
>
<textarea
@input="
$updateText(
'/admin/settings',
'get_started_description',
app.get_started_description
)
"
rows="2"
v-model="app.get_started_description"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
></textarea>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
</div>
</div>
<!--Footer-->
<div class="card shadow-card">
<FormLabel>Footer</FormLabel>
<div class="block-wrapper">
<label>Footer content:</label>
<ValidationProvider
tag="div"
mode="passive"
name="App Title"
rules="required"
v-slot="{ errors }"
>
<input
@input="$updateText('/admin/settings', 'footer_content', app.footer_content)"
v-model="app.footer_content"
type="text"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
<span class="error-message" v-if="errors[0]">{{ errors[0] }}</span>
</ValidationProvider>
</div>
</div>
</div>
</PageTabGroup>
</PageTab>
</template>
<script>
import AppInputSwitch from '../../../../components/Forms/Layouts/AppInputSwitch'
import AppInputText from '../../../../components/Forms/Layouts/AppInputText'
import { ValidationProvider, ValidationObserver } from 'vee-validate/dist/vee-validate.full'
import PageTabGroup from '../../../../components/Layout/PageTabGroup'
import SelectInput from '../../../../components/Inputs/SelectInput'
import SwitchInput from '../../../../components/Inputs/SwitchInput'
import ImageInput from '../../../../components/Inputs/ImageInput'
import FormLabel from '../../../../components/UI/Labels/FormLabel'
import ButtonBase from '../../../../components/UI/Buttons/ButtonBase'
import PageTab from '../../../../components/Layout/PageTab'
import InfoBox from '../../../../components/UI/Others/InfoBox'
import { required } from 'vee-validate/dist/rules'
import axios from 'axios'
import { mapGetters } from 'vuex'
export default {
name: 'AppIndex',
components: {
AppInputSwitch,
AppInputText,
ValidationObserver,
ValidationProvider,
PageTabGroup,
SwitchInput,
SelectInput,
ImageInput,
ButtonBase,
FormLabel,
required,
PageTab,
InfoBox,
},
computed: {
...mapGetters(['config']),
},
data() {
return {
isLoading: true,
app: undefined,
}
},
mounted() {
axios
.get('/api/admin/settings', {
params: {
column: 'allow_homepage|footer_content|get_started_description|get_started_title|pricing_description|pricing_title|feature_description_3|feature_title_3|feature_description_2|feature_title_2|feature_description_1|feature_title_1|features_description|features_title|header_description|header_title|section_get_started|section_pricing_content|section_feature_boxes|section_features',
},
})
.then((response) => {
this.app = {
allow_homepage: parseInt(response.data.allow_homepage),
section_features: parseInt(response.data.section_features),
section_feature_boxes: parseInt(response.data.section_feature_boxes),
section_pricing_content: parseInt(response.data.section_pricing_content),
section_get_started: parseInt(response.data.section_get_started),
header_title: response.data.header_title,
header_description: response.data.header_description,
features_title: response.data.features_title,
features_description: response.data.features_description,
feature_title_1: response.data.feature_title_1,
feature_description_1: response.data.feature_description_1,
feature_title_2: response.data.feature_title_2,
feature_description_2: response.data.feature_description_2,
feature_title_3: response.data.feature_title_3,
feature_description_3: response.data.feature_description_3,
pricing_title: response.data.pricing_title,
pricing_description: response.data.pricing_description,
get_started_title: response.data.get_started_title,
get_started_description: response.data.get_started_description,
footer_content: response.data.footer_content,
}
})
.finally(() => {
this.isLoading = false
})
},
}
</script>
<style lang="scss" scoped>
@import '../../../../../sass/vuefilemanager/variables';
@import '../../../../../sass/vuefilemanager/mixins';
@import '../../../../../sass/vuefilemanager/forms';
.block-form {
max-width: 100%;
}
.page-image {
width: 100%;
margin: 0 auto;
display: block;
border-radius: 8px;
border: 1px solid #ececec;
}
</style>
@@ -58,42 +58,27 @@
{{ $t('user_features') }}
</FormLabel>
<!--Available only when is not metered billing-->
<div v-if="config.subscriptionType !== 'metered'">
<AppInputSwitch
:description="$t('admin_settings.others.storage_limit_help')"
:title="$t('admin_settings.others.storage_limit')"
>
<SwitchInput
v-model="app.storageLimitation"
:state="app.storageLimitation"
class="switch"
@input="$updateText('/admin/settings', 'storage_limitation', app.storageLimitation)"
/>
</AppInputSwitch>
<AppInputSwitch
:description="$t('admin_settings.others.storage_limit_help')"
:title="$t('admin_settings.others.storage_limit')"
>
<SwitchInput
v-model="app.storageLimitation"
:state="app.storageLimitation"
class="switch"
@input="$updateText('/admin/settings', 'storage_limitation', app.storageLimitation)"
/>
</AppInputSwitch>
<AppInputText v-if="app.storageLimitation" :title="$t('admin_settings.others.default_storage')">
<input
v-model="app.defaultStorage"
:placeholder="$t('admin_settings.others.default_storage_plac')"
class="focus-border-theme input-dark"
max="999999999"
min="1"
type="number"
@input="$updateText('/admin/settings', 'default_max_storage_amount', app.defaultStorage)"
/>
</AppInputText>
</div>
<AppInputText :description="$t('zero_for_unlimited_members')" :is-last="true" :title="$t('max_team_members')">
<AppInputText v-if="app.storageLimitation" :title="$t('admin_settings.others.default_storage')">
<input
v-model="app.teamsDefaultMembers"
v-model="app.defaultStorage"
:placeholder="$t('admin_settings.others.default_storage_plac')"
class="focus-border-theme input-dark"
max="999999999"
min="1"
type="number"
@input="$updateText('/admin/settings', 'default_max_team_member', app.teamsDefaultMembers)"
@input="$updateText('/admin/settings', 'default_max_storage_amount', app.defaultStorage)"
/>
</AppInputText>
</div>
@@ -216,63 +201,6 @@
@input="$updateText('/admin/settings', 'google_analytics', app.googleAnalytics, true)"
/>
</AppInputText>
</div>
<!--Upgrade License-->
<div v-if="app && !config.isSaaS" class="card shadow-card">
<FormLabel icon="trending-up">
{{ $t('Upgrade your License') }}
</FormLabel>
<ValidationObserver
ref="upgradeLicense"
v-slot="{ invalid }"
class="mt-6"
tag="form"
@submit.prevent="upgradeLicense"
>
<ValidationProvider
v-slot="{ errors }"
mode="passive"
name="Purchase Code"
rules="required"
tag="div"
>
<AppInputText
:error="errors[0]"
:is-last="true"
>
<div class="space-y-4 sm:flex sm:space-x-4 sm:space-y-0">
<input
v-model="purchaseCode"
:class="{ '!border-rose-600': errors[0] }"
:placeholder="$t('Paste your Purchase code here...')"
class="focus-border-theme input-dark"
type="text"
/>
<ButtonBase :loading="isLoadingUpgradingButton" button-style="theme" class="w-full sm:w-auto" type="submit">
{{ $t('Upgrade') }}
</ButtonBase>
</div>
</AppInputText>
</ValidationProvider>
</ValidationObserver>
</div>
<!-- Subscription -->
<div v-if="app && config.isSaaS" class="card shadow-card">
<FormLabel icon="credit-card">
{{ $t('subscription') }}
</FormLabel>
<AppInputText :description="$t('subscription_type_note')" :is-last="true" :title="$t('subscription_type')">
<SelectInput
:default="app.subscriptionType"
:options="subscriptionTypes"
:placeholder="$t('select_subscription_type')"
@change="subscriptionTypeChange"
/>
</AppInputText>
</div>
</PageTab>
</template>
@@ -310,7 +238,7 @@ export default {
PageTab,
},
computed: {
...mapGetters(['subscriptionTypes', 'config']),
...mapGetters(['config']),
},
data() {
return {
@@ -330,55 +258,6 @@ export default {
}
},
methods: {
async upgradeLicense() {
this.isLoadingUpgradingButton = true
// Validate fields
const isValid = await this.$refs.upgradeLicense.validate()
if (!isValid) return
axios.post('/api/admin/upgrade-license', {
purchaseCode: this.purchaseCode
})
.then((response) => {
this.$store.dispatch('getLanguageTranslations', this.config.locale)
this.$store.commit('REPLACE_CONFIG_VALUE', {
key: 'isSaaS',
value: true,
})
events.$emit('toaster', {
type: 'success',
message: this.$t('Your license was successfully upgraded'),
})
})
.catch((error) => {
if (error.response.status === 400) {
events.$emit('alert:open', {
title: this.$t('Purchase code is invalid or is not Extended License'),
})
} else {
events.$emit('alert:open', {
title: this.$t('popup_error.title'),
message: this.$t('popup_error.message'),
})
}
})
.finally(() => {
this.isLoadingUpgradingButton = false
})
},
subscriptionTypeChange(type) {
events.$emit('confirm:open', {
title: this.$t('subscription_type_change_warn'),
message: this.$t('subscription_type_change_warn_description'),
action: {
type: type,
operation: 'change-subscription-type',
},
})
},
async storeCredentials(service) {
// Validate fields
const isValid = await this.$refs.credentialsForm.validate()
@@ -453,29 +332,9 @@ export default {
storageLimitation: parseInt(response.data.storage_limitation),
mimetypesBlacklist: response.data.mimetypes_blacklist,
uploadLimit: response.data.upload_limit,
subscriptionType: response.data.subscriptionType,
chunkSize: response.data.chunk_size,
teamsDefaultMembers: response.data.default_max_team_member,
}
})
},
created() {
events.$on('action:confirmed', (data) => {
if (data.operation === 'change-subscription-type') {
// Update database
this.$updateText('/admin/settings', 'subscription_type', data.type)
// Update config
this.$store.commit('REPLACE_CONFIG_VALUE', {
key: 'subscriptionType',
value: data.type,
})
}
})
},
destroyed() {
events.$off('action:confirmed')
},
}
</script>
@@ -1,411 +0,0 @@
<template>
<PageTab>
<!--User Login/Registration-->
<div v-if="app" class="card shadow-card">
<FormLabel>
{{ $t('User Login/Registration') }}
</FormLabel>
<AppInputSwitch
:title="$t('admin_settings.others.allow_registration')"
:description="$t('admin_settings.others.allow_registration_help')"
>
<SwitchInput
@input="$updateText('/admin/settings', 'registration', app.userRegistration)"
v-model="app.userRegistration"
class="switch"
:state="app.userRegistration"
/>
</AppInputSwitch>
<AppInputSwitch
:title="$t('require_email_verification')"
:description="$t('require_email_verification_note')"
:is-last="true"
>
<SwitchInput
@input="$updateText('/admin/settings', 'user_verification', app.userVerification)"
v-model="app.userVerification"
class="switch"
:state="app.userVerification"
/>
</AppInputSwitch>
</div>
<!--Facebook Social Authentication-->
<div class="card shadow-card">
<img :src="$getSocialLogo('facebook')" alt="Facebook" class="mb-8 h-5" />
<AppInputSwitch
:title="$t('Allow Login via Facebook')"
:description="$t('You users will be able to login via Facebook account.')"
:is-last="!facebook.allowedService"
>
<SwitchInput
@input="$updateText('/admin/settings', 'allowed_facebook_login', facebook.allowedService)"
v-model="facebook.allowedService"
class="switch"
:state="facebook.allowedService"
/>
</AppInputSwitch>
<AppInputText
v-if="facebook.allowedService"
:title="$t('Your Callback URL')"
:description="$t('Please copy your url and paste it to the service callback URL.')"
>
<CopyInput size="small" :str="getCallbackEndpoint('facebook')" />
</AppInputText>
<div
v-if="config.isFacebookLoginConfigured && facebook.allowedService"
@click="facebook.isVisibleCredentialsForm = !facebook.isVisibleCredentialsForm"
class="flex cursor-pointer items-center"
:class="{ 'mb-4': facebook.isVisibleCredentialsForm }"
>
<edit2-icon size="12" class="vue-feather text-theme mr-2" />
<b class="text-xs">{{ $t('update_your_credentials') }}</b>
</div>
<!--Set up facebook credentials-->
<ValidationObserver
v-if="
(!config.isFacebookLoginConfigured || facebook.isVisibleCredentialsForm) && facebook.allowedService
"
@submit.prevent="storeCredentials('facebook')"
ref="facebook"
v-slot="{ invalid }"
tag="form"
class="rounded-xl p-5 shadow-lg"
>
<FormLabel v-if="!config.isFacebookLoginConfigured" icon="shield">
{{ $t('Configure Credentials') }}
</FormLabel>
<ValidationProvider tag="div" mode="passive" name="Client ID" rules="required" v-slot="{ errors }">
<AppInputText :title="$t('Client ID')" :error="errors[0]">
<input
v-model="facebook.credentials.client_id"
:placeholder="$t('Paste your Client ID here')"
type="text"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
</AppInputText>
</ValidationProvider>
<ValidationProvider tag="div" mode="passive" name="Client Secret" rules="required" v-slot="{ errors }">
<AppInputText :title="$t('Client Secret')" :error="errors[0]">
<input
v-model="facebook.credentials.client_secret"
:placeholder="$t('Paste your Client Secret here')"
type="text"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
</AppInputText>
</ValidationProvider>
<ButtonBase
:disabled="isLoading"
:loading="isLoading"
button-style="theme"
type="submit"
class="w-full"
>
{{ $t('store_credentials') }}
</ButtonBase>
</ValidationObserver>
</div>
<!--Google Social Authentication-->
<div class="card shadow-card">
<img :src="$getSocialLogo('google')" alt="Google" class="mb-8 h-7" />
<AppInputSwitch
:title="$t('Allow Login via Google')"
:description="$t('You users will be able to login via Google account.')"
:is-last="!google.allowedService"
>
<SwitchInput
@input="$updateText('/admin/settings', 'allowed_google_login', google.allowedService)"
v-model="google.allowedService"
class="switch"
:state="google.allowedService"
/>
</AppInputSwitch>
<AppInputText
v-if="google.allowedService"
:title="$t('Your Callback URL')"
:description="$t('Please copy your url and paste it to the service callback URL.')"
>
<CopyInput size="small" :str="getCallbackEndpoint('google')" />
</AppInputText>
<div
v-if="config.isGoogleLoginConfigured && google.allowedService"
@click="google.isVisibleCredentialsForm = !google.isVisibleCredentialsForm"
class="flex cursor-pointer items-center"
:class="{ 'mb-4': google.isVisibleCredentialsForm }"
>
<edit2-icon size="12" class="vue-feather text-theme mr-2" />
<b class="text-xs">{{ $t('update_your_credentials') }}</b>
</div>
<!--Set up Google credentials-->
<ValidationObserver
v-if="(!config.isGoogleLoginConfigured || google.isVisibleCredentialsForm) && google.allowedService"
@submit.prevent="storeCredentials('google')"
ref="google"
v-slot="{ invalid }"
tag="form"
class="rounded-xl p-5 shadow-lg"
>
<FormLabel v-if="!config.isGoogleLoginConfigured" icon="shield">
{{ $t('Configure Credentials') }}
</FormLabel>
<ValidationProvider tag="div" mode="passive" name="Client ID" rules="required" v-slot="{ errors }">
<AppInputText :title="$t('Client ID')" :error="errors[0]">
<input
v-model="google.credentials.client_id"
:placeholder="$t('Paste your Client ID here')"
type="text"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
</AppInputText>
</ValidationProvider>
<ValidationProvider tag="div" mode="passive" name="Client Secret" rules="required" v-slot="{ errors }">
<AppInputText :title="$t('Client Secret')" :error="errors[0]">
<input
v-model="google.credentials.client_secret"
:placeholder="$t('Paste your Client Secret here')"
type="text"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
</AppInputText>
</ValidationProvider>
<ButtonBase
:disabled="isLoading"
:loading="isLoading"
button-style="theme"
type="submit"
class="w-full"
>
{{ $t('store_credentials') }}
</ButtonBase>
</ValidationObserver>
</div>
<!--Github Social Authentication-->
<div class="card shadow-card">
<img :src="$getSocialLogo('github')" alt="Github" class="mb-8 h-5" />
<AppInputSwitch
:title="$t('Allow Login via GitHub')"
:description="$t('You users will be able to login via GitHub account.')"
:is-last="!github.allowedService"
>
<SwitchInput
@input="$updateText('/admin/settings', 'allowed_github_login', github.allowedService)"
v-model="github.allowedService"
class="switch"
:state="github.allowedService"
/>
</AppInputSwitch>
<AppInputText
v-if="github.allowedService"
:title="$t('Your Callback URL')"
:description="$t('Please copy your url and paste it to the service callback URL.')"
>
<CopyInput size="small" :str="getCallbackEndpoint('github')" />
</AppInputText>
<div
v-if="config.isGithubLoginConfigured && github.allowedService"
@click="github.isVisibleCredentialsForm = !github.isVisibleCredentialsForm"
class="flex cursor-pointer items-center"
:class="{ 'mb-4': github.isVisibleCredentialsForm }"
>
<edit2-icon size="12" class="vue-feather text-theme mr-2" />
<b class="text-xs">{{ $t('update_your_credentials') }}</b>
</div>
<!--Set up github credentials-->
<ValidationObserver
v-if="(!config.isGithubLoginConfigured || github.isVisibleCredentialsForm) && github.allowedService"
@submit.prevent="storeCredentials('github')"
ref="github"
v-slot="{ invalid }"
tag="form"
class="rounded-xl p-5 shadow-lg"
>
<FormLabel v-if="!config.isGithubLoginConfigured" icon="shield">
{{ $t('Configure Credentials') }}
</FormLabel>
<ValidationProvider tag="div" mode="passive" name="Client ID" rules="required" v-slot="{ errors }">
<AppInputText :title="$t('Client ID')" :error="errors[0]">
<input
v-model="github.credentials.client_id"
:placeholder="$t('Paste your Client ID here')"
type="text"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
</AppInputText>
</ValidationProvider>
<ValidationProvider tag="div" mode="passive" name="Client Secret" rules="required" v-slot="{ errors }">
<AppInputText :title="$t('Client Secret')" :error="errors[0]">
<input
v-model="github.credentials.client_secret"
:placeholder="$t('Paste your Client Secret here')"
type="text"
:class="{ '!border-rose-600': errors[0] }"
class="focus-border-theme input-dark"
/>
</AppInputText>
</ValidationProvider>
<ButtonBase
:disabled="isLoading"
:loading="isLoading"
button-style="theme"
type="submit"
class="w-full"
>
{{ $t('store_credentials') }}
</ButtonBase>
</ValidationObserver>
</div>
</PageTab>
</template>
<script>
import { Edit2Icon } from 'vue-feather-icons'
import { ValidationProvider, ValidationObserver } from 'vee-validate/dist/vee-validate.full'
import SwitchInput from '../../../../components/Inputs/SwitchInput'
import AppInputSwitch from '../../../../components/Forms/Layouts/AppInputSwitch'
import CopyInput from '../../../../components/Inputs/CopyInput'
import FormLabel from '../../../../components/UI/Labels/FormLabel'
import ButtonBase from '../../../../components/UI/Buttons/ButtonBase'
import AppInputText from '../../../../components/Forms/Layouts/AppInputText'
import PageTab from '../../../../components/Layout/PageTab'
import { required } from 'vee-validate/dist/rules'
import { events } from '../../../../bus'
import { mapGetters } from 'vuex'
import axios from 'axios'
export default {
name: 'SignInUp',
components: {
CopyInput,
ValidationObserver,
ValidationProvider,
AppInputSwitch,
AppInputText,
SwitchInput,
ButtonBase,
Edit2Icon,
FormLabel,
required,
PageTab,
events,
},
computed: {
...mapGetters(['config']),
},
data() {
return {
isLoading: false,
app: {
userRegistration: undefined,
userVerification: undefined,
},
facebook: {
allowedService: false,
isVisibleCredentialsForm: false,
credentials: {
key: undefined,
secret: undefined,
},
},
google: {
allowedService: false,
isVisibleCredentialsForm: false,
credentials: {
key: undefined,
secret: undefined,
},
},
github: {
allowedService: false,
isVisibleCredentialsForm: false,
credentials: {
key: undefined,
secret: undefined,
},
},
}
},
methods: {
getCallbackEndpoint(service) {
return `${this.config.host}/socialite/${service}/callback`
},
async storeCredentials(service) {
// Validate fields
const isValid = await this.$refs[service].validate()
if (!isValid) return
// Start loading
this.isLoading = true
// Send request to get verify account
axios
.post('/api/admin/settings/social-service', {
client_id: this[service].credentials.client_id,
client_secret: this[service].credentials.client_secret,
service: service,
})
.then(() => {
// Commit credentials
this.$store.commit('SET_SOCIAL_LOGIN_CONFIGURED', service)
this[service].allowedService = true
this[service].isVisibleCredentialsForm = false
// Show toaster
events.$emit('toaster', {
type: 'success',
message: this.$t('toaster.credentials_set', {
service: service,
}),
})
})
.catch((error) => {
if (error.response.status === 500) {
this.isError = true
this.errorMessage = error.response.data.message
}
})
.finally(() => (this.isLoading = false))
},
},
created() {
this.facebook.allowedService = this.config.allowedFacebookLogin
this.google.allowedService = this.config.allowedGoogleLogin
this.github.allowedService = this.config.allowedGithubLogin
this.app.userRegistration = this.config.userRegistration
this.app.userVerification = this.config.userVerification
},
}
</script>
-155
View File
@@ -1,155 +0,0 @@
<template>
<div>
<!--Datatable-->
<DatatableWrapper
v-if="!config.isEmptySubscriptions"
@init="isLoading = false"
api="/api/subscriptions/admin"
:paginator="true"
:columns="columns"
class="card overflow-x-auto shadow-card"
>
<!--Table data content-->
<template slot-scope="{ row }">
<tr class="whitespace-nowrap border-b border-dashed border-light dark:border-opacity-5">
<td class="py-5 pr-3 md:pr-1">
<router-link
class="flex items-center"
:to="{
name: 'UserDetail',
params: {
id: row.data.relationships.user.data.id,
},
}"
>
<MemberAvatar :is-border="false" :size="36" :member="row.data.relationships.user" />
<div class="ml-3 pr-10">
<b
class="max-w-1 block overflow-hidden text-ellipsis whitespace-nowrap text-sm font-bold"
style="max-width: 155px"
>
{{ row.data.relationships.user.data.attributes.name }}
</b>
<span class="block text-xs text-gray-600 dark:text-gray-500">
{{ row.data.relationships.user.data.attributes.email }}
</span>
</div>
</router-link>
</td>
<td class="px-3 md:px-1">
<ColorLabel :color="$getSubscriptionStatusColor(row.data.attributes.status)">
{{ $t(row.data.attributes.status) }}
</ColorLabel>
</td>
<td class="px-3 md:px-1">
<span class="text-limit text-sm font-bold capitalize" style="max-width: 160px">
{{ row.data.attributes.name }}
</span>
<span class="block text-xs font-bold text-gray-400">
{{ row.data.relationships.plan.data.attributes.price }}
/
{{ $t(`interval.${row.data.relationships.plan.data.attributes.interval}`) }}
</span>
</td>
<td class="px-3 md:px-1">
<span class="text-sm font-bold">
<!--todo: update renew attribute-->
{{
row.data.attributes.renews_at
? row.data.attributes.renews_at
: row.data.attributes.created_at
}}
</span>
</td>
<td class="px-3 md:px-1">
<span class="text-sm font-bold">
{{ row.data.attributes.ends_at ? row.data.attributes.ends_at : '-' }}
</span>
</td>
<td class="pl-3 text-right md:pl-1">
<img
class="inline-block max-h-5"
:src="$getPaymentLogo(row.data.attributes.driver)"
:alt="row.data.attributes.driver"
/>
</td>
</tr>
</template>
</DatatableWrapper>
<!--Empty State-->
<div v-if="config.isEmptySubscriptions" class="flex items-center justify-center fixed top-0 bottom-0 left-0 right-0">
<div class="text-center">
<img
class="mb-6 inline-block w-28"
src="https://twemoji.maxcdn.com/v/13.1.0/svg/1f5c3.svg"
alt="transaction"
/>
<h1 class="mb-1 text-2xl font-bold">
{{ $t('there_is_nothing') }}
</h1>
<p class="text-sm text-gray-600">
{{ $t('all_visible_subscriptions_here') }}
</p>
</div>
</div>
</div>
</template>
<script>
import ColorLabel from '../../components/UI/Labels/ColorLabel'
import MemberAvatar from '../../components/UI/Others/MemberAvatar'
import DatatableWrapper from '../../components/UI/Table/DatatableWrapper'
import { mapGetters } from 'vuex'
export default {
name: 'Subscriptions',
components: {
ColorLabel,
MemberAvatar,
DatatableWrapper,
},
computed: {
...mapGetters(['config']),
},
data() {
return {
isLoading: true,
columns: [
{
label: this.$t('user'),
field: 'user_id',
sortable: true,
},
{
label: this.$t('status'),
field: 'status',
sortable: true,
},
{
label: this.$t('note'),
field: 'plan.name',
sortable: true,
},
{
label: this.$t('renews_at'),
field: 'created_at',
sortable: true,
},
{
label: this.$t('ends_at'),
field: 'created_at',
sortable: true,
},
{
label: this.$t('service'),
field: 'driver.driver',
sortable: true,
},
],
}
},
}
</script>
+30 -257
View File
@@ -22,9 +22,7 @@
class="overflow-x-auto"
>
<template slot-scope="{ row }">
<!--Not a subscription-->
<tr
v-if="config.subscriptionType === 'none'"
class="whitespace-nowrap border-b border-dashed border-light dark:border-opacity-5"
>
<td class="py-3 pr-3 md:pr-1">
@@ -99,165 +97,6 @@
</div>
</td>
</tr>
<!--Fixed subscription-->
<tr
v-if="config.subscriptionType === 'fixed'"
class="whitespace-nowrap border-b border-dashed border-light dark:border-opacity-5"
>
<td class="py-3 pr-3 md:pr-1">
<router-link
:to="{
name: 'UserDetail',
params: { id: row.data.id },
}"
>
<div class="flex items-center">
<MemberAvatar
:is-border="false"
:size="44"
:member="row"
/>
<div class="ml-3 pr-10">
<b
class="max-w-1 block overflow-hidden text-ellipsis whitespace-nowrap text-sm font-bold"
style="max-width: 155px"
>
{{ row.data.relationships.settings.data.attributes.name }}
</b>
<span class="block text-xs text-gray-600 dark:text-gray-500">
{{ row.data.attributes.email }}
</span>
</div>
</div>
</router-link>
</td>
<td class="px-3 md:px-1">
<ColorLabel :color="$getUserRoleColor(row.data.attributes.role)">
{{ $t(row.data.attributes.role) }}
</ColorLabel>
</td>
<td class="px-3 md:px-1">
<span class="text-sm font-bold">
{{ row.data.relationships.subscription ? $t('premium') : $t('free') }}
</span>
</td>
<td class="px-3 md:px-1">
<span v-if="row.data.attributes.storage.capacity !== 0" class="text-sm font-bold">
{{ row.data.attributes.storage.used_formatted }}
</span>
<span v-if="row.data.attributes.storage.capacity === 0" class="text-sm font-bold"> - </span>
</td>
<td class="px-3 md:px-1" v-if="config.storageLimit">
<span v-if="row.data.attributes.storage.capacity !== 0" class="text-sm font-bold">
{{ row.data.attributes.storage.capacity_formatted }}
</span>
<span v-if="row.data.attributes.storage.capacity === 0" class="text-sm font-bold"> - </span>
</td>
<td class="px-3 md:px-1">
<span class="text-sm font-bold">
{{ row.data.attributes.created_at }}
</span>
</td>
<td class="pl-3 text-right md:pl-1">
<div class="flex w-full justify-end space-x-2">
<router-link
class="flex h-8 w-8 items-center justify-center rounded-md bg-light-background transition-colors hover:bg-green-100 dark:bg-2x-dark-foreground"
:to="{
name: 'UserDetail',
params: { id: row.data.id },
}"
>
<Edit2Icon size="15" class="opacity-75" />
</router-link>
<router-link
class="flex h-8 w-8 items-center justify-center rounded-md bg-light-background transition-colors hover:bg-red-100 dark:bg-2x-dark-foreground"
:to="{
name: 'UserDelete',
params: { id: row.data.id },
}"
>
<Trash2Icon size="15" class="opacity-75" />
</router-link>
</div>
</td>
</tr>
<!--Metered subscription-->
<tr
v-if="config.subscriptionType === 'metered'"
class="whitespace-nowrap border-b border-dashed border-light dark:border-opacity-5"
>
<td class="py-3 pr-3 md:pr-1">
<router-link
:to="{
name: 'UserDetail',
params: { id: row.data.id },
}"
>
<div class="flex items-center">
<MemberAvatar
:is-border="false"
:size="44"
:member="row"
/>
<div class="ml-3 pr-10">
<b
class="max-w-1 block overflow-hidden text-ellipsis whitespace-nowrap text-sm font-bold"
style="max-width: 155px"
>
{{ row.data.relationships.settings.data.attributes.name }}
</b>
<span class="block text-xs text-gray-600 dark:text-gray-500">
{{ row.data.attributes.email }}
</span>
</div>
</div>
</router-link>
</td>
<td class="px-3 md:px-1">
<ColorLabel :color="$getUserRoleColor(row.data.attributes.role)">
{{ $t(row.data.attributes.role) }}
</ColorLabel>
</td>
<td class="px-3 md:px-1">
<span class="text-sm font-bold">
{{ row.data.meta.usages ? row.data.meta.usages.featureEstimates.storage.usage : '-' }}
</span>
</td>
<td class="px-3 md:px-1">
<span class="text-sm font-bold">
{{ row.data.meta.usages ? row.data.meta.usages.costEstimate : '-' }}
</span>
</td>
<td class="px-3 md:px-1">
<span class="text-sm font-bold">
{{ row.data.attributes.created_at }}
</span>
</td>
<td class="pl-3 text-right md:pl-1">
<div class="flex w-full justify-end space-x-2">
<router-link
class="flex h-8 w-8 items-center justify-center rounded-md bg-light-background transition-colors hover:bg-green-100 dark:bg-2x-dark-foreground"
:to="{
name: 'UserDetail',
params: { id: row.data.id },
}"
>
<Edit2Icon size="15" class="opacity-75" />
</router-link>
<router-link
class="flex h-8 w-8 items-center justify-center rounded-md bg-light-background transition-colors hover:bg-red-100 dark:bg-2x-dark-foreground"
:to="{
name: 'UserDelete',
params: { id: row.data.id },
}"
>
<Trash2Icon size="15" class="opacity-75" />
</router-link>
</div>
</td>
</tr>
</template>
</DatatableWrapper>
</div>
@@ -279,7 +118,6 @@ import ColorLabel from '../../components/UI/Labels/ColorLabel'
import Spinner from '../../components/UI/Others/Spinner'
import { Trash2Icon, Edit2Icon } from 'vue-feather-icons'
import { mapGetters } from 'vuex'
import axios from 'axios'
export default {
name: 'Users',
@@ -299,101 +137,36 @@ export default {
computed: {
...mapGetters(['config']),
columns() {
return {
metered: [
{
label: this.$t('user'),
field: 'email',
sortable: true,
},
{
label: this.$t('role'),
field: 'role',
sortable: true,
},
{
label: this.$t('storage_used'),
sortable: false,
},
{
label: this.$t('billing_est.'),
sortable: false,
},
{
label: this.$t('created_at'),
field: 'created_at',
sortable: true,
},
{
label: this.$t('action'),
sortable: false,
},
],
fixed: [
{
label: this.$t('user'),
field: 'email',
sortable: true,
},
{
label: this.$t('role'),
field: 'role',
sortable: true,
},
{
label: this.$t('account'),
sortable: false,
},
{
label: this.$t('storage_used'),
sortable: false,
},
{
label: this.$t('max_storage'),
sortable: false,
hidden: !this.config.storageLimit,
},
{
label: this.$t('created_at'),
field: 'created_at',
sortable: true,
},
{
label: this.$t('action'),
sortable: false,
},
],
none: [
{
label: this.$t('user'),
field: 'email',
sortable: true,
},
{
label: this.$t('role'),
field: 'role',
sortable: true,
},
{
label: this.$t('storage_used'),
sortable: false,
},
{
label: this.$t('max_storage'),
sortable: false,
hidden: !this.config.storageLimit,
},
{
label: this.$t('created_at'),
field: 'created_at',
sortable: true,
},
{
label: this.$t('action'),
sortable: false,
},
],
}[this.config.subscriptionType]
return [
{
label: this.$t('user'),
field: 'email',
sortable: true,
},
{
label: this.$t('role'),
field: 'role',
sortable: true,
},
{
label: this.$t('storage_used'),
sortable: false,
},
{
label: this.$t('max_storage'),
sortable: false,
hidden: !this.config.storageLimit,
},
{
label: this.$t('created_at'),
field: 'created_at',
sortable: true,
},
{
label: this.$t('action'),
sortable: false,
},
]
},
},
data() {
+69 -93
View File
@@ -8,7 +8,7 @@
<!--Image input for replace avatar-->
<MemberAvatar class="shadow-lg rounded-xl" :size="64" :is-border="false" :member="user" />
<!--User name & email-->
<!--User name & email-->
<div class="ml-4">
<b class="text-md block font-bold sm:text-lg">
{{ user.data.relationships.settings.data.attributes.first_name }}
@@ -27,7 +27,7 @@
<CardNavigation :pages="pages" class="-mx-1" />
</div>
<!--Router Content-->
<!--Router Content-->
<router-view :user="user" @reload-user="fetchUser" />
</div>
<div id="loader" v-if="isLoading">
@@ -38,106 +38,82 @@
<script>
import CardNavigation from '../../../components/UI/Others/CardNavigation'
import { UserIcon, HardDriveIcon, LockIcon, Trash2Icon, FileTextIcon, CreditCardIcon } from 'vue-feather-icons'
import {UserIcon, HardDriveIcon, LockIcon, Trash2Icon, FileTextIcon, CreditCardIcon} from 'vue-feather-icons'
import MobileHeader from '../../../components/Mobile/MobileHeader'
import SectionTitle from '../../../components/UI/Labels/SectionTitle'
import ColorLabel from '../../../components/UI/Labels/ColorLabel'
import Spinner from '../../../components/UI/Others/Spinner'
import { events } from '../../../bus'
import { mapGetters } from 'vuex'
import {events} from '../../../bus'
import {mapGetters} from 'vuex'
import axios from 'axios'
import MemberAvatar from "../../../components/UI/Others/MemberAvatar";
export default {
name: 'Profile',
components: {
name: 'Profile',
components: {
MemberAvatar,
CardNavigation,
CreditCardIcon,
HardDriveIcon,
SectionTitle,
FileTextIcon,
MobileHeader,
ColorLabel,
Trash2Icon,
UserIcon,
LockIcon,
Spinner,
},
watch: {
'$route.fullPath': function () {
this.fetchUser()
},
},
computed: {
...mapGetters(['config']),
admin() {
return this.$store.getters.user ? this.$store.getters.user : undefined
},
pages() {
if (this.config.subscriptionType === 'none') {
return [
{
title: this.$t('detail'),
route: 'UserDetail',
},
{
title: this.$t('storage'),
route: 'UserStorage',
},
{
title: this.$t('password'),
route: 'UserPassword',
},
{
title: this.$t('delete_account'),
route: 'UserDelete',
},
]
}
CardNavigation,
CreditCardIcon,
HardDriveIcon,
SectionTitle,
FileTextIcon,
MobileHeader,
ColorLabel,
Trash2Icon,
UserIcon,
LockIcon,
Spinner,
},
watch: {
'$route.fullPath': function () {
this.fetchUser()
},
},
computed: {
...mapGetters(['config']),
admin() {
return this.$store.getters.user ? this.$store.getters.user : undefined
},
pages() {
return [
{
title: this.$t('detail'),
route: 'UserDetail',
},
{
title: this.$t('storage'),
route: 'UserStorage',
},
{
title: this.$t('password'),
route: 'UserPassword',
},
{
title: this.$t('delete_account'),
route: 'UserDelete',
},
]
},
},
data() {
return {
isLoading: true,
user: undefined,
}
},
methods: {
fetchUser() {
axios.get('/api/admin/users/' + this.$route.params.id)
.then((response) => {
this.user = response.data
this.isLoading = false
})
},
},
created() {
this.fetchUser()
return [
{
title: this.$t('detail'),
route: 'UserDetail',
},
{
title: this.$t('storage'),
route: 'UserStorage',
},
{
title: this.$t('billing'),
route: 'UserSubscription',
},
{
title: this.$t('password'),
route: 'UserPassword',
},
{
title: this.$t('delete_account'),
route: 'UserDelete',
},
]
},
},
data() {
return {
isLoading: true,
user: undefined,
}
},
methods: {
fetchUser() {
axios.get('/api/admin/users/' + this.$route.params.id).then((response) => {
this.user = response.data
this.isLoading = false
})
},
},
created() {
this.fetchUser()
events.$on('reload:user', () => this.fetchUser())
},
events.$on('reload:user', () => this.fetchUser())
},
}
</script>
@@ -71,67 +71,6 @@
/>
</AppInputText>
</div>
<div class="card shadow-card">
<FormLabel>{{ $t('billing_information') }}</FormLabel>
<AppInputText :title="$t('name')">
<input
:value="user.data.relationships.settings.data.attributes.name"
type="text"
class="focus-border-theme input-dark disabled:text-gray-900 disabled:opacity-100"
disabled
/>
</AppInputText>
<AppInputText :title="$t('address')">
<input
:value="user.data.relationships.settings.data.attributes.address"
type="text"
disabled
class="focus-border-theme input-dark disabled:text-gray-900 disabled:opacity-100"
/>
</AppInputText>
<AppInputText :title="$t('country')">
<input
:value="user.data.relationships.settings.data.attributes.country"
type="text"
disabled
class="focus-border-theme input-dark disabled:text-gray-900 disabled:opacity-100"
/>
</AppInputText>
<div class="flex space-x-4">
<AppInputText :title="$t('city')" class="w-full">
<input
:value="user.data.relationships.settings.data.attributes.city"
type="text"
disabled
class="focus-border-theme input-dark disabled:text-gray-900 disabled:opacity-100"
/>
</AppInputText>
<AppInputText :title="$t('postal_code')" class="w-full">
<input
:value="user.data.relationships.settings.data.attributes.postal_code"
type="text"
disabled
class="focus-border-theme input-dark disabled:text-gray-900 disabled:opacity-100"
/>
</AppInputText>
</div>
<AppInputText :title="$t('state')">
<input
:value="user.data.relationships.settings.data.attributes.state"
type="text"
disabled
class="focus-border-theme input-dark disabled:text-gray-900 disabled:opacity-100"
/>
</AppInputText>
<AppInputText :title="$t('phone_number')" :is-last="true">
<input
:value="user.data.relationships.settings.data.attributes.phone_number"
type="text"
disabled
class="focus-border-theme input-dark disabled:text-gray-900 disabled:opacity-100"
/>
</AppInputText>
</div>
</PageTab>
</template>
@@ -1,88 +0,0 @@
<template>
<div class="card shadow-card">
<FormLabel>
{{ $t('subscription') }}
</FormLabel>
<b class="-mt-3 mb-0.5 block text-xl font-extrabold sm:text-3xl">
{{ status }}
</b>
<b class="mb-3 mb-8 block text-sm dark:text-gray-500 text-gray-400">
{{ subscription.relationships.plan.data.attributes.name }} /
{{ price }}
</b>
<div v-for="(limit, i) in limitations" :key="i" :class="{ 'mb-6': Object.keys(limitations).length - 1 !== i }">
<b class="mb-3 block text-sm dark:text-gray-500 text-gray-400">
{{ limit.message }}
</b>
<ProgressLine v-if="limit.isVisibleBar" :data="limit.distribution" />
</div>
</div>
</template>
<script>
import FormLabel from '../../../../components/UI/Labels/FormLabel'
import ProgressLine from '../../../../components/UI/ProgressChart/ProgressLine'
import { mapGetters } from 'vuex'
export default {
name: 'UserFixedSubscription',
props: ['subscription', 'user'],
components: {
ProgressLine,
FormLabel,
},
computed: {
status() {
return {
active: this.$t('active_until', {date: this.subscription.attributes.renews_at}),
cancelled: this.$t('ends_at_date', {date: this.subscription.attributes.ends_at}),
}[this.subscription.attributes.status]
},
price() {
return {
month: this.$t('price_per_month', {price: this.subscription.relationships.plan.data.attributes.price}),
year: this.$t('price_per_year', {price: this.subscription.relationships.plan.data.attributes.price}),
}[this.subscription.relationships.plan.data.attributes.interval]
},
},
data() {
return {
limitations: [],
}
},
created() {
Object.entries(this.user.data.meta.limitations).map(([key, item]) => {
let payload = {
color: {
max_storage_amount: 'warning',
max_team_members: 'purple',
},
message: {
max_storage_amount: this.$t('total_x_of_x_used', {use: item.use, total:item.total }),
max_team_members: item.total === -1
? this.$t('max_team_members.unlimited')
: this.$t('total_x_of_x_members', {use: item.use, total:item.total }),
},
title: {
max_storage_amount: this.$t('storage'),
max_team_members: this.$t('team_members'),
},
}
this.limitations.push({
message: payload.message[key],
isVisibleBar: item.total === -1,
distribution: [
{
progress: item.percentage,
color: payload.color[key],
title: payload.title[key],
},
],
})
})
},
}
</script>
@@ -1,161 +0,0 @@
<template>
<div>
<!--Balance-->
<div class="card shadow-card">
<FormLabel icon="hard-drive">
{{ $t('balance') }}
</FormLabel>
<b class="-mt-3 mb-0.5 block text-2xl font-extrabold sm:text-3xl">
{{ user.data.relationships.balance.data.attributes.formatted }}
</b>
<ValidationObserver
ref="creditUserBalance"
@submit.prevent="increaseBalance"
v-slot="{ invalid }"
tag="form"
class="mt-6"
>
<ValidationProvider tag="div" v-slot="{ errors }" mode="passive" name="Balance Amount" rules="required">
<AppInputText
:description="$t('balance_will_be_increased')"
:error="errors[0]"
:is-last="true"
>
<div class="space-y-4 sm:flex sm:space-x-4 sm:space-y-0">
<input
v-model="balanceAmount"
:placeholder="$t('increase_for')"
type="number"
min="1"
max="999999999"
class="focus-border-theme input-dark"
:class="{ '!border-rose-600': errors[0] }"
/>
<ButtonBase
type="submit"
button-style="theme"
class="w-full sm:w-auto"
:loading="isUpdatingBalanceAmount"
:disabled="isUpdatingBalanceAmount"
>
{{ $t('increase_balance') }}
</ButtonBase>
</div>
</AppInputText>
</ValidationProvider>
</ValidationObserver>
</div>
<!--Usage Estimates-->
<div class="card shadow-card">
<FormLabel icon="hard-drive">
{{ $t('usage_estimates') }}
</FormLabel>
<b class="-mt-3 mb-0.5 block text-2xl font-extrabold sm:text-3xl">
{{ user.data.meta.usages.costEstimate }}
</b>
<b class="mb-3 mb-5 block text-sm text-gray-400">
{{ user.data.relationships.subscription.data.attributes.updated_at }}
{{ $t('till_now') }}
</b>
<div>
<div
class="flex items-center justify-between border-b border-dashed border-light py-2 dark:border-opacity-5"
v-for="(usage, i) in user.data.meta.usages.featureEstimates"
:key="i"
>
<div class="w-2/4 leading-none">
<b class="text-sm font-bold leading-none">
{{ $t(usage.feature) }}
</b>
<small class="block pt-2 text-xs leading-none dark:text-gray-500 text-gray-500">
{{ $t(`feature_usage_desc_${usage.feature}`) }}
</small>
</div>
<div class="w-1/4 text-left">
<span class="text-gray-560 text-sm font-bold">
{{ usage.usage }}
</span>
</div>
<div class="w-1/4 text-right">
<span class="text-theme text-sm font-bold">
{{ usage.cost }}
</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { ValidationProvider, ValidationObserver } from 'vee-validate/dist/vee-validate.full'
import AppInputText from '../../../../components/Forms/Layouts/AppInputText'
import FormLabel from '../../../../components/UI/Labels/FormLabel'
import ButtonBase from '../../../../components/UI/Buttons/ButtonBase'
import ColorLabel from '../../../../components/UI/Labels/ColorLabel'
import { mapGetters } from 'vuex'
import axios from 'axios'
import { events } from '../../../../bus'
export default {
name: 'UserMeteredSubscription',
props: ['subscription', 'user'],
components: {
ValidationProvider,
ValidationObserver,
AppInputText,
ButtonBase,
ColorLabel,
FormLabel,
},
computed: {
...mapGetters([]),
},
data() {
return {
balanceAmount: undefined,
isUpdatingBalanceAmount: false,
}
},
methods: {
async increaseBalance() {
// Validate fields
const isValid = await this.$refs.creditUserBalance.validate()
if (!isValid) return
this.isUpdatingBalanceAmount = true
axios
.post(`/api/subscriptions/admin/users/${this.user.data.id}/credit`, {
amount: this.balanceAmount,
})
.then(() => {
events.$emit('reload:user')
this.balanceAmount = undefined
events.$emit('toaster', {
type: 'success',
message: this.$t('balance_was_increased'),
})
})
.catch(() => {
events.$emit('toaster', {
type: 'danger',
message: this.$t('popup_error.title'),
})
})
.finally(() => {
this.isUpdatingBalanceAmount = false
})
},
},
created() {},
}
</script>
@@ -11,11 +11,11 @@
</b>
<b
v-if="
v-if="
config.subscriptionType === 'fixed' || (config.subscriptionType === 'none' && config.storageLimit)
"
class="mt-0.5 block text-sm dark:text-gray-500 text-gray-400"
>
class="mt-0.5 block text-sm dark:text-gray-500 text-gray-400"
>
{{ $t('total_of', {capacity: storage.data.attributes.capacity}) }}
{{ $t('used') }}
</b>
@@ -23,7 +23,7 @@
<ProgressLine v-if="storage.data.attributes.used !== '0B'" :data="distribution" class="mt-5" />
</div>
<!--Upload-->
<!--Upload-->
<div v-if="distribution" class="card shadow-card">
<FormLabel icon="hard-drive">
{{ $t('upload') }}
@@ -40,7 +40,7 @@
<BarChart :data="storage.data.meta.traffic.chart.upload" color="#FFBD2D" />
</div>
<!--Download-->
<!--Download-->
<div v-if="distribution" class="card shadow-card">
<FormLabel icon="hard-drive">
{{ $t('download') }}
@@ -57,44 +57,44 @@
<BarChart :data="storage.data.meta.traffic.chart.download" color="#9d66fe" />
</div>
<!--Set Storage Size-->
<!--Set Storage Size-->
<div
v-if="config.storageLimit && !user.data.attributes.subscription && config.subscriptionType !== 'metered'"
class="card shadow-card"
>
v-if="config.storageLimit"
class="card shadow-card"
>
<FormLabel>
{{ $t('user_box_storage.title') }}
</FormLabel>
<ValidationObserver
ref="changeStorageCapacity"
@submit.prevent="changeStorageCapacity"
v-slot="{ invalid }"
tag="form"
>
ref="changeStorageCapacity"
@submit.prevent="changeStorageCapacity"
v-slot="{ invalid }"
tag="form"
>
<ValidationProvider tag="div" v-slot="{ errors }" mode="passive" name="Capacity" rules="required">
<AppInputText
:title="$t('admin_page_user.label_change_capacity')"
:description="$t('user_box_storage.description')"
:error="errors[0]"
:is-last="true"
>
:title="$t('admin_page_user.label_change_capacity')"
:description="$t('user_box_storage.description')"
:error="errors[0]"
:is-last="true"
>
<div class="space-y-4 sm:flex sm:space-x-4 sm:space-y-0">
<input
v-model="capacity"
:placeholder="$t('admin_page_user.label_change_capacity')"
type="number"
min="1"
max="999999999"
class="focus-border-theme input-dark"
:class="{ '!border-rose-600': errors[0] }"
/>
v-model="capacity"
:placeholder="$t('admin_page_user.label_change_capacity')"
type="number"
min="1"
max="999999999"
class="focus-border-theme input-dark"
:class="{ '!border-rose-600': errors[0] }"
/>
<ButtonBase
:loading="isSendingRequest"
:disabled="isSendingRequest"
type="submit"
button-style="theme"
class="w-full sm:w-auto"
>
:loading="isSendingRequest"
:disabled="isSendingRequest"
type="submit"
button-style="theme"
class="w-full sm:w-auto"
>
{{ $t('change_capacity') }}
</ButtonBase>
</div>
@@ -112,102 +112,103 @@ import FormLabel from '../../../../components/UI/Labels/FormLabel'
import InfoBox from '../../../../components/UI/Others/InfoBox'
import PageTabGroup from '../../../../components/Layout/PageTabGroup'
import PageTab from '../../../../components/Layout/PageTab'
import { ValidationProvider, ValidationObserver } from 'vee-validate/dist/vee-validate.full'
import {ValidationProvider, ValidationObserver} from 'vee-validate/dist/vee-validate.full'
import ButtonBase from '../../../../components/UI/Buttons/ButtonBase'
import { required } from 'vee-validate/dist/rules'
import {required} from 'vee-validate/dist/rules'
import BarChart from '../../../../components/UI/BarChart/BarChart'
import { events } from '../../../../bus'
import { mapGetters } from 'vuex'
import {events} from '../../../../bus'
import {mapGetters} from 'vuex'
import axios from 'axios'
export default {
name: 'UserStorage',
props: ['user'],
components: {
ProgressLine,
AppInputText,
PageTabGroup,
FormLabel,
PageTab,
InfoBox,
ValidationProvider,
ValidationObserver,
ButtonBase,
required,
BarChart,
},
computed: {
...mapGetters(['config']),
},
data() {
return {
isLoading: true,
isSendingRequest: false,
capacity: undefined,
storage: undefined,
distribution: undefined,
}
},
methods: {
async changeStorageCapacity() {
// Validate fields
const isValid = await this.$refs.changeStorageCapacity.validate()
name: 'UserStorage',
props: ['user'],
components: {
ProgressLine,
AppInputText,
PageTabGroup,
FormLabel,
PageTab,
InfoBox,
ValidationProvider,
ValidationObserver,
ButtonBase,
required,
BarChart,
},
computed: {
...mapGetters(['config']),
},
data() {
return {
isLoading: true,
isSendingRequest: false,
capacity: undefined,
storage: undefined,
distribution: undefined,
}
},
methods: {
async changeStorageCapacity() {
// Validate fields
const isValid = await this.$refs.changeStorageCapacity.validate()
if (!isValid) return
if (!isValid) return
this.isSendingRequest = true
this.isSendingRequest = true
// Send request to get user reset link
axios
.post(this.$store.getters.api + '/admin/users/' + this.$route.params.id + '/capacity', {
attributes: {
max_storage_amount: this.capacity,
},
_method: 'patch',
})
.then(() => {
// Reset errors
this.$refs.changeStorageCapacity.reset()
// Send request to get user reset link
axios
.post(this.$store.getters.api + '/admin/users/' + this.$route.params.id + '/capacity', {
attributes: {
max_storage_amount: this.capacity,
},
_method: 'patch',
})
.then(() => {
// Reset errors
this.$refs.changeStorageCapacity.reset()
this.isSendingRequest = false
this.isSendingRequest = false
this.getStorageDetails()
this.getStorageDetails()
events.$emit('toaster', {
type: 'success',
message: this.$t('toaster.changed_capacity'),
})
})
.catch((error) => {
this.isSendingRequest = false
events.$emit('toaster', {
type: 'success',
message: this.$t('toaster.changed_capacity'),
})
})
.catch((error) => {
this.isSendingRequest = false
if (error.response.status == 422) {
// Password validation error
if (error.response.data.errors['attributes.max_storage_amount']) {
this.$refs.changeStorageCapacity.setErrors({
Capacity: this.$t('errors.capacity_digit'),
})
}
} else {
events.$emit('alert:open', {
title: this.$t('popup_error.title'),
message: this.$t('popup_error.message'),
})
}
})
},
getStorageDetails() {
axios.get('/api/admin/users/' + this.$route.params.id + '/storage').then((response) => {
this.distribution = this.$mapStorageUsage(response.data)
if (error.response.status == 422) {
// Password validation error
if (error.response.data.errors['attributes.max_storage_amount']) {
this.$refs.changeStorageCapacity.setErrors({
Capacity: this.$t('errors.capacity_digit'),
})
}
} else {
events.$emit('alert:open', {
title: this.$t('popup_error.title'),
message: this.$t('popup_error.message'),
})
}
})
},
getStorageDetails() {
axios.get('/api/admin/users/' + this.$route.params.id + '/storage')
.then((response) => {
this.distribution = this.$mapStorageUsage(response.data)
this.storage = response.data
this.storage = response.data
this.isLoading = false
})
},
},
created() {
this.getStorageDetails()
},
this.isLoading = false
})
},
},
created() {
this.getStorageDetails()
},
}
</script>
@@ -1,138 +0,0 @@
<template>
<PageTab :is-loading="isLoading">
<UserMeteredSubscription
v-if="subscription && config.subscriptionType === 'metered'"
:subscription="subscription"
:user="user"
/>
<UserFixedSubscription
v-if="subscription && config.subscriptionType === 'fixed'"
:subscription="subscription"
:user="user"
/>
<!--Free Plan-->
<div v-if="!subscription && config.subscriptionType === 'fixed'" class="card shadow-card">
<FormLabel>
{{ $t('subscription') }}
</FormLabel>
<b class="-mt-3 mb-0.5 block text-2xl font-extrabold sm:text-3xl">
{{ $t('free_plan') }}
</b>
<b class="block text-sm text-gray-400">
{{ $t('free_plan_parameters', {storage: config.storageDefaultSpaceFormatted, members: config.teamsDefaultMembers}) }}
</b>
</div>
<!--Transactions-->
<div class="card shadow-card">
<FormLabel icon="file-text">
{{ $t('transactions') }}
</FormLabel>
<DatatableWrapper
class="overflow-x-auto"
@init="isLoading = false"
:api="'/api/admin/users/' + this.$route.params.id + '/transactions'"
:paginator="true"
:columns="columns"
>
<template slot-scope="{ row }">
<!--Transaction rows-->
<MeteredTransactionRow
v-if="config.subscriptionType === 'metered'"
:row="row"
@showDetail="showTransactionDetail"
/>
<FixedTransactionRow v-if="config.subscriptionType === 'fixed'" :row="row" />
<!--Transaction detail-->
<MeteredTransactionDetailRow
v-if="row.data.attributes.metadata && showedTransactionDetailById === row.data.id"
:row="row"
/>
</template>
<!--Empty page-->
<template v-slot:empty-page>
<InfoBox style="margin-bottom: 0">
<p>
{{ $t("user_dont_have_transactions") }}
</p>
</InfoBox>
</template>
</DatatableWrapper>
</div>
</PageTab>
</template>
<script>
import MeteredTransactionDetailRow from '../../../../components/Subscription/MeteredTransactionDetailRow'
import MeteredTransactionRow from '../../../../components/Subscription/MeteredTransactionRow'
import FixedTransactionRow from '../../../../components/Subscription/FixedTransactionRow'
import DatatableWrapper from '../../../../components/UI/Table/DatatableWrapper'
import FormLabel from '../../../../components/UI/Labels/FormLabel'
import PageTab from '../../../../components/Layout/PageTab'
import InfoBox from '../../../../components/UI/Others/InfoBox'
import UserMeteredSubscription from './UserMeteredSubscription'
import UserFixedSubscription from './UserFixedSubscription'
import { mapGetters } from 'vuex'
import axios from 'axios'
export default {
name: 'UserSubscription',
props: ['user'],
components: {
MeteredTransactionDetailRow,
UserMeteredSubscription,
MeteredTransactionRow,
UserFixedSubscription,
FixedTransactionRow,
DatatableWrapper,
FormLabel,
InfoBox,
PageTab,
},
computed: {
...mapGetters(['config']),
columns() {
let filter = {
metered: ['user_id'],
fixed: ['type', 'user_id'],
}
return this.$store.getters.transactionColumns.filter(
(column) => !filter[config.subscriptionType].includes(column.field)
)
},
},
data() {
return {
showedTransactionDetailById: undefined,
subscription: undefined,
isLoading: true,
}
},
methods: {
showTransactionDetail(id) {
if (this.showedTransactionDetailById === id) this.showedTransactionDetailById = undefined
else this.showedTransactionDetailById = id
},
},
created() {
axios
.get(`/api/subscriptions/admin/users/${this.$route.params.id}/subscription`)
.then((response) => {
this.subscription = response.data.data
this.isLoading = false
})
.catch((error) => {
if (error.response.status === 404) this.isLoading = false
})
},
}
</script>