feat: new billing page

This commit is contained in:
Shivam Mishra
2025-01-22 18:25:08 +05:30
parent 8d3cecc997
commit d420c2cec7
9 changed files with 256 additions and 70 deletions
@@ -265,7 +265,7 @@
"CAPTAIN": "Captain",
"CAPTAIN_ASSISTANTS": "Assistants",
"CAPTAIN_DOCUMENTS": "Documents",
"CAPTAIN_RESPONSES" : "FAQs",
"CAPTAIN_RESPONSES": "FAQs",
"HOME": "Home",
"AGENTS": "Agents",
"AGENT_BOTS": "Bots",
@@ -327,15 +327,26 @@
},
"BILLING_SETTINGS": {
"TITLE": "Billing",
"DESCRIPTION": "Manage your subscription here, upgrade your plan and get more for your team.",
"CURRENT_PLAN": {
"TITLE": "Current Plan",
"PLAN_NOTE": "You are currently subscribed to the **{plan}** plan with **{quantity}** licenses"
"PLAN_NOTE": "You are currently subscribed to the **{plan}** plan with **{quantity}** licenses",
"SEAT_COUNT": "Number of seats",
"RENEWS_ON": "Renews on"
},
"VIEW_PRICING": "View Pricing",
"MANAGE_SUBSCRIPTION": {
"TITLE": "Manage your subscription",
"DESCRIPTION": "View your previous invoices, edit your billing details, or cancel your subscription.",
"BUTTON_TXT": "Go to the billing portal"
},
"CAPTAIN": {
"TITLE": "Captain",
"DESCRIPTION": "Manage usage and credits for Captain AI.",
"BUTTON_TXT": "Buy more credits",
"DOCUMENTS": "Documents",
"RESPONSES": "Responses"
},
"CHAT_WITH_US": {
"TITLE": "Need help?",
"DESCRIPTION": "Do you face any issues in billing? We are here to help.",
@@ -2,12 +2,19 @@
import { computed, onMounted } from 'vue';
import { useMapGetter, useStore } from 'dashboard/composables/store.js';
import { useAccount } from 'dashboard/composables/useAccount';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import { useCamelCase } from 'dashboard/composables/useTransformKeys';
import { format } from 'date-fns';
import BillingItem from './components/BillingItem.vue';
import BillingMeter from './components/BillingMeter.vue';
import BillingCard from './components/BillingCard.vue';
import BillingHeader from './components/BillingHeader.vue';
import DetailItem from './components/DetailItem.vue';
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
import SettingsLayout from '../SettingsLayout.vue';
import ButtonV4 from 'next/button/Button.vue';
const { currentAccount } = useAccount();
const { formatMessage } = useMessageFormatter();
const uiFlags = useMapGetter('accounts/getUIFlags');
const store = useStore();
@@ -31,6 +38,33 @@ const subscribedQuantity = computed(() => {
return customAttributes.value.subscribed_quantity;
});
const subscriptionRenewsOn = computed(() => {
if (!customAttributes.value.subscription_ends_on) return '';
const endDate = new Date(customAttributes.value.subscription_ends_on);
// return date as 12 Jan, 2034
return format(endDate, 'dd MMM, yyyy');
});
const captainLimits = computed(() => {
return currentAccount.value?.limits?.captain;
});
const documentLimits = computed(() => {
if (captainLimits.value?.documents) {
return useCamelCase(captainLimits.value.documents);
}
return null;
});
const responseLimits = computed(() => {
if (captainLimits.value?.responses) {
return useCamelCase(captainLimits.value.responses);
}
return null;
});
/**
* Computed property indicating if user has a billing plan
* @returns {boolean}
@@ -42,6 +76,7 @@ const hasABillingPlan = computed(() => {
const fetchAccountDetails = async () => {
if (!hasABillingPlan.value) {
store.dispatch('accounts/subscription');
store.dispatch('accounts/limits');
}
};
@@ -59,66 +94,86 @@ onMounted(fetchAccountDetails);
</script>
<template>
<div class="flex-1 p-6 overflow-auto dark:bg-slate-900">
<woot-loading-state v-if="uiFlags.isFetchingItem" />
<div v-else-if="!hasABillingPlan">
<p>{{ $t('BILLING_SETTINGS.NO_BILLING_USER') }}</p>
</div>
<div v-else class="w-full">
<div class="current-plan--details">
<h6>{{ $t('BILLING_SETTINGS.CURRENT_PLAN.TITLE') }}</h6>
<div
v-dompurify-html="
formatMessage(
$t('BILLING_SETTINGS.CURRENT_PLAN.PLAN_NOTE', {
plan: planName,
quantity: subscribedQuantity,
})
)
"
/>
</div>
<BillingItem
:title="$t('BILLING_SETTINGS.MANAGE_SUBSCRIPTION.TITLE')"
:description="$t('BILLING_SETTINGS.MANAGE_SUBSCRIPTION.DESCRIPTION')"
:button-label="$t('BILLING_SETTINGS.MANAGE_SUBSCRIPTION.BUTTON_TXT')"
@open="onClickBillingPortal"
<SettingsLayout
:is-loading="uiFlags.isFetchingItem"
:loading-message="$t('ATTRIBUTES_MGMT.LOADING')"
:no-records-found="!hasABillingPlan"
:no-records-message="$t('BILLING_SETTINGS.NO_BILLING_USER')"
class="max-w-2xl"
>
<template #header>
<BaseSettingsHeader
:title="$t('BILLING_SETTINGS.TITLE')"
:description="$t('BILLING_SETTINGS.DESCRIPTION')"
:link-text="$t('BILLING_SETTINGS.VIEW_PRICING')"
feature-name="billing"
/>
<BillingItem
:title="$t('BILLING_SETTINGS.CHAT_WITH_US.TITLE')"
:description="$t('BILLING_SETTINGS.CHAT_WITH_US.DESCRIPTION')"
:button-label="$t('BILLING_SETTINGS.CHAT_WITH_US.BUTTON_TXT')"
button-icon="chat-multiple"
@open="onToggleChatWindow"
/>
</div>
</div>
</template>
<template #body>
<section class="grid gap-4">
<BillingCard
:title="$t('BILLING_SETTINGS.MANAGE_SUBSCRIPTION.TITLE')"
:description="$t('BILLING_SETTINGS.MANAGE_SUBSCRIPTION.DESCRIPTION')"
>
<template #action>
<ButtonV4 sm solid blue @click="onClickBillingPortal">
{{ $t('BILLING_SETTINGS.MANAGE_SUBSCRIPTION.BUTTON_TXT') }}
</ButtonV4>
</template>
<div class="grid grid-cols-3 gap-2 divide-x divide-n-weak">
<DetailItem
:label="$t('BILLING_SETTINGS.CURRENT_PLAN.TITLE')"
:value="planName"
/>
<DetailItem
:label="$t('BILLING_SETTINGS.CURRENT_PLAN.SEAT_COUNT')"
:value="subscribedQuantity"
/>
<DetailItem
:label="$t('BILLING_SETTINGS.CURRENT_PLAN.RENEWS_ON')"
:value="subscriptionRenewsOn"
/>
</div>
</BillingCard>
<BillingCard
:title="$t('BILLING_SETTINGS.CAPTAIN.TITLE')"
:description="$t('BILLING_SETTINGS.CAPTAIN.DESCRIPTION')"
>
<template #action>
<ButtonV4 sm solid slate disabled>
{{ $t('BILLING_SETTINGS.CAPTAIN.BUTTON_TXT') }}
</ButtonV4>
</template>
<div v-if="captainLimits && responseLimits" class="px-5">
<BillingMeter
:title="$t('BILLING_SETTINGS.CAPTAIN.RESPONSES')"
v-bind="responseLimits"
/>
</div>
<div v-if="captainLimits && documentLimits" class="px-5">
<BillingMeter
:title="$t('BILLING_SETTINGS.CAPTAIN.DOCUMENTS')"
v-bind="documentLimits"
/>
</div>
</BillingCard>
<BillingHeader
class="px-1 mt-5"
:title="$t('BILLING_SETTINGS.CHAT_WITH_US.TITLE')"
:description="$t('BILLING_SETTINGS.CHAT_WITH_US.DESCRIPTION')"
>
<ButtonV4
sm
solid
slate
icon="i-lucide-life-buoy"
@open="onToggleChatWindow"
>
{{ $t('BILLING_SETTINGS.CHAT_WITH_US.BUTTON_TXT') }}
</ButtonV4>
</BillingHeader>
</section>
</template>
</SettingsLayout>
</template>
<style lang="scss">
.manage-subscription {
@apply bg-white dark:bg-slate-800 flex justify-between mb-2 py-6 px-4 items-center rounded-md border border-solid border-slate-75 dark:border-slate-700;
}
.current-plan--details {
@apply border-b border-solid border-slate-75 dark:border-slate-800 mb-4 pb-4;
h6 {
@apply text-slate-800 dark:text-slate-100;
}
p {
@apply text-slate-600 dark:text-slate-200;
}
}
.manage-subscription {
.manage-subscription--description {
@apply mb-0 text-slate-600 dark:text-slate-200;
}
h6 {
@apply text-slate-800 dark:text-slate-100;
}
}
</style>
@@ -1,5 +1,5 @@
import { frontendURL } from '../../../../helper/URLHelper';
import SettingsContent from '../Wrapper.vue';
import SettingsWrapper from '../SettingsWrapper.vue';
import Index from './Index.vue';
export default {
@@ -9,7 +9,7 @@ export default {
meta: {
permissions: ['administrator'],
},
component: SettingsContent,
component: SettingsWrapper,
props: {
headerTitle: 'BILLING_SETTINGS.TITLE',
icon: 'credit-card-person',
@@ -0,0 +1,25 @@
<script setup>
import BillingHeader from './BillingHeader.vue';
defineProps({
title: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
});
</script>
<template>
<div
class="rounded-xl shadow-sm border border-n-weak bg-n-solid-3 py-5 space-y-5"
>
<BillingHeader :title :description class="px-5">
<slot name="action" />
</BillingHeader>
<slot />
</div>
</template>
@@ -0,0 +1,26 @@
<script setup>
defineProps({
title: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
});
</script>
<template>
<div class="grid grid-cols-[1fr_200px] gap-5">
<div>
<span class="text-base font-medium text-n-slate-12">
{{ title }}
</span>
<p class="text-sm mt-1 text-n-slate-11">
{{ description }}
</p>
</div>
<slot />
</div>
</template>
@@ -26,8 +26,8 @@ export default {
<template>
<div class="manage-subscription">
<div>
<h6>{{ title }}</h6>
<p class="manage-subscription--description">
<h6 class="text-n-slate-12">{{ title }}</h6>
<p class="text-n-slate-11">
{{ description }}
</p>
</div>
@@ -0,0 +1,45 @@
<script setup>
import { computed } from 'vue';
const props = defineProps({
title: {
type: String,
required: true,
},
consumed: {
type: Number,
required: true,
},
totalCount: {
type: Number,
required: true,
},
});
const percent = computed(() =>
Math.round((props.consumed / props.totalCount) * 100)
);
const colorClass = computed(() => {
if (percent.value < 50) {
return 'bg-n-teal-10';
}
if (percent.value < 80) {
return 'bg-n-amber-10';
}
return 'bg-n-ruby-10';
});
</script>
<template>
<div
class="flex gap-5 items-center justify-between text-xs uppercase text-n-slate-10"
>
<div class="font-medium tracking-wider">
{{ title }}
</div>
<div class="tabular-nums">{{ consumed }} / {{ totalCount }}</div>
</div>
<div class="rounded-full overflow-hidden h-2 w-full bg-n-container mt-2">
<div class="h-2" :class="colorClass" :style="{ width: `${percent}%` }" />
</div>
</template>
@@ -0,0 +1,23 @@
<script setup>
defineProps({
label: {
type: String,
required: true,
},
value: {
type: String,
required: true,
},
});
</script>
<template>
<div class="px-5">
<span class="text-n-slate-11 text-xs">
{{ label }}
</span>
<div class="mt-2 text-xl font-medium text-n-slate-12">
{{ value }}
</div>
</div>
</template>