| "use client"; |
|
|
| import type { RouterOutputs } from "@api/trpc/routers/_app"; |
| import { Button } from "@midday/ui/button"; |
| import { |
| DropdownMenu, |
| DropdownMenuContent, |
| DropdownMenuItem, |
| DropdownMenuTrigger, |
| } from "@midday/ui/dropdown-menu"; |
| import { useToast } from "@midday/ui/use-toast"; |
| import { DotsHorizontalIcon } from "@radix-ui/react-icons"; |
| import { useMutation, useQuery } from "@tanstack/react-query"; |
| import { useCallback, useEffect, useState } from "react"; |
| import { useTRPC } from "@/trpc/client"; |
|
|
| type Props = { |
| order: RouterOutputs["billing"]["orders"]["data"][number]; |
| }; |
|
|
| export function ActionsMenu({ order }: Props) { |
| const { toast } = useToast(); |
| const trpc = useTRPC(); |
| const [isDownloading, setIsDownloading] = useState(false); |
| const [currentToast, setCurrentToast] = useState<{ |
| id: string; |
| dismiss: () => void; |
| update: (props: any) => void; |
| } | null>(null); |
| const [shouldPoll, setShouldPoll] = useState(false); |
| const [_progress, setProgress] = useState(0); |
| const [pollCount, setPollCount] = useState(0); |
|
|
| |
| const { data: invoiceStatus, error: invoiceError } = useQuery({ |
| ...trpc.billing.checkInvoiceStatus.queryOptions(order.id), |
| enabled: shouldPoll, |
| refetchInterval: shouldPoll ? 2000 : false, |
| refetchIntervalInBackground: false, |
| }); |
|
|
| |
| useEffect(() => { |
| if (!shouldPoll || !invoiceStatus) return; |
|
|
| if (invoiceStatus.status === "ready" && invoiceStatus.downloadUrl) { |
| |
| setShouldPoll(false); |
|
|
| |
| if (currentToast) { |
| currentToast.dismiss(); |
| } |
|
|
| |
| const link = document.createElement("a"); |
| link.href = invoiceStatus.downloadUrl; |
| link.download = `invoice-${order.id}.pdf`; |
| document.body.appendChild(link); |
| link.click(); |
| document.body.removeChild(link); |
|
|
| setIsDownloading(false); |
| setCurrentToast(null); |
| setProgress(0); |
| setPollCount(0); |
| } else if (invoiceStatus.status === "generating" && currentToast) { |
| |
| setPollCount((prev) => prev + 1); |
|
|
| |
| const newProgress = Math.min(90, 10 + pollCount * 8); |
| setProgress(newProgress); |
|
|
| |
| currentToast.update({ |
| title: "Generating invoice...", |
| description: `Processing your invoice (${Math.round(newProgress)}% complete)`, |
| duration: Number.POSITIVE_INFINITY, |
| variant: "progress", |
| progress: newProgress, |
| }); |
| } |
| }, [invoiceStatus, shouldPoll, order.id, toast, pollCount]); |
|
|
| |
| useEffect(() => { |
| if (invoiceError && shouldPoll) { |
| setShouldPoll(false); |
|
|
| |
| if (currentToast) { |
| currentToast.update({ |
| title: "Generation failed", |
| description: "Unable to generate invoice. Please try again later.", |
| variant: "error", |
| duration: 5000, |
| }); |
| } |
|
|
| setIsDownloading(false); |
| setCurrentToast(null); |
| setProgress(0); |
| setPollCount(0); |
| } |
| }, [invoiceError, shouldPoll, currentToast]); |
|
|
| |
| useEffect(() => { |
| if (shouldPoll) { |
| const timeout = setTimeout(() => { |
| setShouldPoll(false); |
| if (currentToast) { |
| currentToast.update({ |
| title: "Generation timeout", |
| description: |
| "Invoice generation is taking longer than expected. Please try again later.", |
| variant: "error", |
| duration: 5000, |
| }); |
| setIsDownloading(false); |
| setCurrentToast(null); |
| setProgress(0); |
| setPollCount(0); |
| } |
| }, 120000); |
|
|
| return () => clearTimeout(timeout); |
| } |
| }, [shouldPoll, currentToast]); |
|
|
| const downloadInvoiceMutation = useMutation( |
| trpc.billing.getInvoice.mutationOptions({ |
| onSuccess: (result) => { |
| if (result.status === "ready" && result.downloadUrl) { |
| |
| const link = document.createElement("a"); |
| link.href = result.downloadUrl; |
| link.download = `invoice-${order.id}.pdf`; |
| document.body.appendChild(link); |
| link.click(); |
| document.body.removeChild(link); |
|
|
| setIsDownloading(false); |
| } else if (result.status === "generating") { |
| |
| setProgress(10); |
| setPollCount(0); |
|
|
| const toastInstance = toast({ |
| title: "Generating invoice...", |
| description: "This may take a few moments", |
| duration: Number.POSITIVE_INFINITY, |
| variant: "progress", |
| progress: 10, |
| }); |
|
|
| setCurrentToast(toastInstance); |
| setShouldPoll(true); |
| } |
| }, |
| onError: (_error) => { |
| toast({ |
| title: "Download failed", |
| description: "Unable to download invoice. Please try again later.", |
| variant: "error", |
| }); |
| setIsDownloading(false); |
| setCurrentToast(null); |
| setProgress(0); |
| setPollCount(0); |
| }, |
| }), |
| ); |
|
|
| const handleDownload = useCallback(() => { |
| setIsDownloading(true); |
| downloadInvoiceMutation.mutate(order.id); |
| }, [downloadInvoiceMutation, order.id]); |
|
|
| return ( |
| <DropdownMenu> |
| <DropdownMenuTrigger asChild> |
| <Button variant="ghost" className="h-8 w-8 p-0"> |
| <DotsHorizontalIcon className="h-4 w-4" /> |
| </Button> |
| </DropdownMenuTrigger> |
| |
| <DropdownMenuContent align="end"> |
| <DropdownMenuItem onClick={handleDownload} disabled={isDownloading}> |
| {isDownloading ? "Preparing..." : "Download Invoice"} |
| </DropdownMenuItem> |
| </DropdownMenuContent> |
| </DropdownMenu> |
| ); |
| } |
|
|