import { useState } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod/v4"; import { Dialog, DialogContent, DialogDescription, DialogTitle, } from "@/src/components/ui/dialog"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/src/components/ui/form"; import { Input } from "@/src/components/ui/input"; import { Button } from "@/src/components/ui/button"; import { api } from "@/src/utils/api"; import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture"; import { toast } from "sonner"; import { Info } from "lucide-react"; const spendAlertSchema = z.object({ title: z .string() .min(1, "Title is required") .max(100, "Title must be less than 100 characters"), limit: z.coerce .number() .positive("Limit must be positive") .max(1000000, "Limit must be less than $1,000,000"), }); type SpendAlertFormInput = z.input; type SpendAlertFormOutput = z.output; interface SpendAlertDialogProps { orgId: string; alert?: { id: string; title: string; threshold: { toString(): string }; }; open: boolean; onOpenChange: (open: boolean) => void; onSuccess: () => void; } export function SpendAlertDialog({ orgId, alert, open, onOpenChange, onSuccess, }: SpendAlertDialogProps) { const [isSubmitting, setIsSubmitting] = useState(false); const capture = usePostHogClientCapture(); const form = useForm({ resolver: zodResolver(spendAlertSchema), defaultValues: { title: alert?.title ?? "", limit: alert ? parseFloat(alert.threshold.toString()) : undefined, }, }); const createMutation = api.spendAlerts.createSpendAlert.useMutation(); const updateMutation = api.spendAlerts.updateSpendAlert.useMutation(); const onSubmit = async (data: SpendAlertFormOutput) => { setIsSubmitting(true); try { if (alert) { // Update existing alert await updateMutation.mutateAsync({ orgId, id: alert.id, title: data.title, threshold: data.limit, }); capture("spend_alert:updated", { orgId, alertId: alert.id, limit: data.limit, }); toast.success("Spend alert updated successfully"); } else { // Create new alert await createMutation.mutateAsync({ orgId, title: data.title, threshold: data.limit, }); capture("spend_alert:created", { orgId, limit: data.limit, }); toast.success("Spend alert created successfully"); } onSuccess(); } catch (error) { console.error("Failed to save spend alert:", error); toast.error( `Failed to ${alert ? "update" : "create"} spend alert. Please try again.`, ); } finally { setIsSubmitting(false); } }; return ( {alert ? "Edit Spend Alert" : "Create Spend Alert"} Get notified when your organization's spending exceeds a limit.
( Alert Title )} /> ( Limit (USD) )} />
How it works
  • The limit is evaluated against your upcoming invoice total, including base fee, running usage fees, discounts, and taxes.
  • Alerts trigger once per billing cycle.
  • You will receive an email when the alert is triggered.
  • Alerts are evaluated with a 90 minute delay.
); }