garage-erp/apps/dashboard/modules/invoices/invoice-status-badge.tsx
2026-04-23 14:38:41 +03:00

118 lines
4.3 KiB
TypeScript

"use client"
import { useEffect, useState } from "react"
import { useRouter } from "next/navigation"
import { toast } from "sonner"
import { InvoiceStatus } from "@garage/api"
import { confirm } from "@/shared/components/confirm-dialog"
import { badgeVariants } from "@/shared/components/ui/badge"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/shared/components/ui/select"
import { cn } from "@/shared/lib/utils"
import { formatEnum } from "@/shared/utils/formatters"
import { useAuthApi } from "@/shared/useApi"
type InvoiceStatusValue = (typeof InvoiceStatus)[number]
type InvoiceStatusBadgeProps = {
invoice: {
status?: string | null
id: number | string
}
}
const STATUS_TRIGGER_CLASS_NAMES: Record<InvoiceStatusValue, string> = {
draft: "border-slate-200 bg-slate-100 text-slate-700 dark:border-slate-800 dark:bg-slate-900/70 dark:text-slate-200",
open: "border-blue-200 bg-blue-100 text-blue-700 dark:border-blue-900 dark:bg-blue-950/70 dark:text-blue-200",
over_due: "border-red-200 bg-red-100 text-red-700 dark:border-red-900 dark:bg-red-950/70 dark:text-red-200",
paid: "border-emerald-200 bg-emerald-100 text-emerald-700 dark:border-emerald-900 dark:bg-emerald-950/70 dark:text-emerald-200",
partially_paid: "border-amber-200 bg-amber-100 text-amber-700 dark:border-amber-900 dark:bg-amber-950/70 dark:text-amber-200",
un_paid: "border-orange-200 bg-orange-100 text-orange-700 dark:border-orange-900 dark:bg-orange-950/70 dark:text-orange-200",
}
function isInvoiceStatus(value?: string | null): value is InvoiceStatusValue {
return !!value && InvoiceStatus.includes(value as InvoiceStatusValue)
}
export default function InvoiceStatusBadge({ invoice }: InvoiceStatusBadgeProps) {
const api = useAuthApi()
const router = useRouter()
const [isUpdating, setIsUpdating] = useState(false)
const [status, setStatus] = useState<InvoiceStatusValue | undefined>(
isInvoiceStatus(invoice.status) ? invoice.status : undefined,
)
useEffect(() => {
setStatus(isInvoiceStatus(invoice.status) ? invoice.status : undefined)
}, [invoice.status])
const handleStatusChange = async (nextStatus: string) => {
if (!isInvoiceStatus(nextStatus) || nextStatus === status || isUpdating) {
return
}
const currentStatus = status
const confirmed = await confirm({
title: "Change Invoice Status",
description: currentStatus
? `Change invoice status from ${formatEnum(currentStatus)} to ${formatEnum(nextStatus)}?`
: `Change invoice status to ${formatEnum(nextStatus)}?`,
confirmLabel: "Update",
})
if (!confirmed) {
return
}
setIsUpdating(true)
try {
const promise = api.invoices.update(String(invoice.id), { status: nextStatus })
toast.promise(promise, {
loading: `Updating invoice status to ${formatEnum(nextStatus)}...`,
success: "Invoice status updated successfully.",
error: "Failed to update invoice status.",
})
await promise
setStatus(nextStatus)
router.refresh()
} finally {
setIsUpdating(false)
}
}
return (
<Select value={status} onValueChange={handleStatusChange} disabled={isUpdating}>
<SelectTrigger
size="sm"
aria-label="Invoice status"
className={cn(
badgeVariants({ variant: "outline" }),
"h-8 gap-1.5 rounded-full px-3 py-1 text-xs font-medium shadow-none focus-visible:ring-2 [&_svg]:size-3.5",
status
? STATUS_TRIGGER_CLASS_NAMES[status]
: "border-border bg-muted/50 text-muted-foreground",
)}
>
<SelectValue placeholder="Set status">
{formatEnum(status ?? null)}
</SelectValue>
</SelectTrigger>
<SelectContent>
{InvoiceStatus.map((value) => (
<SelectItem key={value} value={value}>
{formatEnum(value)}
</SelectItem>
))}
</SelectContent>
</Select>
)
}