"use client"; import { useSession } from "next-auth/react"; import { useRouter, usePathname } from "next/navigation"; import { useState, useMemo } from "react"; import { useForm } from "react-hook-form"; import dayjs from "@calcom/dayjs"; import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider"; import { IS_SMS_CREDITS_ENABLED } from "@calcom/lib/constants"; import { downloadAsCsv } from "@calcom/lib/csvUtils"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback"; import { trpc } from "@calcom/trpc/react"; import classNames from "@calcom/ui/classNames"; import { Button } from "@calcom/ui/components/button"; import { Select } from "@calcom/ui/components/form"; import { TextField, Label, InputError } from "@calcom/ui/components/form"; import { ProgressBar } from "@calcom/ui/components/progress-bar"; import { InfoIcon } from "@coss/ui/icons"; import { showToast } from "@calcom/ui/components/toast"; import { Tooltip } from "@calcom/ui/components/tooltip"; import { MemberInvitationModalWithoutMembers } from "~/ee/teams/components/MemberInvitationModal"; import { LearnMoreLink } from "@calcom/features/eventtypes/components/LearnMoreLink"; import { BillingCreditsSkeleton } from "./BillingCreditsSkeleton"; type MonthOption = { value: string; label: string; startDate: string; endDate: string; }; type CreditRowProps = { label: string; value: number; isBold?: boolean; underline?: "dashed" | "solid"; className?: string; }; const CreditRow = ({ label, value, isBold = false, underline, className = "" }: CreditRowProps) => { const numberFormatter = new Intl.NumberFormat(); return (
{label} {numberFormatter.format(value)}
); }; const getMonthOptions = (): MonthOption[] => { const options: MonthOption[] = []; const minDate = dayjs.utc("2025-05-01"); let date = dayjs.utc(); let count = 0; while ((date.isAfter(minDate) || date.isSame(minDate, "month")) && count < 12) { const startDate = date.startOf("month"); const endDate = date.endOf("month"); options.push({ value: date.format("MMMM YYYY"), label: date.format("MMMM YYYY"), startDate: startDate.toISOString(), endDate: endDate.toISOString(), }); date = date.subtract(1, "month"); count++; } return options; }; export default function BillingCredits() { const { t } = useLocale(); const router = useRouter(); const pathname = usePathname(); const session = useSession(); const orgBranding = useOrgBranding(); const monthOptions = useMemo(() => getMonthOptions(), []); const [selectedMonth, setSelectedMonth] = useState(monthOptions[0]); const [isDownloading, setIsDownloading] = useState(false); const [showMemberInvitationModal, setShowMemberInvitationModal] = useState(false); const utils = trpc.useUtils(); const { register, handleSubmit, setValue, formState: { errors }, } = useForm<{ quantity: number }>({ defaultValues: { quantity: 50 } }); const params = useParamsWithFallback(); const orgId = session.data?.user?.org?.id; const orgSlug = session.data?.user?.org?.slug; const parsedTeamId = Number(params.id); const teamId: number | undefined = Number.isFinite(parsedTeamId) ? parsedTeamId : typeof orgId === "number" ? orgId : undefined; const tokens = (pathname ?? "").split("/").filter(Boolean); const settingsIndex = tokens.indexOf("settings"); const isOrgScopedPath = settingsIndex >= 0 && ["organizations", "teams"].includes(tokens[settingsIndex + 1]); const shouldRender = IS_SMS_CREDITS_ENABLED && !(orgId && !isOrgScopedPath && !orgBranding?.slug); const { data: creditsData, isLoading } = trpc.viewer.credits.getAllCredits.useQuery( { teamId }, { enabled: shouldRender } ); const { data: teamPlanData } = trpc.viewer.teams.hasTeamPlan.useQuery(undefined, { enabled: shouldRender && !teamId, }); const isUserInTeam = teamPlanData?.hasTeamPlan ?? false; if (!shouldRender) return null; const buyCreditsMutation = trpc.viewer.credits.buyCredits.useMutation({ onSuccess: (data) => { if (data.sessionUrl) { router.push(data.sessionUrl); } }, onError: () => { showToast(t("credit_purchase_failed"), "error"); }, }); const handleDownload = async () => { setIsDownloading(true); try { const result = await utils.viewer.credits.downloadExpenseLog.fetch({ teamId, startDate: selectedMonth.startDate, endDate: selectedMonth.endDate, }); if (result?.csvData) { const filename = `credit-expense-log-${selectedMonth.value.toLowerCase().replace(" ", "-")}.csv`; downloadAsCsv(result.csvData, filename); } else { showToast(t("error_downloading_expense_log"), "error"); } } catch (error) { showToast(t("error_downloading_expense_log"), "error"); } finally { setIsDownloading(false); } }; if (isLoading && teamId) return ; if (!creditsData) return null; // For personal billing page: hide credits section if user is part of a team and has no personal credits if (!teamId && isUserInTeam && creditsData.credits.additionalCredits <= 0) { return null; } const onSubmit = (data: { quantity: number }) => { buyCreditsMutation.mutate({ quantity: data.quantity, teamId }); }; const totalCredits = creditsData.credits.totalMonthlyCredits ?? 0; const totalUsed = creditsData.credits.totalCreditsUsedThisMonth ?? 0; const teamCreditsPercentageUsed = totalCredits > 0 ? (totalUsed / totalCredits) * 100 : 0; const numberFormatter = new Intl.NumberFormat(); return ( <>

{t("credits")}

{t("view_and_manage_credits")}

{totalCredits > 0 ? ( <>
{/*750 credits per tip*/}

{orgSlug ? t("credits_per_tip_org") : t("credits_per_tip_teams")}


) : ( <> )} {/*Auto Top-Up goes here when we have it*/} {/*

*/} {/*Additional Credits*/}

{t("current_balance")}{" "} {numberFormatter.format(creditsData.credits.additionalCredits)}

{/* Users who are part of a team cannot buy personal credits */} {teamId || !isUserInTeam ? (
setValue("quantity", Number(e.target.value))} min={50} addOnSuffix={<>{t("credits")}} /> ) : null} {errors.quantity && }

{/*Download Expense Log*/}