chore: Update credit sections
This commit is contained in:
@@ -37,10 +37,13 @@ const titleColorClasses = {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-4 rounded-xl outline outline-1" :class="colorClasses[color]">
|
||||
<div
|
||||
class="p-4 rounded-xl outline outline-1 -outline-offset-1"
|
||||
:class="colorClasses[color]"
|
||||
>
|
||||
<div class="flex gap-3 items-start">
|
||||
<Icon :icon="icon" class="flex-shrink-0 mt-0.5" />
|
||||
<div class="flex-1">
|
||||
<div class="flex-1 space-y-1">
|
||||
<h4
|
||||
v-if="title"
|
||||
class="text-sm font-semibold"
|
||||
@@ -48,7 +51,7 @@ const titleColorClasses = {
|
||||
>
|
||||
{{ title }}
|
||||
</h4>
|
||||
<p v-if="message" class="mt-1 mb-0 text-sm">
|
||||
<p v-if="message" class="mb-0 text-sm">
|
||||
{{ message }}
|
||||
</p>
|
||||
<slot />
|
||||
|
||||
@@ -413,13 +413,20 @@
|
||||
"CREDITS": "credits",
|
||||
"CREDITS_SHORT": "cr",
|
||||
"REFRESH": "Refresh",
|
||||
"BUY_CREDITS": "Buy Credits",
|
||||
"VIEW_HISTORY": "View History",
|
||||
"MONTHLY_CREDITS": "Monthly Credits",
|
||||
"TOPUP_CREDITS": "Top-up Credits",
|
||||
"USED_THIS_MONTH": "Used This Month",
|
||||
"TOTAL_USED": "Total Used",
|
||||
"USAGE_BASED_INFO": "Usage-based pricing: Credits are consumed as you use Chatwoot features.",
|
||||
"LEDGER_TITLE": "Recent Transactions",
|
||||
"NO_TRANSACTIONS": "No transactions yet. Start by topping up or subscribing to a plan."
|
||||
},
|
||||
"PLAN_SUMMARY": {
|
||||
"TITLE": "Plan Summary",
|
||||
"PRICE_PER_MONTH": "Price/month",
|
||||
"PRICE": "Price",
|
||||
"PER_MONTH": "{price}/month",
|
||||
"RENEWAL_DATE": "Renewal Date",
|
||||
"CREDITS_INCLUDED": "Credits included/month",
|
||||
"NUMBER_OF_SEATS": "Number of Seats",
|
||||
@@ -480,11 +487,10 @@
|
||||
},
|
||||
"SUBSCRIBE_MODAL": {
|
||||
"TITLE": "Confirm Subscription",
|
||||
"PLAN_NAME": "Plan Name:",
|
||||
"NUMBER_OF_SEATS": "Number of seats",
|
||||
"MIN": "Min",
|
||||
"MAX": "Max",
|
||||
"PER_SEAT": "per seat",
|
||||
"TOTAL": "total",
|
||||
"BASE_PRICE": "Base price per seat",
|
||||
"INCLUDED_CREDITS": "Included credits",
|
||||
"CREDITS": "credits",
|
||||
|
||||
@@ -64,6 +64,7 @@ export default {
|
||||
bypassUpgradePage() {
|
||||
return [
|
||||
'billing_settings_index',
|
||||
'billing_settings_v2_index',
|
||||
'settings_inbox_list',
|
||||
'general_settings_index',
|
||||
'agent_list',
|
||||
|
||||
@@ -3,22 +3,17 @@ import { computed, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store.js';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
import CreditsBalance from './components/CreditsBalance.vue';
|
||||
import CreditGrants from './components/CreditGrants.vue';
|
||||
import CreditsSection from './components/CreditsSection.vue';
|
||||
import PricingPlans from './components/PricingPlans.vue';
|
||||
import TopupOptions from './components/TopupOptions.vue';
|
||||
import BillingHeader from '../billing/components/BillingHeader.vue';
|
||||
import BillingCard from '../billing/components/BillingCard.vue';
|
||||
import ButtonV4 from 'next/button/Button.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
const { isOnChatwootCloud, isCloudFeatureEnabled, currentAccount } =
|
||||
useAccount();
|
||||
const { isOnChatwootCloud, currentAccount } = useAccount();
|
||||
|
||||
const uiFlags = useMapGetter('accounts/getUIFlags');
|
||||
const v2BillingData = useMapGetter('accounts/getV2BillingData');
|
||||
@@ -87,30 +82,22 @@ onMounted(handleInitialLoad);
|
||||
:description="$t('BILLING_SETTINGS_V2.DESCRIPTION')"
|
||||
:link-text="$t('BILLING_SETTINGS_V2.VIEW_PRICING')"
|
||||
feature-name="billing-v2"
|
||||
/>
|
||||
>
|
||||
<template #actions>
|
||||
<ButtonV4
|
||||
solid
|
||||
blue
|
||||
icon="i-lucide-external-link"
|
||||
:is-loading="uiFlags.isCheckoutInProcess"
|
||||
@click="handleOpenStripeDashboard"
|
||||
>
|
||||
{{ $t('BILLING_SETTINGS_V2.OPEN_STRIPE_DASHBOARD') }}
|
||||
</ButtonV4>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
<template #body>
|
||||
<!-- Stripe Dashboard Button -->
|
||||
<div class="mb-6 flex justify-end">
|
||||
<ButtonV4
|
||||
solid
|
||||
blue
|
||||
icon="i-lucide-external-link"
|
||||
:is-loading="uiFlags.isCheckoutInProcess"
|
||||
@click="handleOpenStripeDashboard"
|
||||
>
|
||||
{{ $t('BILLING_SETTINGS_V2.OPEN_STRIPE_DASHBOARD') }}
|
||||
</ButtonV4>
|
||||
</div>
|
||||
|
||||
<section class="grid gap-6">
|
||||
<!-- Credits Balance and Ledger -->
|
||||
<CreditsBalance
|
||||
:balance-data="v2BillingData.creditsBalance"
|
||||
:is-loading="v2BillingUIFlags.isFetchingBalance"
|
||||
@refresh="handleRefresh"
|
||||
/>
|
||||
|
||||
<!-- Pricing Plans -->
|
||||
<PricingPlans
|
||||
:plans="v2BillingData.pricingPlans"
|
||||
@@ -120,33 +107,17 @@ onMounted(handleInitialLoad);
|
||||
:is-canceling="v2BillingUIFlags.isCancelInProcess"
|
||||
/>
|
||||
|
||||
<!-- Top-up Options -->
|
||||
<TopupOptions
|
||||
v-if="canUseTopup"
|
||||
:options="v2BillingData.topupOptions"
|
||||
:is-loading="v2BillingUIFlags.isFetchingTopupOptions"
|
||||
:is-processing="v2BillingUIFlags.isTopupInProcess"
|
||||
/>
|
||||
|
||||
<!-- Upgrade Notice for Hacker Plan -->
|
||||
<BillingCard
|
||||
v-else
|
||||
:title="$t('BILLING_SETTINGS_V2.TOPUP_OPTIONS.TITLE')"
|
||||
:description="
|
||||
$t('BILLING_SETTINGS_V2.TOPUP_OPTIONS.UPGRADE_REQUIRED')
|
||||
"
|
||||
>
|
||||
<div class="px-5 pb-5">
|
||||
<p class="text-sm text-n-600">
|
||||
{{ $t('BILLING_SETTINGS_V2.TOPUP_OPTIONS.UPGRADE_MESSAGE') }}
|
||||
</p>
|
||||
</div>
|
||||
</BillingCard>
|
||||
|
||||
<!-- Credit History -->
|
||||
<CreditGrants
|
||||
:grants="v2BillingData.creditGrants"
|
||||
:is-loading="v2BillingUIFlags.isFetchingGrants"
|
||||
<!-- Credits Section -->
|
||||
<CreditsSection
|
||||
:balance-data="v2BillingData.creditsBalance"
|
||||
:topup-options="v2BillingData.topupOptions"
|
||||
:credit-grants="v2BillingData.creditGrants"
|
||||
:is-loading-balance="v2BillingUIFlags.isFetchingBalance"
|
||||
:is-loading-topup="v2BillingUIFlags.isFetchingTopupOptions"
|
||||
:is-loading-grants="v2BillingUIFlags.isFetchingGrants"
|
||||
:is-processing-topup="v2BillingUIFlags.isTopupInProcess"
|
||||
:can-use-topup="canUseTopup"
|
||||
@refresh="handleRefresh"
|
||||
/>
|
||||
|
||||
<!-- Chat Support -->
|
||||
|
||||
-1
@@ -41,7 +41,6 @@ defineExpose({ dialogRef });
|
||||
@confirm="handleCancel"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<!-- Warning -->
|
||||
<Notice
|
||||
color="ruby"
|
||||
icon="i-lucide-alert-triangle"
|
||||
|
||||
+71
-67
@@ -2,7 +2,6 @@
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import format from 'date-fns/format';
|
||||
import BillingCard from 'dashboard/routes/dashboard/settings/billing/components/BillingCard.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -52,75 +51,80 @@ const getCategoryConfig = category =>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BillingCard
|
||||
:title="$t('BILLING_SETTINGS_V2.CREDIT_GRANTS.TITLE')"
|
||||
:description="$t('BILLING_SETTINGS_V2.CREDIT_GRANTS.DESCRIPTION')"
|
||||
>
|
||||
<div class="px-5 pb-5">
|
||||
<div v-if="sortedGrants.length" class="space-y-3">
|
||||
<div
|
||||
v-for="grant in sortedGrants"
|
||||
:key="grant.id"
|
||||
class="p-4 rounded-xl outline outline-1 outline-n-weak transition-opacity"
|
||||
:class="{ 'opacity-50': grant.voided_at }"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span
|
||||
class="px-2 py-0.5 text-xs font-semibold rounded-md"
|
||||
:class="getCategoryConfig(grant.category).class"
|
||||
>
|
||||
{{ getCategoryConfig(grant.category).label }}
|
||||
</span>
|
||||
<span
|
||||
v-if="grant.voided_at"
|
||||
class="px-2 py-0.5 text-xs font-semibold rounded-md bg-n-ruby-3 text-n-ruby-11 outline outline-1 outline-n-ruby-4"
|
||||
>
|
||||
{{ t('BILLING_SETTINGS_V2.CREDIT_GRANTS.VOIDED') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<h6 class="text-base font-semibold mb-2">
|
||||
{{ t('BILLING_SETTINGS_V2.CREDIT_GRANTS.TITLE') }}
|
||||
</h6>
|
||||
<div v-if="sortedGrants.length" class="space-y-2">
|
||||
<div
|
||||
v-for="grant in sortedGrants"
|
||||
:key="grant.id"
|
||||
class="flex items-center justify-between gap-4 p-3 rounded-lg border border-n-weak"
|
||||
:class="{ 'opacity-60': grant.voided_at }"
|
||||
>
|
||||
<div class="flex items-center gap-2.5 min-w-0 flex-1">
|
||||
<span
|
||||
class="px-2 py-0.5 text-xs font-semibold rounded whitespace-nowrap"
|
||||
:class="getCategoryConfig(grant.category).class"
|
||||
>
|
||||
{{ getCategoryConfig(grant.category).label }}
|
||||
</span>
|
||||
<span class="text-sm font-medium text-n-slate-12 truncate">
|
||||
{{ grant.name }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h4 class="text-sm font-semibold text-n-slate-12 truncate">
|
||||
{{ grant.name }}
|
||||
</h4>
|
||||
<p class="mt-1 text-xs text-n-slate-11">
|
||||
{{ formatNumber(grant.credits) }}
|
||||
{{ t('BILLING_SETTINGS_V2.CREDIT_GRANTS.CREDITS') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="text-right flex-shrink-0">
|
||||
<div v-if="grant.expires_at" class="text-xs text-n-slate-11">
|
||||
<span class="font-medium">
|
||||
{{ t('BILLING_SETTINGS_V2.CREDIT_GRANTS.EXPIRES') }}
|
||||
</span>
|
||||
<div class="mt-0.5">{{ formatDate(grant.expires_at) }}</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="text-xs text-n-teal-10 font-medium">
|
||||
{{ t('BILLING_SETTINGS_V2.CREDIT_GRANTS.NO_EXPIRY') }}
|
||||
</div>
|
||||
|
||||
<div v-if="grant.created_at" class="mt-1 text-xs text-n-slate-10">
|
||||
{{
|
||||
t('BILLING_SETTINGS_V2.CREDIT_GRANTS.ADDED', {
|
||||
date: formatDate(grant.created_at),
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 flex-shrink-0">
|
||||
<div
|
||||
class="text-sm font-semibold text-n-slate-12 whitespace-nowrap hidden sm:block"
|
||||
>
|
||||
{{ formatNumber(grant.credits) }}
|
||||
<span class="text-xs font-normal text-n-slate-10 ml-1">
|
||||
{{ t('BILLING_SETTINGS_V2.CREDIT_GRANTS.CREDITS') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-right min-w-[110px]">
|
||||
<span
|
||||
v-if="grant.expires_at"
|
||||
class="font-medium text-n-slate-11"
|
||||
:title="t('BILLING_SETTINGS_V2.CREDIT_GRANTS.EXPIRES')"
|
||||
>
|
||||
{{ formatDate(grant.expires_at) }}
|
||||
</span>
|
||||
<span v-else class="font-medium text-n-teal-10">
|
||||
{{ t('BILLING_SETTINGS_V2.CREDIT_GRANTS.NO_EXPIRY') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="grant.created_at"
|
||||
class="text-xs text-n-slate-10 whitespace-nowrap"
|
||||
:title="
|
||||
t('BILLING_SETTINGS_V2.CREDIT_GRANTS.ADDED', {
|
||||
date: formatDate(grant.created_at),
|
||||
})
|
||||
"
|
||||
>
|
||||
{{ formatDate(grant.created_at) }}
|
||||
</div>
|
||||
|
||||
<span
|
||||
v-if="grant.voided_at"
|
||||
class="px-2 py-0.5 text-xs font-semibold rounded bg-n-ruby-3 text-n-ruby-11 border border-n-ruby-6 whitespace-nowrap"
|
||||
>
|
||||
{{ t('BILLING_SETTINGS_V2.CREDIT_GRANTS.VOIDED') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="!isLoading" class="py-8 text-center">
|
||||
<p class="text-sm text-n-slate-11">
|
||||
{{ t('BILLING_SETTINGS_V2.CREDIT_GRANTS.NO_GRANTS') }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-else class="py-8 flex justify-center items-center">
|
||||
<Spinner class="text-n-brand" />
|
||||
</div>
|
||||
</div>
|
||||
</BillingCard>
|
||||
<div v-else-if="!isLoading" class="py-8 text-center">
|
||||
<p class="text-sm text-n-slate-11">
|
||||
{{ t('BILLING_SETTINGS_V2.CREDIT_GRANTS.NO_GRANTS') }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-else class="py-8 flex justify-center items-center">
|
||||
<Spinner class="text-n-brand" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+88
-140
@@ -1,7 +1,8 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import BillingCard from '../../billing/components/BillingCard.vue';
|
||||
import ButtonV4 from 'next/button/Button.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
balanceData: {
|
||||
@@ -14,7 +15,9 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['refresh']);
|
||||
const emit = defineEmits(['refresh', 'viewTopup', 'viewHistory']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const currentBalance = computed(() => {
|
||||
return props.balanceData?.total_credits || 0;
|
||||
@@ -36,169 +39,114 @@ const usageTotal = computed(() => {
|
||||
return props.balanceData?.usage_total || 0;
|
||||
});
|
||||
|
||||
const currency = computed(() => {
|
||||
return props.balanceData?.currency || 'USD';
|
||||
});
|
||||
|
||||
const ledger = computed(() => {
|
||||
return props.balanceData?.ledger || [];
|
||||
});
|
||||
|
||||
const hasLedger = computed(() => {
|
||||
return ledger.value.length > 0;
|
||||
});
|
||||
|
||||
const formatDate = timestamp => {
|
||||
if (!timestamp) return '';
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
const formatAmount = amount => {
|
||||
if (!amount) return '0';
|
||||
return new Intl.NumberFormat('en-US').format(amount);
|
||||
};
|
||||
|
||||
const formatMoney = amount => {
|
||||
if (!amount) return '$0.00';
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: currency.value,
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
const getTransactionTypeColor = type => {
|
||||
const colorMap = {
|
||||
topup: 'text-g-800',
|
||||
subscription: 'text-b-800',
|
||||
usage: 'text-y-800',
|
||||
refund: 'text-p-800',
|
||||
};
|
||||
return colorMap[type?.toLowerCase()] || 'text-n-800';
|
||||
};
|
||||
|
||||
const getTransactionTypeLabel = type => {
|
||||
if (!type) return '';
|
||||
return type.charAt(0).toUpperCase() + type.slice(1).toLowerCase();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BillingCard
|
||||
:title="$t('BILLING_SETTINGS_V2.CREDITS_BALANCE.TITLE')"
|
||||
:description="$t('BILLING_SETTINGS_V2.CREDITS_BALANCE.DESCRIPTION')"
|
||||
<div
|
||||
class="p-5 rounded-xl outline outline-1 outline-n-weak bg-n-solid-1 mx-5 mt-5"
|
||||
>
|
||||
<template #action>
|
||||
<ButtonV4
|
||||
sm
|
||||
faded
|
||||
slate
|
||||
icon="i-lucide-refresh-cw"
|
||||
:is-loading="isLoading"
|
||||
@click="emit('refresh')"
|
||||
>
|
||||
{{ $t('BILLING_SETTINGS_V2.CREDITS_BALANCE.REFRESH') }}
|
||||
</ButtonV4>
|
||||
</template>
|
||||
|
||||
<!-- Current Balance -->
|
||||
<div class="px-5 pb-4">
|
||||
<div class="flex items-baseline gap-2">
|
||||
<span class="text-4xl font-bold text-n-800">
|
||||
<div class="p-4 rounded-lg bg-n-slate-2 dark:bg-n-solid-2 mb-4">
|
||||
<p class="text-xs text-n-slate-11 mb-1">
|
||||
{{ t('BILLING_SETTINGS_V2.CREDITS_BALANCE.CREDITS') }}
|
||||
</p>
|
||||
<div class="flex items-baseline gap-2 mb-1">
|
||||
<span class="text-4xl font-bold text-n-slate-12">
|
||||
{{ formatAmount(currentBalance) }}
|
||||
</span>
|
||||
<span class="text-base text-n-600">
|
||||
{{ $t('BILLING_SETTINGS_V2.CREDITS_BALANCE.CREDITS') }}
|
||||
<span class="text-base text-n-slate-11">
|
||||
{{ t('BILLING_SETTINGS_V2.CREDITS_BALANCE.CREDITS') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-3 grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-xs text-n-600 uppercase">Monthly Credits</p>
|
||||
<p class="text-lg font-semibold text-b-800">
|
||||
<p class="text-xs text-n-slate-11 mb-0">
|
||||
{{ t('BILLING_SETTINGS_V2.CREDITS_BALANCE.USAGE_BASED_INFO') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-6 mb-4">
|
||||
<div>
|
||||
<p class="text-xs text-n-slate-11 mb-1">
|
||||
{{ t('BILLING_SETTINGS_V2.CREDITS_BALANCE.MONTHLY_CREDITS') }}
|
||||
</p>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Icon icon="i-lucide-calendar-check" class="text-n-blue-9" />
|
||||
<p class="text-lg font-semibold text-n-slate-12 mb-0">
|
||||
{{ formatAmount(monthlyCredits) }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-n-600 uppercase">Top-up Credits</p>
|
||||
<p class="text-lg font-semibold text-g-800">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-xs text-n-slate-11 mb-1">
|
||||
{{ t('BILLING_SETTINGS_V2.CREDITS_BALANCE.TOPUP_CREDITS') }}
|
||||
</p>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Icon icon="i-lucide-plus-circle" class="text-n-teal-9" />
|
||||
<p class="text-lg font-semibold text-n-slate-12 mb-0">
|
||||
{{ formatAmount(topupCredits) }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-n-600 uppercase">Used This Month</p>
|
||||
<p class="text-lg font-semibold text-y-800">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-xs text-n-slate-11 mb-1">
|
||||
{{ t('BILLING_SETTINGS_V2.CREDITS_BALANCE.USED_THIS_MONTH') }}
|
||||
</p>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Icon icon="i-lucide-trending-down" class="text-n-amber-9" />
|
||||
<p class="text-lg font-semibold text-n-slate-12 mb-0">
|
||||
{{ formatAmount(usageThisMonth) }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-n-600 uppercase">Total Used</p>
|
||||
<p class="text-lg font-semibold text-n-700">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-xs text-n-slate-11 mb-1">
|
||||
{{ t('BILLING_SETTINGS_V2.CREDITS_BALANCE.TOTAL_USED') }}
|
||||
</p>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Icon icon="i-lucide-bar-chart-3" class="text-n-slate-9" />
|
||||
<p class="text-lg font-semibold text-n-slate-12 mb-0">
|
||||
{{ formatAmount(usageTotal) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-3 text-sm text-n-600">
|
||||
{{ $t('BILLING_SETTINGS_V2.CREDITS_BALANCE.USAGE_BASED_INFO') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Credit Ledger -->
|
||||
<div v-if="hasLedger" class="border-t border-n-weak">
|
||||
<div class="px-5 py-3">
|
||||
<h4 class="text-sm font-semibold text-n-800">
|
||||
{{ $t('BILLING_SETTINGS_V2.CREDITS_BALANCE.LEDGER_TITLE') }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="px-5 pb-4">
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="transaction in ledger.slice(0, 10)"
|
||||
:key="transaction.id"
|
||||
class="flex items-center justify-between py-2 border-b border-n-weak last:border-0"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="text-sm font-medium"
|
||||
:class="getTransactionTypeColor(transaction.type)"
|
||||
>
|
||||
{{ getTransactionTypeLabel(transaction.type) }}
|
||||
</span>
|
||||
<span v-if="transaction.note" class="text-xs text-n-600">
|
||||
- {{ transaction.note }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs text-n-600 mt-0.5">
|
||||
{{ formatDate(transaction.ts) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div
|
||||
class="text-sm font-semibold"
|
||||
:class="
|
||||
transaction.amount_credits > 0 ? 'text-g-800' : 'text-r-800'
|
||||
"
|
||||
>
|
||||
{{ transaction.amount_credits > 0 ? '+' : ''
|
||||
}}{{ formatAmount(transaction.amount_credits) }}
|
||||
{{ $t('BILLING_SETTINGS_V2.CREDITS_BALANCE.CREDITS_SHORT') }}
|
||||
</div>
|
||||
<div
|
||||
v-if="transaction.amount_money"
|
||||
class="text-xs text-n-600 mt-0.5"
|
||||
>
|
||||
{{ formatMoney(transaction.amount_money) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center justify-end gap-2 pt-4 border-t border-n-weak"
|
||||
>
|
||||
<Button
|
||||
variant="faded"
|
||||
color="blue"
|
||||
size="sm"
|
||||
icon="i-lucide-refresh-cw"
|
||||
:is-loading="isLoading"
|
||||
@click="emit('refresh')"
|
||||
>
|
||||
{{ t('BILLING_SETTINGS_V2.CREDITS_BALANCE.REFRESH') }}
|
||||
</Button>
|
||||
<Button
|
||||
variant="faded"
|
||||
color="teal"
|
||||
size="sm"
|
||||
icon="i-lucide-plus-circle"
|
||||
@click="emit('viewTopup')"
|
||||
>
|
||||
{{ t('BILLING_SETTINGS_V2.CREDITS_BALANCE.BUY_CREDITS') }}
|
||||
</Button>
|
||||
<Button
|
||||
variant="faded"
|
||||
color="slate"
|
||||
size="sm"
|
||||
icon="i-lucide-history"
|
||||
@click="emit('viewHistory')"
|
||||
>
|
||||
{{ t('BILLING_SETTINGS_V2.CREDITS_BALANCE.VIEW_HISTORY') }}
|
||||
</Button>
|
||||
</div>
|
||||
</BillingCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import BillingCard from '../../billing/components/BillingCard.vue';
|
||||
import CreditsBalance from './CreditsBalance.vue';
|
||||
import TopupOptions from './TopupOptions.vue';
|
||||
import CreditGrants from './CreditGrants.vue';
|
||||
import Notice from 'dashboard/components-next/notice/Notice.vue';
|
||||
|
||||
defineProps({
|
||||
balanceData: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
topupOptions: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
creditGrants: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
isLoadingBalance: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isLoadingTopup: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isLoadingGrants: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isProcessingTopup: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
canUseTopup: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['refresh']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const showTopupOptions = ref(false);
|
||||
const showCreditHistory = ref(false);
|
||||
|
||||
const handleViewTopup = () => {
|
||||
showTopupOptions.value = !showTopupOptions.value;
|
||||
if (showTopupOptions.value) {
|
||||
showCreditHistory.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewHistory = () => {
|
||||
showCreditHistory.value = !showCreditHistory.value;
|
||||
if (showCreditHistory.value) {
|
||||
showTopupOptions.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
emit('refresh');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BillingCard
|
||||
:title="t('BILLING_SETTINGS_V2.CREDITS_BALANCE.TITLE')"
|
||||
:description="t('BILLING_SETTINGS_V2.CREDITS_BALANCE.DESCRIPTION')"
|
||||
>
|
||||
<CreditsBalance
|
||||
:balance-data="balanceData"
|
||||
:is-loading="isLoadingBalance"
|
||||
@refresh="handleRefresh"
|
||||
@view-topup="handleViewTopup"
|
||||
@view-history="handleViewHistory"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="transition-all duration-500 ease-in-out grid overflow-hidden mx-5 !mt-0"
|
||||
:class="
|
||||
showTopupOptions
|
||||
? 'grid-rows-[1fr] opacity-100'
|
||||
: 'grid-rows-[0fr] opacity-0'
|
||||
"
|
||||
>
|
||||
<div class="overflow-hidden">
|
||||
<TopupOptions
|
||||
v-if="canUseTopup"
|
||||
:options="topupOptions"
|
||||
:is-loading="isLoadingTopup"
|
||||
:is-processing="isProcessingTopup"
|
||||
/>
|
||||
<div v-else class="mt-4">
|
||||
<Notice
|
||||
color="amber"
|
||||
icon="i-lucide-alert-triangle"
|
||||
:title="t('BILLING_SETTINGS_V2.TOPUP_OPTIONS.UPGRADE_REQUIRED')"
|
||||
:message="t('BILLING_SETTINGS_V2.TOPUP_OPTIONS.UPGRADE_MESSAGE')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="transition-all duration-500 ease-in-out grid overflow-hidden mx-5 !mt-0"
|
||||
:class="
|
||||
showCreditHistory
|
||||
? 'grid-rows-[1fr] opacity-100'
|
||||
: 'grid-rows-[0fr] opacity-0'
|
||||
"
|
||||
>
|
||||
<div class="overflow-hidden">
|
||||
<CreditGrants :grants="creditGrants" :is-loading="isLoadingGrants" />
|
||||
</div>
|
||||
</div>
|
||||
</BillingCard>
|
||||
</template>
|
||||
+9
-11
@@ -102,7 +102,6 @@ const handleIncreaseSeats = () => {
|
||||
<div
|
||||
class="p-5 rounded-xl outline outline-1 outline-n-weak bg-n-solid-1 mx-5 mt-5"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<h3 class="text-base font-semibold text-n-slate-12">
|
||||
{{ t('BILLING_SETTINGS_V2.PLAN_SUMMARY.TITLE') }}
|
||||
@@ -114,24 +113,25 @@ const handleIncreaseSeats = () => {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Details Grid -->
|
||||
<div class="grid grid-cols-3 gap-6 mb-4">
|
||||
<!-- Price/month -->
|
||||
<div>
|
||||
<p class="text-xs text-n-slate-11 mb-1">
|
||||
{{ t('BILLING_SETTINGS_V2.PLAN_SUMMARY.PRICE_PER_MONTH') }}
|
||||
{{ t('BILLING_SETTINGS_V2.PLAN_SUMMARY.PRICE') }}
|
||||
</p>
|
||||
<div class="flex items-center gap-1">
|
||||
<p class="text-lg font-semibold text-n-slate-12 mb-0">
|
||||
{{ formatPrice(totalPrice) }}
|
||||
</p>
|
||||
<p class="text-xs text-n-slate-11 mb-0">
|
||||
({{ formatPrice(pricePerMonth) }})
|
||||
{{
|
||||
t('BILLING_SETTINGS_V2.PLAN_SUMMARY.PER_MONTH', {
|
||||
price: formatPrice(pricePerMonth),
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Renewal Date -->
|
||||
<div>
|
||||
<p class="text-xs text-n-slate-11 mb-1">
|
||||
{{ t('BILLING_SETTINGS_V2.PLAN_SUMMARY.RENEWAL_DATE') }}
|
||||
@@ -141,7 +141,6 @@ const handleIncreaseSeats = () => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Credits Included -->
|
||||
<div>
|
||||
<p class="text-xs text-n-slate-11 mb-1">
|
||||
{{ t('BILLING_SETTINGS_V2.PLAN_SUMMARY.CREDITS_INCLUDED') }}
|
||||
@@ -155,17 +154,16 @@ const handleIncreaseSeats = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Seats Management -->
|
||||
<div
|
||||
v-if="!isCancelling"
|
||||
class="flex items-center justify-between p-3 rounded-lg bg-n-slate-2 dark:bg-n-solid-2 mb-4"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon="i-lucide-users" class="text-n-slate-11" />
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-n-slate-12">
|
||||
<h6 class="text-sm font-semibold text-n-slate-12">
|
||||
{{ t('BILLING_SETTINGS_V2.PLAN_SUMMARY.NUMBER_OF_SEATS') }}
|
||||
</p>
|
||||
</h6>
|
||||
<p class="text-xs text-n-slate-11">
|
||||
{{ t('BILLING_SETTINGS_V2.PLAN_SUMMARY.ADJUST_SEATS_HINT') }}
|
||||
</p>
|
||||
|
||||
+4
-6
@@ -50,19 +50,18 @@ const formatNumber = num => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-5 pb-5">
|
||||
<div class="mt-5">
|
||||
<div class="grid gap-4 grid-cols-1 xs:grid-cols-2 xl:grid-cols-4">
|
||||
<div
|
||||
v-for="plan in plans"
|
||||
:key="plan.id || plan.display_name"
|
||||
class="relative p-4 rounded-xl outline outline-1 transition-all"
|
||||
class="relative p-4 rounded-xl border transition-all"
|
||||
:class="
|
||||
isCurrentPlan(plan)
|
||||
? 'outline-n-blue-8 bg-n-blue-3'
|
||||
: 'outline-n-weak hover:outline-n-slate-8 hover:shadow-sm'
|
||||
? 'border-n-blue-8 bg-n-blue-3'
|
||||
: 'border-n-weak hover:border-n-slate-8 hover:shadow-sm'
|
||||
"
|
||||
>
|
||||
<!-- Recommended Badge -->
|
||||
<div
|
||||
v-if="plan.recommended"
|
||||
class="absolute -top-2 -right-2 px-2 py-0.5 bg-n-teal-9 text-white text-xs font-semibold rounded-md"
|
||||
@@ -124,7 +123,6 @@ const formatNumber = num => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="plans.length === 0 && !isLoading" class="py-8 text-center">
|
||||
<p class="text-sm text-n-slate-11">
|
||||
{{ t('BILLING_SETTINGS_V2.PRICING_PLANS.NO_PLANS') }}
|
||||
|
||||
+18
-23
@@ -190,7 +190,6 @@ const handleUpdateSeats = async ({ quantity, direction }) => {
|
||||
:title="$t('BILLING_SETTINGS_V2.PRICING_PLANS.TITLE')"
|
||||
:description="$t('BILLING_SETTINGS_V2.PRICING_PLANS.DESCRIPTION')"
|
||||
>
|
||||
<!-- Cancellation Notice -->
|
||||
<Notice
|
||||
v-if="isCancellingAtPeriodEnd"
|
||||
color="amber"
|
||||
@@ -210,7 +209,6 @@ const handleUpdateSeats = async ({ quantity, direction }) => {
|
||||
</p>
|
||||
</Notice>
|
||||
|
||||
<!-- Pending Changes Notice -->
|
||||
<Notice
|
||||
v-if="hasPendingChanges && !isCancellingAtPeriodEnd"
|
||||
color="blue"
|
||||
@@ -242,7 +240,6 @@ const handleUpdateSeats = async ({ quantity, direction }) => {
|
||||
</p>
|
||||
</Notice>
|
||||
|
||||
<!-- Plan Summary -->
|
||||
<PlanSummary
|
||||
v-if="hasActiveSubscription && currentPlan"
|
||||
:plan-name="currentPlan.name"
|
||||
@@ -260,28 +257,27 @@ const handleUpdateSeats = async ({ quantity, direction }) => {
|
||||
@update-seats="handleUpdateSeats"
|
||||
/>
|
||||
|
||||
<!-- Plans Grid -->
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-500 ease-out"
|
||||
enter-from-class="opacity-0 -translate-y-2 scale-98"
|
||||
enter-to-class="opacity-100 translate-y-0 scale-100"
|
||||
leave-active-class="transition-all duration-400 ease-in-out"
|
||||
leave-from-class="opacity-100 translate-y-0 scale-100"
|
||||
leave-to-class="opacity-0 -translate-y-2 scale-98"
|
||||
<div
|
||||
class="transition-all duration-500 ease-in-out grid overflow-hidden mx-5 !mt-0"
|
||||
:class="
|
||||
!hasActiveSubscription || showAllPlans
|
||||
? 'grid-rows-[1fr] opacity-100'
|
||||
: 'grid-rows-[0fr] opacity-0'
|
||||
"
|
||||
>
|
||||
<PlansGrid
|
||||
v-if="!hasActiveSubscription || showAllPlans"
|
||||
:plans="transformedPlans"
|
||||
:current-plan-id="currentPlanId"
|
||||
:has-active-subscription="hasActiveSubscription"
|
||||
:is-cancelling-at-period-end="isCancellingAtPeriodEnd"
|
||||
:is-loading="isLoading"
|
||||
@select-plan="openSubscribeModal"
|
||||
/>
|
||||
</Transition>
|
||||
<div class="overflow-hidden">
|
||||
<PlansGrid
|
||||
:plans="transformedPlans"
|
||||
:current-plan-id="currentPlanId"
|
||||
:has-active-subscription="hasActiveSubscription"
|
||||
:is-cancelling-at-period-end="isCancellingAtPeriodEnd"
|
||||
:is-loading="isLoading"
|
||||
@select-plan="openSubscribeModal"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</BillingCard>
|
||||
|
||||
<!-- Subscribe Dialog -->
|
||||
<SubscribeDialog
|
||||
ref="subscribeDialogRef"
|
||||
:plan="selectedPlan"
|
||||
@@ -289,7 +285,6 @@ const handleUpdateSeats = async ({ quantity, direction }) => {
|
||||
@subscribe="handleSubscribe"
|
||||
/>
|
||||
|
||||
<!-- Cancel Subscription Modal -->
|
||||
<CancelSubscriptionDialog
|
||||
ref="cancelModalRef"
|
||||
:is-canceling="isCanceling"
|
||||
|
||||
+44
-59
@@ -82,74 +82,59 @@ defineExpose({
|
||||
@confirm="handleSubscribe"
|
||||
>
|
||||
<div v-if="plan" class="space-y-4">
|
||||
<!-- Plan Info -->
|
||||
<div class="p-4 rounded-lg bg-n-slate-2 dark:bg-n-solid-2">
|
||||
<h4 class="font-semibold text-n-slate-12">{{ plan.name }}</h4>
|
||||
<div class="px-4 py-3 rounded-lg bg-n-slate-2 dark:bg-n-solid-2">
|
||||
<h4 class="font-medium text-n-slate-12">
|
||||
{{ t('BILLING_SETTINGS_V2.SUBSCRIBE_MODAL.PLAN_NAME') }}
|
||||
<span class="text-n-slate-12 font-semibold">{{ plan.name }}</span>
|
||||
</h4>
|
||||
<p class="mt-1 text-sm text-n-slate-11">{{ plan.description }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Quantity Selector -->
|
||||
<div
|
||||
class="p-4 rounded-lg bg-n-slate-2 dark:bg-n-solid-2 border border-n-slate-4 dark:border-n-solid-4"
|
||||
class="p-4 rounded-lg bg-n-slate-2 dark:bg-n-solid-2 border border-n-slate-4 dark:border-n-solid-4 flex items-center justify-between"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-n-slate-12">
|
||||
{{ t('BILLING_SETTINGS_V2.SUBSCRIBE_MODAL.NUMBER_OF_SEATS') }}
|
||||
</label>
|
||||
<p class="mt-0.5 text-xs text-n-slate-11">
|
||||
{{ t('BILLING_SETTINGS_V2.SUBSCRIBE_MODAL.MIN') }}:
|
||||
{{ minSeats }} •
|
||||
{{ t('BILLING_SETTINGS_V2.SUBSCRIBE_MODAL.MAX') }}:
|
||||
{{ maxSeats }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="faded"
|
||||
color="slate"
|
||||
size="sm"
|
||||
icon="i-lucide-minus"
|
||||
:disabled="quantity <= minSeats"
|
||||
@click="decrementQuantity"
|
||||
/>
|
||||
<div class="relative">
|
||||
<Input
|
||||
v-model="quantity"
|
||||
type="number"
|
||||
:min="String(minSeats)"
|
||||
:max="String(maxSeats)"
|
||||
custom-input-class="w-16 text-center font-semibold text-base"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="faded"
|
||||
color="slate"
|
||||
size="sm"
|
||||
icon="i-lucide-plus"
|
||||
:disabled="quantity >= maxSeats"
|
||||
@click="incrementQuantity"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-n-slate-12">
|
||||
{{ t('BILLING_SETTINGS_V2.SUBSCRIBE_MODAL.NUMBER_OF_SEATS') }}
|
||||
</label>
|
||||
<p class="mt-0.5 text-xs text-n-slate-11">
|
||||
{{ t('BILLING_SETTINGS_V2.SUBSCRIBE_MODAL.MIN') }}: {{ minSeats }} •
|
||||
{{ t('BILLING_SETTINGS_V2.SUBSCRIBE_MODAL.MAX') }}:
|
||||
{{ maxSeats }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs text-n-slate-11">
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<span class="w-1 h-1 rounded-full bg-n-blue-9" />
|
||||
{{ formatPrice(plan.base_price) }}
|
||||
{{ t('BILLING_SETTINGS_V2.SUBSCRIBE_MODAL.PER_SEAT') }}
|
||||
</span>
|
||||
<span class="text-n-slate-8">•</span>
|
||||
<span class="font-semibold text-n-slate-12">
|
||||
{{ formatPrice(totalPrice) }}
|
||||
{{ t('BILLING_SETTINGS_V2.SUBSCRIBE_MODAL.TOTAL') }}
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="faded"
|
||||
color="slate"
|
||||
size="sm"
|
||||
icon="i-lucide-minus"
|
||||
:disabled="quantity <= minSeats"
|
||||
@click="decrementQuantity"
|
||||
/>
|
||||
<div class="relative">
|
||||
<Input
|
||||
v-model="quantity"
|
||||
type="number"
|
||||
:min="String(minSeats)"
|
||||
:max="String(maxSeats)"
|
||||
custom-input-class="w-16 text-center font-semibold text-base"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="faded"
|
||||
color="slate"
|
||||
size="sm"
|
||||
icon="i-lucide-plus"
|
||||
:disabled="quantity >= maxSeats"
|
||||
@click="incrementQuantity"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary -->
|
||||
<div class="p-4 rounded-lg bg-n-blue-2 dark:bg-n-blue-3 space-y-2">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-n-slate-11">
|
||||
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
import Notice from 'dashboard/components-next/notice/Notice.vue';
|
||||
|
||||
const props = defineProps({
|
||||
option: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isProcessing: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['topup']);
|
||||
|
||||
const { t } = useI18n();
|
||||
const dialogRef = ref(null);
|
||||
|
||||
const formatPrice = price => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
}).format(price);
|
||||
};
|
||||
|
||||
const formatNumber = num => {
|
||||
return new Intl.NumberFormat('en-US').format(num);
|
||||
};
|
||||
|
||||
const handleTopup = () => {
|
||||
emit('topup', {
|
||||
credits: props.option.credits,
|
||||
});
|
||||
};
|
||||
|
||||
const open = () => {
|
||||
dialogRef.value?.open();
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
dialogRef.value?.close();
|
||||
};
|
||||
|
||||
defineExpose({ open, close });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
ref="dialogRef"
|
||||
type="edit"
|
||||
width="sm"
|
||||
:title="t('BILLING_SETTINGS_V2.TOPUP_MODAL.TITLE')"
|
||||
:confirm-button-label="t('BILLING_SETTINGS_V2.TOPUP_MODAL.CONFIRM')"
|
||||
:cancel-button-label="t('BILLING_SETTINGS_V2.TOPUP_MODAL.CANCEL')"
|
||||
:is-loading="isProcessing"
|
||||
@confirm="handleTopup"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div class="p-4 rounded-lg bg-n-blue-2 dark:bg-n-blue-3 text-center">
|
||||
<div class="text-4xl font-bold text-n-blue-11">
|
||||
{{ formatNumber(option.credits) }}
|
||||
</div>
|
||||
<div class="mt-1 text-sm text-n-slate-11">
|
||||
{{ t('BILLING_SETTINGS_V2.TOPUP_MODAL.CREDITS') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 rounded-lg bg-n-slate-2 dark:bg-n-solid-2 space-y-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm text-n-slate-11">
|
||||
{{ t('BILLING_SETTINGS_V2.TOPUP_MODAL.PRICE') }}
|
||||
</span>
|
||||
<span class="text-2xl font-bold text-n-slate-12">
|
||||
{{ formatPrice(option.price) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="option.description"
|
||||
class="pt-3 border-t border-n-weak text-sm text-n-slate-11"
|
||||
>
|
||||
{{ option.description }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Notice
|
||||
color="teal"
|
||||
icon="i-lucide-info"
|
||||
:message="t('BILLING_SETTINGS_V2.TOPUP_MODAL.INFO')"
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
-114
@@ -1,114 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import Modal from 'dashboard/components/Modal.vue';
|
||||
import ButtonV4 from 'next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
option: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isProcessing: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close', 'topup']);
|
||||
|
||||
const show = ref(true);
|
||||
|
||||
const formatPrice = price => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
}).format(price);
|
||||
};
|
||||
|
||||
const formatNumber = num => {
|
||||
return new Intl.NumberFormat('en-US').format(num);
|
||||
};
|
||||
|
||||
const handleTopup = () => {
|
||||
emit('topup', {
|
||||
credits: props.option.credits,
|
||||
});
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
show.value = false;
|
||||
emit('close');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal v-model:show="show" :on-close="close">
|
||||
<div class="flex flex-col h-auto overflow-auto">
|
||||
<div class="w-full px-5 py-4 border-b border-n-weak">
|
||||
<h3 class="text-lg font-semibold text-n-800">
|
||||
{{ $t('BILLING_SETTINGS_V2.TOPUP_MODAL.TITLE') }}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="p-5 space-y-4">
|
||||
<!-- Option Info -->
|
||||
<div class="p-4 bg-n-25 rounded-lg">
|
||||
<h4 class="text-sm font-medium text-n-700 uppercase tracking-wide">
|
||||
{{ $t('BILLING_SETTINGS_V2.TOPUP_MODAL.SELECTED_OPTION') }}
|
||||
</h4>
|
||||
<div class="mt-2 text-center">
|
||||
<div class="text-4xl font-bold text-b-800">
|
||||
{{ formatNumber(option.credits) }}
|
||||
</div>
|
||||
<div class="mt-1 text-sm text-n-600">
|
||||
{{ $t('BILLING_SETTINGS_V2.TOPUP_MODAL.CREDITS') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary -->
|
||||
<div class="p-4 bg-b-50 rounded-lg space-y-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm text-n-700">{{
|
||||
$t('BILLING_SETTINGS_V2.TOPUP_MODAL.PRICE')
|
||||
}}</span>
|
||||
<span class="text-2xl font-bold text-b-800">
|
||||
{{ formatPrice(option.price) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="option.description" class="pt-2 border-t border-b-200">
|
||||
<p class="text-xs text-n-600">
|
||||
{{ option.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<div class="p-3 bg-g-50 rounded-lg">
|
||||
<div class="flex gap-2">
|
||||
<span class="i-lucide-info text-g-700 flex-shrink-0 mt-0.5" />
|
||||
<p class="text-xs text-g-800">
|
||||
{{ $t('BILLING_SETTINGS_V2.TOPUP_MODAL.INFO') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 px-5 py-4 border-t border-n-weak">
|
||||
<ButtonV4 sm faded slate :disabled="isProcessing" @click="close">
|
||||
{{ $t('BILLING_SETTINGS_V2.TOPUP_MODAL.CANCEL') }}
|
||||
</ButtonV4>
|
||||
<ButtonV4
|
||||
sm
|
||||
solid
|
||||
blue
|
||||
:is-loading="isProcessing"
|
||||
:disabled="isProcessing"
|
||||
@click="handleTopup"
|
||||
>
|
||||
{{ $t('BILLING_SETTINGS_V2.TOPUP_MODAL.CONFIRM') }}
|
||||
</ButtonV4>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
+61
-73
@@ -1,13 +1,12 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, nextTick } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore } from 'dashboard/composables/store.js';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import BillingCard from '../../billing/components/BillingCard.vue';
|
||||
import ButtonV4 from 'next/button/Button.vue';
|
||||
import TopupModal from './TopupModal.vue';
|
||||
import Button from 'next/button/Button.vue';
|
||||
import TopupDialog from './TopupDialog.vue';
|
||||
|
||||
const props = defineProps({
|
||||
defineProps({
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
@@ -23,10 +22,9 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const showTopupModal = ref(false);
|
||||
const topupDialogRef = ref(null);
|
||||
const selectedOption = ref(null);
|
||||
|
||||
const formatPrice = price => {
|
||||
@@ -41,90 +39,80 @@ const formatNumber = num => {
|
||||
return new Intl.NumberFormat('en-US').format(num);
|
||||
};
|
||||
|
||||
const openTopupModal = option => {
|
||||
const openTopupDialog = async option => {
|
||||
selectedOption.value = option;
|
||||
showTopupModal.value = true;
|
||||
await nextTick();
|
||||
topupDialogRef.value?.open();
|
||||
};
|
||||
|
||||
const handleTopup = async data => {
|
||||
const result = await store.dispatch('accounts/v2Topup', data);
|
||||
if (result.success) {
|
||||
useAlert(t('BILLING_SETTINGS_V2.TOPUP_OPTIONS.TOPUP_SUCCESS'));
|
||||
showTopupModal.value = false;
|
||||
} else if (result.error) {
|
||||
// Display validation error from API
|
||||
showTopupModal.value = false;
|
||||
|
||||
// Check if it's a payment method error for custom message
|
||||
if (result.success) {
|
||||
topupDialogRef.value?.close();
|
||||
useAlert(t('BILLING_SETTINGS_V2.TOPUP_OPTIONS.TOPUP_SUCCESS'));
|
||||
} else if (result.error) {
|
||||
topupDialogRef.value?.close();
|
||||
|
||||
if (result.error.includes('payment method')) {
|
||||
useAlert(
|
||||
t('BILLING_SETTINGS_V2.TOPUP_OPTIONS.NO_PAYMENT_METHOD_ERROR'),
|
||||
'warning'
|
||||
);
|
||||
useAlert(t('BILLING_SETTINGS_V2.TOPUP_OPTIONS.NO_PAYMENT_METHOD_ERROR'));
|
||||
} else {
|
||||
// Display the error message from API
|
||||
useAlert(result.error, 'warning');
|
||||
useAlert(result.error);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<BillingCard
|
||||
:title="$t('BILLING_SETTINGS_V2.TOPUP_OPTIONS.TITLE')"
|
||||
:description="$t('BILLING_SETTINGS_V2.TOPUP_OPTIONS.DESCRIPTION')"
|
||||
>
|
||||
<div class="px-5 pb-5">
|
||||
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div
|
||||
v-for="option in options"
|
||||
:key="option.id"
|
||||
class="p-4 border border-n-weak rounded-lg hover:border-b-500 hover:shadow-sm transition-all cursor-pointer"
|
||||
@click="openTopupModal(option)"
|
||||
>
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-b-800">
|
||||
{{ formatNumber(option.credits) }}
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-n-600 uppercase tracking-wide">
|
||||
{{ $t('BILLING_SETTINGS_V2.TOPUP_OPTIONS.CREDITS') }}
|
||||
</div>
|
||||
<div class="mt-3 text-2xl font-bold text-n-800">
|
||||
{{ formatPrice(option.price) }}
|
||||
</div>
|
||||
<p v-if="option.description" class="mt-2 text-xs text-n-600">
|
||||
{{ option.description }}
|
||||
</p>
|
||||
<ButtonV4
|
||||
class="w-full mt-4"
|
||||
sm
|
||||
solid
|
||||
blue
|
||||
@click.stop="openTopupModal(option)"
|
||||
>
|
||||
{{ $t('BILLING_SETTINGS_V2.TOPUP_OPTIONS.BUY_NOW') }}
|
||||
</ButtonV4>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<h6 class="text-base font-semibold mb-2">
|
||||
{{ t('BILLING_SETTINGS_V2.TOPUP_OPTIONS.TITLE') }}
|
||||
</h6>
|
||||
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div
|
||||
v-for="option in options"
|
||||
:key="option.id"
|
||||
class="p-4 border border-n-weak rounded-lg hover:border-b-500 hover:shadow-sm transition-all cursor-pointer"
|
||||
@click="openTopupDialog(option)"
|
||||
>
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-b-800">
|
||||
{{ formatNumber(option.credits) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="options.length === 0 && !isLoading" class="py-8 text-center">
|
||||
<p class="text-sm text-n-600">
|
||||
{{ $t('BILLING_SETTINGS_V2.TOPUP_OPTIONS.NO_OPTIONS') }}
|
||||
<div class="mt-1 text-xs text-n-600 uppercase tracking-wide">
|
||||
{{ $t('BILLING_SETTINGS_V2.TOPUP_OPTIONS.CREDITS') }}
|
||||
</div>
|
||||
<div class="mt-3 text-2xl font-bold text-n-800">
|
||||
{{ formatPrice(option.price) }}
|
||||
</div>
|
||||
<p v-if="option.description" class="mt-2 text-xs text-n-600">
|
||||
{{ option.description }}
|
||||
</p>
|
||||
<Button
|
||||
class="w-full mt-4"
|
||||
sm
|
||||
solid
|
||||
blue
|
||||
@click.stop="openTopupDialog(option)"
|
||||
>
|
||||
{{ $t('BILLING_SETTINGS_V2.TOPUP_OPTIONS.BUY_NOW') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</BillingCard>
|
||||
</div>
|
||||
|
||||
<!-- Topup Modal -->
|
||||
<TopupModal
|
||||
v-if="showTopupModal"
|
||||
:option="selectedOption"
|
||||
:is-processing="isProcessing"
|
||||
@close="showTopupModal = false"
|
||||
@topup="handleTopup"
|
||||
/>
|
||||
<div v-if="options.length === 0 && !isLoading" class="py-8 text-center">
|
||||
<p class="text-sm text-n-600">
|
||||
{{ $t('BILLING_SETTINGS_V2.TOPUP_OPTIONS.NO_OPTIONS') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TopupDialog
|
||||
v-if="selectedOption"
|
||||
ref="topupDialogRef"
|
||||
:option="selectedOption"
|
||||
:is-processing="isProcessing"
|
||||
@topup="handleTopup"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -241,7 +241,10 @@ export const actions = {
|
||||
commit(types.default.SET_V2_BILLING_UI_FLAG, { isTopupInProcess: true });
|
||||
try {
|
||||
await EnterpriseAccountAPI.v2Topup(data);
|
||||
// Refresh balance and ledger after successful topup
|
||||
// Wait for backend to process, then refresh balance
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 2000);
|
||||
});
|
||||
await dispatch('fetchCreditsBalance');
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user