@@ -8,17 +8,21 @@ import { useForm } from "react-hook-form";
88
99import dayjs from "@calcom/dayjs" ;
1010import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider" ;
11+ import { MemberInvitationModalWithoutMembers } from "@calcom/features/ee/teams/components/MemberInvitationModal" ;
1112import ServerTrans from "@calcom/lib/components/ServerTrans" ;
1213import { IS_SMS_CREDITS_ENABLED } from "@calcom/lib/constants" ;
1314import { downloadAsCsv } from "@calcom/lib/csvUtils" ;
1415import { useLocale } from "@calcom/lib/hooks/useLocale" ;
1516import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback" ;
1617import { trpc } from "@calcom/trpc/react" ;
18+ import classNames from "@calcom/ui/classNames" ;
1719import { Button } from "@calcom/ui/components/button" ;
1820import { Select } from "@calcom/ui/components/form" ;
1921import { TextField , Label , InputError } from "@calcom/ui/components/form" ;
22+ import { Icon } from "@calcom/ui/components/icon" ;
2023import { ProgressBar } from "@calcom/ui/components/progress-bar" ;
2124import { showToast } from "@calcom/ui/components/toast" ;
25+ import { Tooltip } from "@calcom/ui/components/tooltip" ;
2226
2327import { BillingCreditsSkeleton } from "./BillingCreditsSkeleton" ;
2428
@@ -29,6 +33,39 @@ type MonthOption = {
2933 endDate : string ;
3034} ;
3135
36+ type CreditRowProps = {
37+ label : string ;
38+ value : number ;
39+ isBold ?: boolean ;
40+ underline ?: "dashed" | "solid" ;
41+ className ?: string ;
42+ } ;
43+
44+ const CreditRow = ( { label, value, isBold = false , underline, className = "" } : CreditRowProps ) => {
45+ const numberFormatter = new Intl . NumberFormat ( ) ;
46+ return (
47+ < div
48+ className = { classNames (
49+ `my-1 flex justify-between` ,
50+ underline === "dashed"
51+ ? "border-subtle border-b border-dashed"
52+ : underline === "solid"
53+ ? "border-subtle border-b border-solid"
54+ : "mt-1" ,
55+ className
56+ ) } >
57+ < span
58+ className = { classNames ( "text-sm" , isBold ? "font-semibold" : "text-subtle font-medium leading-tight" ) } >
59+ { label }
60+ </ span >
61+ < span
62+ className = { classNames ( `text-sm` , isBold ? "font-semibold" : "text-subtle font-medium leading-tight" ) } >
63+ { numberFormatter . format ( value ) }
64+ </ span >
65+ </ div >
66+ ) ;
67+ } ;
68+
3269const getMonthOptions = ( ) : MonthOption [ ] => {
3370 const options : MonthOption [ ] = [ ] ;
3471 const minDate = dayjs . utc ( "2025-05-01" ) ;
@@ -60,6 +97,7 @@ export default function BillingCredits() {
6097 const monthOptions = useMemo ( ( ) => getMonthOptions ( ) , [ ] ) ;
6198 const [ selectedMonth , setSelectedMonth ] = useState < MonthOption > ( monthOptions [ 0 ] ) ;
6299 const [ isDownloading , setIsDownloading ] = useState ( false ) ;
100+ const [ showMemberInvitationModal , setShowMemberInvitationModal ] = useState ( false ) ;
63101 const utils = trpc . useUtils ( ) ;
64102
65103 const {
@@ -71,6 +109,7 @@ export default function BillingCredits() {
71109
72110 const params = useParamsWithFallback ( ) ;
73111 const orgId = session . data ?. user ?. org ?. id ;
112+ const orgSlug = session . data ?. user ?. org ?. slug ;
74113
75114 const parsedTeamId = Number ( params . id ) ;
76115 const teamId : number | undefined = Number . isFinite ( parsedTeamId )
@@ -132,117 +171,153 @@ export default function BillingCredits() {
132171 buyCreditsMutation . mutate ( { quantity : data . quantity , teamId } ) ;
133172 } ;
134173
135- const teamCreditsPercentageUsed =
136- creditsData . credits . totalMonthlyCredits > 0
137- ? ( creditsData . credits . totalRemainingMonthlyCredits / creditsData . credits . totalMonthlyCredits ) * 100
138- : 0 ;
174+ const totalCredits = creditsData . credits . totalMonthlyCredits ?? 0 ;
175+ const totalUsed = creditsData . credits . totalCreditsUsedThisMonth ?? 0 ;
176+
177+ const teamCreditsPercentageUsed = totalCredits > 0 ? ( totalUsed / totalCredits ) * 100 : 0 ;
178+ const numberFormatter = new Intl . NumberFormat ( ) ;
139179
140180 return (
141- < div className = "border-subtle mt-8 space-y-6 rounded-lg border px-6 py-6 pb-6 text-sm sm:space-y-8" >
142- < div >
143- < h2 className = "text-base font-semibold" > { t ( "credits" ) } </ h2 >
144- < ServerTrans
145- t = { t }
146- i18nKey = "view_and_manage_credits_description"
147- components = { [
148- < Link
149- key = "Credit System"
150- className = "underline underline-offset-2"
151- target = "_blank"
152- href = "https://cal.com/help/billing-and-usage/messaging-credits" >
153- Learn more
154- </ Link > ,
155- ] }
156- />
157- < div className = "-mx-6 mt-6" >
158- < hr className = "border-subtle" />
181+ < >
182+ < div className = "bg-muted border-muted mt-5 rounded-xl border p-1" >
183+ < div className = "flex flex-col gap-1 px-4 py-5" >
184+ < h2 className = "text-default text-base font-semibold leading-none" > { t ( "credits" ) } </ h2 >
185+ < p className = "text-subtle text-sm font-medium leading-tight" > { t ( "view_and_manage_credits" ) } </ p >
159186 </ div >
160- < div className = "mt-6" >
161- { creditsData . credits . totalMonthlyCredits > 0 ? (
162- < div className = "mb-4" >
163- < Label > { t ( "monthly_credits" ) } </ Label >
164- < ProgressBar
165- color = "green"
166- percentageValue = { teamCreditsPercentageUsed }
167- label = { `${ Math . max ( 0 , Math . round ( teamCreditsPercentageUsed ) ) } %` }
168- />
169- < div className = "text-subtle" >
170- < div >
171- { t ( "total_credits" , {
172- totalCredits : creditsData . credits . totalMonthlyCredits ,
173- } ) }
187+ < div className = "bg-default border-muted flex w-full rounded-[10px] border px-5 py-4" >
188+ < div className = "w-full" >
189+ { totalCredits > 0 ? (
190+ < >
191+ < div className = "mb-4" >
192+ < CreditRow
193+ label = { t ( "monthly_credits" ) }
194+ value = { totalCredits }
195+ isBold = { true }
196+ underline = "dashed"
197+ />
198+ < CreditRow label = { t ( "credits_used" ) } value = { totalUsed } underline = "solid" />
199+ < CreditRow
200+ label = { t ( "total_credits_remaining" ) }
201+ value = { creditsData . credits . totalRemainingMonthlyCredits }
202+ />
203+ < div className = "mt-4" >
204+ < ProgressBar color = "green" percentageValue = { 100 - teamCreditsPercentageUsed } />
205+ </ div >
206+ { /*750 credits per tip*/ }
207+ < div className = "mt-4 flex flex-1 items-center justify-between" >
208+ < p className = "text-subtle text-sm font-medium leading-tight" >
209+ { orgSlug ? t ( "credits_per_tip_org" ) : t ( "credits_per_tip_teams" ) }
210+ </ p >
211+ < Button onClick = { ( ) => setShowMemberInvitationModal ( true ) } size = "sm" color = "secondary" >
212+ { t ( "add_members_no_ellipsis" ) }
213+ </ Button >
214+ </ div >
174215 </ div >
175- < div >
176- { t ( "remaining_credits" , {
177- remainingCredits : creditsData . credits . totalRemainingMonthlyCredits ,
178- } ) }
216+ < div className = "-mx-5 mt-5" >
217+ < hr className = "border-subtle" />
218+ </ div >
219+ </ >
220+ ) : (
221+ < > </ >
222+ ) }
223+
224+ { /*Auto Top-Up goes here when we have it*/ }
225+ { /*<div className="-mx-5 mt-5">
226+ <hr className="border-subtle" />
227+ </div>*/ }
228+ { /*Additional Credits*/ }
229+ < form onSubmit = { handleSubmit ( onSubmit ) } className = "mt-4 flex" >
230+ < div className = "-mb-1 mr-auto w-full" >
231+ < div className = "flex justify-between" >
232+ < Label > { t ( "additional_credits" ) } </ Label >
233+ < div className = "mb-2 flex items-center gap-1" >
234+ < p className = "text-sm font-semibold leading-none" >
235+ < span className = "text-subtle font-medium" > { t ( "current_balance" ) } </ span > { " " }
236+ { numberFormatter . format ( creditsData . credits . additionalCredits ) }
237+ </ p >
238+ < Tooltip content = { t ( "view_additional_credits_expense_tip" ) } >
239+ < Icon name = "info" className = "text-emphasis h-3 w-3" />
240+ </ Tooltip >
241+ </ div >
242+ </ div >
243+ < div className = "flex w-full items-center gap-2" >
244+ < TextField
245+ required
246+ type = "number"
247+ { ...register ( "quantity" , {
248+ required : t ( "error_required_field" ) ,
249+ min : { value : 50 , message : t ( "minimum_of_credits_required" ) } ,
250+ valueAsNumber : true ,
251+ } ) }
252+ label = ""
253+ containerClassName = "w-full -mt-1"
254+ size = "sm"
255+ onChange = { ( e ) => setValue ( "quantity" , Number ( e . target . value ) ) }
256+ min = { 50 }
257+ addOnSuffix = { < > { t ( "credits" ) } </ > }
258+ />
259+ < Button color = "secondary" target = "_blank" size = "sm" type = "submit" data-testid = "buy-credits" >
260+ { t ( "buy" ) }
261+ </ Button >
179262 </ div >
180- </ div >
181- </ div >
182- ) : (
183- < > </ >
184- ) }
185- < Label >
186- { creditsData . credits . totalMonthlyCredits ? t ( "additional_credits" ) : t ( "available_credits" ) }
187- </ Label >
188- < div className = "mt-2 text-sm" > { creditsData . credits . additionalCredits } </ div >
189- < div className = "-mx-6 mb-6 mt-6" >
190- < hr className = "border-subtle mb-3 mt-3" />
191- </ div >
192- < form onSubmit = { handleSubmit ( onSubmit ) } className = "flex" >
193- < div className = "-mb-1 mr-auto" >
194- < Label > { t ( "buy_additional_credits" ) } </ Label >
195- < div className = "flex flex-col" >
196- < TextField
197- required
198- type = "number"
199- { ...register ( "quantity" , {
200- required : t ( "error_required_field" ) ,
201- min : { value : 50 , message : t ( "minimum_of_credits_required" ) } ,
202- valueAsNumber : true ,
203- } ) }
204- label = ""
205- containerClassName = "w-60"
206- onChange = { ( e ) => setValue ( "quantity" , Number ( e . target . value ) ) }
207- min = { 50 }
208- addOnSuffix = { < > { t ( "credits" ) } </ > }
209- />
210263 { errors . quantity && < InputError message = { errors . quantity . message ?? t ( "invalid_input" ) } /> }
211264 </ div >
265+ </ form >
266+ < div className = "-mx-5 mt-5" >
267+ < hr className = "border-subtle" />
212268 </ div >
213- < div className = "mt-auto" >
214- < Button
215- color = "primary"
216- target = "_blank"
217- EndIcon = "external-link"
218- type = "submit"
219- data-testid = "buy-credits" >
220- { t ( "buy_credits" ) }
221- </ Button >
222- </ div >
223- </ form >
224- < div className = "-mx-6 mb-6 mt-6" >
225- < hr className = "border-subtle mb-3 mt-3" />
226- </ div >
227- < div className = "flex" >
228- < div className = "mr-auto" >
229- < Label className = "mb-4" > { t ( "download_expense_log" ) } </ Label >
230- < div className = "mt-2 flex flex-col" >
231- < Select
232- options = { monthOptions }
233- value = { selectedMonth }
234- onChange = { ( option ) => option && setSelectedMonth ( option ) }
235- />
269+ { /*Download Expense Log*/ }
270+ < div className = "mt-4 flex" >
271+ < div className = "mr-auto w-full" >
272+ < Label className = "mb-4" > { t ( "download_expense_log" ) } </ Label >
273+ < div className = "mr-2 mt-1" >
274+ < Select
275+ size = "sm"
276+ className = "w-full"
277+ innerClassNames = { {
278+ control : "font-medium text-emphasis" ,
279+ } }
280+ options = { monthOptions }
281+ value = { selectedMonth }
282+ onChange = { ( option ) => option && setSelectedMonth ( option ) }
283+ />
284+ </ div >
285+ </ div >
286+ < div className = "mt-auto" >
287+ < Button onClick = { handleDownload } loading = { isDownloading } color = "secondary" size = "sm" >
288+ { t ( "download" ) }
289+ </ Button >
236290 </ div >
237- </ div >
238- < div className = "mt-auto" >
239- < Button onClick = { handleDownload } loading = { isDownloading } StartIcon = "file-down" >
240- { t ( "download" ) }
241- </ Button >
242291 </ div >
243292 </ div >
244293 </ div >
294+ { /*Credit Worth Section*/ }
295+ < div className = "text-subtle px-5 py-4 text-sm font-medium leading-tight" >
296+ < ServerTrans
297+ t = { t }
298+ i18nKey = "credit_worth_description"
299+ components = { [
300+ < Link
301+ key = "Credit System"
302+ className = "underline underline-offset-2"
303+ target = "_blank"
304+ href = "https://cal.com/help/billing-and-usage/messaging-credits" >
305+ Learn more
306+ </ Link > ,
307+ ] }
308+ />
309+ </ div >
245310 </ div >
246- </ div >
311+ { teamId && (
312+ < MemberInvitationModalWithoutMembers
313+ teamId = { teamId }
314+ showMemberInvitationModal = { showMemberInvitationModal }
315+ hideInvitationModal = { ( ) => setShowMemberInvitationModal ( false ) }
316+ onSettingsOpen = { ( ) => {
317+ return ;
318+ } }
319+ />
320+ ) }
321+ </ >
247322 ) ;
248323}
0 commit comments