Add UI for credits
This commit is contained in:
parent
871cf86925
commit
051d8ef401
|
|
@ -25,6 +25,11 @@ const getPlanProration = (subscriptionId, newPlanId) =>
|
||||||
const updateSubscriptionPlan = (subscriptionId, newPlanId, prorationDate) =>
|
const updateSubscriptionPlan = (subscriptionId, newPlanId, prorationDate) =>
|
||||||
client.post(`/organization/update-subscription-plan`, { subscriptionId, newPlanId, prorationDate })
|
client.post(`/organization/update-subscription-plan`, { subscriptionId, newPlanId, prorationDate })
|
||||||
const getCurrentUsage = () => client.get(`/organization/get-current-usage`)
|
const getCurrentUsage = () => client.get(`/organization/get-current-usage`)
|
||||||
|
const getPredictionEligibility = () => client.get(`/organization/prediction-eligibility`)
|
||||||
|
const purchaseCredits = (packageType) => client.post(`/organization/purchase-credits`, { packageType })
|
||||||
|
const getCreditsBalance = () => client.get(`/organization/credits-balance`)
|
||||||
|
const getUsageWithCredits = () => client.get(`/organization/usage-with-credits`)
|
||||||
|
const getCreditsPackages = () => client.get(`/organization/credits-packages`)
|
||||||
|
|
||||||
// workspace users
|
// workspace users
|
||||||
const getAllUsersByWorkspaceId = (workspaceId) => client.get(`/workspaceuser?workspaceId=${workspaceId}`)
|
const getAllUsersByWorkspaceId = (workspaceId) => client.get(`/workspaceuser?workspaceId=${workspaceId}`)
|
||||||
|
|
@ -55,5 +60,10 @@ export default {
|
||||||
getPlanProration,
|
getPlanProration,
|
||||||
updateSubscriptionPlan,
|
updateSubscriptionPlan,
|
||||||
getCurrentUsage,
|
getCurrentUsage,
|
||||||
|
getPredictionEligibility,
|
||||||
|
purchaseCredits,
|
||||||
|
getCreditsBalance,
|
||||||
|
getUsageWithCredits,
|
||||||
|
getCreditsPackages,
|
||||||
deleteOrganizationUser
|
deleteOrganizationUser
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ import SettingsSection from '@/ui-component/form/settings'
|
||||||
import PricingDialog from '@/ui-component/subscription/PricingDialog'
|
import PricingDialog from '@/ui-component/subscription/PricingDialog'
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
import { IconAlertCircle, IconCreditCard, IconExternalLink, IconSparkles, IconX } from '@tabler/icons-react'
|
import { IconAlertCircle, IconCoins, IconCreditCard, IconExternalLink, IconSparkles, IconX } from '@tabler/icons-react'
|
||||||
|
|
||||||
// API
|
// API
|
||||||
import accountApi from '@/api/account.api'
|
import accountApi from '@/api/account.api'
|
||||||
|
|
@ -88,6 +88,12 @@ const AccountSettings = () => {
|
||||||
const [purchasedSeats, setPurchasedSeats] = useState(0)
|
const [purchasedSeats, setPurchasedSeats] = useState(0)
|
||||||
const [occupiedSeats, setOccupiedSeats] = useState(0)
|
const [occupiedSeats, setOccupiedSeats] = useState(0)
|
||||||
const [totalSeats, setTotalSeats] = useState(0)
|
const [totalSeats, setTotalSeats] = useState(0)
|
||||||
|
const [creditsBalance, setCreditsBalance] = useState(null)
|
||||||
|
const [creditsPackages, setCreditsPackages] = useState([])
|
||||||
|
const [usageWithCredits, setUsageWithCredits] = useState(null)
|
||||||
|
const [openCreditsDialog, setOpenCreditsDialog] = useState(false)
|
||||||
|
const [selectedPackage, setSelectedPackage] = useState(null)
|
||||||
|
const [isPurchasingCredits, setIsPurchasingCredits] = useState(false)
|
||||||
|
|
||||||
const predictionsUsageInPercent = useMemo(() => {
|
const predictionsUsageInPercent = useMemo(() => {
|
||||||
return usage ? calculatePercentage(usage.predictions?.usage, usage.predictions?.limit) : 0
|
return usage ? calculatePercentage(usage.predictions?.usage, usage.predictions?.limit) : 0
|
||||||
|
|
@ -106,6 +112,11 @@ const AccountSettings = () => {
|
||||||
const getCustomerDefaultSourceApi = useApi(userApi.getCustomerDefaultSource)
|
const getCustomerDefaultSourceApi = useApi(userApi.getCustomerDefaultSource)
|
||||||
const updateAdditionalSeatsApi = useApi(userApi.updateAdditionalSeats)
|
const updateAdditionalSeatsApi = useApi(userApi.updateAdditionalSeats)
|
||||||
const getCurrentUsageApi = useApi(userApi.getCurrentUsage)
|
const getCurrentUsageApi = useApi(userApi.getCurrentUsage)
|
||||||
|
const getCreditsBalanceApi = useApi(userApi.getCreditsBalance)
|
||||||
|
const getCreditsPackagesApi = useApi(userApi.getCreditsPackages)
|
||||||
|
const getUsageWithCreditsApi = useApi(userApi.getUsageWithCredits)
|
||||||
|
const purchaseCreditsApi = useApi(userApi.purchaseCredits)
|
||||||
|
const getPredictionEligibilityApi = useApi(userApi.getPredictionEligibility)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isCloud) {
|
if (isCloud) {
|
||||||
|
|
@ -113,6 +124,10 @@ const AccountSettings = () => {
|
||||||
getPricingPlansApi.request()
|
getPricingPlansApi.request()
|
||||||
getAdditionalSeatsQuantityApi.request(currentUser?.activeOrganizationSubscriptionId)
|
getAdditionalSeatsQuantityApi.request(currentUser?.activeOrganizationSubscriptionId)
|
||||||
getCurrentUsageApi.request()
|
getCurrentUsageApi.request()
|
||||||
|
getCreditsBalanceApi.request()
|
||||||
|
getCreditsPackagesApi.request()
|
||||||
|
getUsageWithCreditsApi.request()
|
||||||
|
getPredictionEligibilityApi.request()
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [isCloud])
|
}, [isCloud])
|
||||||
|
|
@ -140,13 +155,31 @@ const AccountSettings = () => {
|
||||||
}, [getCurrentUsageApi.data])
|
}, [getCurrentUsageApi.data])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (openRemoveSeatsDialog || openAddSeatsDialog) {
|
if (getCreditsBalanceApi.data) {
|
||||||
|
setCreditsBalance(getCreditsBalanceApi.data)
|
||||||
|
}
|
||||||
|
}, [getCreditsBalanceApi.data])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (getCreditsPackagesApi.data) {
|
||||||
|
setCreditsPackages(getCreditsPackagesApi.data)
|
||||||
|
}
|
||||||
|
}, [getCreditsPackagesApi.data])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (getUsageWithCreditsApi.data) {
|
||||||
|
setUsageWithCredits(getUsageWithCreditsApi.data)
|
||||||
|
}
|
||||||
|
}, [getUsageWithCreditsApi.data])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (openRemoveSeatsDialog || openAddSeatsDialog || openCreditsDialog) {
|
||||||
setSeatsQuantity(0)
|
setSeatsQuantity(0)
|
||||||
getCustomerDefaultSourceApi.request(currentUser?.activeOrganizationCustomerId)
|
getCustomerDefaultSourceApi.request(currentUser?.activeOrganizationCustomerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [openRemoveSeatsDialog, openAddSeatsDialog])
|
}, [openRemoveSeatsDialog, openAddSeatsDialog, openCreditsDialog])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (getAdditionalSeatsProrationApi.data) {
|
if (getAdditionalSeatsProrationApi.data) {
|
||||||
|
|
@ -447,6 +480,61 @@ const AccountSettings = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handlePurchaseCredits = async (packageType) => {
|
||||||
|
try {
|
||||||
|
setIsPurchasingCredits(true)
|
||||||
|
|
||||||
|
const response = await purchaseCreditsApi.request(packageType)
|
||||||
|
|
||||||
|
if (response.data?.success) {
|
||||||
|
enqueueSnackbar({
|
||||||
|
message: 'Credits purchased successfully!',
|
||||||
|
options: {
|
||||||
|
key: new Date().getTime() + Math.random(),
|
||||||
|
variant: 'success',
|
||||||
|
action: (key) => (
|
||||||
|
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||||
|
<IconX />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Refresh credits data
|
||||||
|
getCreditsBalanceApi.request()
|
||||||
|
getUsageWithCreditsApi.request()
|
||||||
|
setOpenCreditsDialog(false)
|
||||||
|
setSelectedPackage(null)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error purchasing credits:', error)
|
||||||
|
enqueueSnackbar({
|
||||||
|
message: `Failed to purchase credits: ${
|
||||||
|
typeof error.response?.data === 'object' ? error.response.data.message : error.response?.data || error.message
|
||||||
|
}`,
|
||||||
|
options: {
|
||||||
|
key: new Date().getTime() + Math.random(),
|
||||||
|
variant: 'error',
|
||||||
|
persist: true,
|
||||||
|
action: (key) => (
|
||||||
|
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||||
|
<IconX />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setIsPurchasingCredits(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCreditsDialogClose = () => {
|
||||||
|
if (!isPurchasingCredits) {
|
||||||
|
setOpenCreditsDialog(false)
|
||||||
|
setSelectedPackage(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate empty seats
|
// Calculate empty seats
|
||||||
const emptySeats = Math.min(purchasedSeats, totalSeats - occupiedSeats)
|
const emptySeats = Math.min(purchasedSeats, totalSeats - occupiedSeats)
|
||||||
|
|
||||||
|
|
@ -726,6 +814,78 @@ const AccountSettings = () => {
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
<SettingsSection title='Credits'>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(3, 1fr)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
gridColumn: 'span 2 / span 2',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'start',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 1,
|
||||||
|
px: 2.5,
|
||||||
|
py: 2
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack sx={{ alignItems: 'center' }} flexDirection='row'>
|
||||||
|
<Typography variant='body2'>Available Credits:</Typography>
|
||||||
|
<Typography sx={{ ml: 1, color: theme.palette.success.dark }} variant='h3'>
|
||||||
|
{getCreditsBalanceApi.loading ? (
|
||||||
|
<CircularProgress size={16} />
|
||||||
|
) : (
|
||||||
|
creditsBalance?.balance || 0
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
{usageWithCredits && (
|
||||||
|
<Stack sx={{ alignItems: 'center' }} flexDirection='row'>
|
||||||
|
<Typography variant='body2'>Credits Used This Month:</Typography>
|
||||||
|
<Typography sx={{ ml: 1, color: 'inherit' }} variant='h3'>
|
||||||
|
{getUsageWithCreditsApi.loading ? (
|
||||||
|
<CircularProgress size={16} />
|
||||||
|
) : (
|
||||||
|
usageWithCredits?.creditsUsed || 0
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
<Typography
|
||||||
|
sx={{ opacity: customization.isDarkMode ? 0.7 : 1 }}
|
||||||
|
variant='body2'
|
||||||
|
color='text.secondary'
|
||||||
|
>
|
||||||
|
Purchase credits for predictions beyond your plan limits
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'end',
|
||||||
|
px: 2.5,
|
||||||
|
py: 2,
|
||||||
|
gap: 2
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StyledButton
|
||||||
|
variant='contained'
|
||||||
|
disabled={!currentUser.isOrganizationAdmin}
|
||||||
|
onClick={() => setOpenCreditsDialog(true)}
|
||||||
|
startIcon={<IconCoins />}
|
||||||
|
sx={{ borderRadius: 2, height: 40 }}
|
||||||
|
>
|
||||||
|
Buy Credits
|
||||||
|
</StyledButton>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</SettingsSection>
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
action={
|
action={
|
||||||
<StyledButton onClick={saveProfileData} sx={{ borderRadius: 2, height: 40 }} variant='contained'>
|
<StyledButton onClick={saveProfileData} sx={{ borderRadius: 2, height: 40 }} variant='contained'>
|
||||||
|
|
@ -1457,6 +1617,148 @@ const AccountSettings = () => {
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
)}
|
)}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Credits Purchase Dialog */}
|
||||||
|
<Dialog fullWidth maxWidth='sm' open={openCreditsDialog} onClose={handleCreditsDialogClose}>
|
||||||
|
<DialogTitle variant='h4'>Purchase Credits</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: 1 }}>
|
||||||
|
<Typography variant='body2' color='text.secondary'>
|
||||||
|
Credits are used for predictions beyond your plan limits. Each prediction costs 1 credit.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* Current Credits Balance */}
|
||||||
|
<Box sx={{ p: 2, backgroundColor: theme.palette.background.default, borderRadius: 1 }}>
|
||||||
|
<Typography variant='subtitle2' gutterBottom>
|
||||||
|
Current Balance
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='h4' color='success.main'>
|
||||||
|
{creditsBalance?.balance || 0} Credits
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Payment Method Check */}
|
||||||
|
{getCustomerDefaultSourceApi.loading ? (
|
||||||
|
<CircularProgress size={20} />
|
||||||
|
) : getCustomerDefaultSourceApi.data?.invoice_settings?.default_payment_method ? (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, p: 2 }}>
|
||||||
|
<Typography variant='subtitle2'>Payment Method</Typography>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
{getCustomerDefaultSourceApi.data.invoice_settings.default_payment_method.card && (
|
||||||
|
<>
|
||||||
|
<IconCreditCard size={20} stroke={1.5} color={theme.palette.primary.main} />
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<Typography sx={{ textTransform: 'capitalize' }}>
|
||||||
|
{getCustomerDefaultSourceApi.data.invoice_settings.default_payment_method.card.brand}
|
||||||
|
</Typography>
|
||||||
|
<Typography>
|
||||||
|
••••{' '}
|
||||||
|
{getCustomerDefaultSourceApi.data.invoice_settings.default_payment_method.card.last4}
|
||||||
|
</Typography>
|
||||||
|
<Typography color='text.secondary'>
|
||||||
|
(expires{' '}
|
||||||
|
{
|
||||||
|
getCustomerDefaultSourceApi.data.invoice_settings.default_payment_method.card
|
||||||
|
.exp_month
|
||||||
|
}
|
||||||
|
/
|
||||||
|
{getCustomerDefaultSourceApi.data.invoice_settings.default_payment_method.card.exp_year}
|
||||||
|
)
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, p: 2 }}>
|
||||||
|
<Typography color='error' sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<IconAlertCircle size={20} />
|
||||||
|
No payment method found
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant='contained'
|
||||||
|
endIcon={<IconExternalLink />}
|
||||||
|
onClick={() => {
|
||||||
|
setOpenCreditsDialog(false)
|
||||||
|
handleBillingPortalClick()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add Payment Method in Billing Portal
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Credit Packages */}
|
||||||
|
{getCustomerDefaultSourceApi.data?.invoice_settings?.default_payment_method && (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
<Typography variant='subtitle2'>Select Credit Package</Typography>
|
||||||
|
{getCreditsPackagesApi.loading ? (
|
||||||
|
<CircularProgress size={20} />
|
||||||
|
) : (
|
||||||
|
creditsPackages.map((pkg) => (
|
||||||
|
<Box
|
||||||
|
key={pkg.credits}
|
||||||
|
sx={{
|
||||||
|
p: 2,
|
||||||
|
border: 2,
|
||||||
|
borderColor: selectedPackage?.credits === pkg.credits ? 'primary.main' : 'divider',
|
||||||
|
borderRadius: 2,
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
'&:hover': {
|
||||||
|
borderColor: 'primary.light',
|
||||||
|
backgroundColor: 'action.hover'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClick={() => setSelectedPackage(pkg)}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Box>
|
||||||
|
<Typography variant='h6'>{pkg.credits} Credits</Typography>
|
||||||
|
<Typography variant='body2' color='text.secondary'>
|
||||||
|
${(pkg.price / 100).toFixed(2)} USD
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Typography variant='body2' color='success.main'>
|
||||||
|
${(pkg.price / 100 / pkg.credits).toFixed(3)} per credit
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
{getCustomerDefaultSourceApi.data?.invoice_settings?.default_payment_method && (
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={handleCreditsDialogClose} disabled={isPurchasingCredits}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant='contained'
|
||||||
|
onClick={() => handlePurchaseCredits(selectedPackage?.id)}
|
||||||
|
disabled={
|
||||||
|
!selectedPackage ||
|
||||||
|
isPurchasingCredits ||
|
||||||
|
getCustomerDefaultSourceApi.loading ||
|
||||||
|
!getCustomerDefaultSourceApi.data
|
||||||
|
}
|
||||||
|
startIcon={<IconCoins />}
|
||||||
|
>
|
||||||
|
{isPurchasingCredits ? (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<CircularProgress size={16} color='inherit' />
|
||||||
|
Processing...
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
`Purchase ${selectedPackage?.credits || 0} Credits for $${((selectedPackage?.price || 0) / 100).toFixed(2)}`
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
</MainCard>
|
</MainCard>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue