Files
ai-wpa/components/billing/BillingHistory.tsx
2025-08-30 18:18:57 +05:30

309 lines
10 KiB
TypeScript

'use client'
import React, { useState, useEffect } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Skeleton } from '@/components/ui/skeleton'
import { useToast } from '@/hooks/use-toast'
import { formatCurrency } from '@/lib/balance-service'
interface BillingRecord {
billing_id: string
service: string
service_type: string
amount: number
currency: string
user_email: string
silicon_id: string
status: string
payment_status: string
cycle: string
service_name?: string
total_amount: number
remarks?: string
createdAt: string
updatedAt: string
}
interface BillingStats {
totalSpent: number
activeServices: number
totalServices: number
serviceBreakdown: Record<string, { count: number; amount: number }>
}
interface BillingHistoryProps {
className?: string
}
const statusColors = {
pending: 'bg-yellow-100 text-yellow-800',
active: 'bg-green-100 text-green-800',
completed: 'bg-blue-100 text-blue-800',
cancelled: 'bg-gray-100 text-gray-800',
failed: 'bg-red-100 text-red-800',
refunded: 'bg-purple-100 text-purple-800',
}
const paymentStatusColors = {
pending: 'bg-yellow-100 text-yellow-800',
paid: 'bg-green-100 text-green-800',
failed: 'bg-red-100 text-red-800',
refunded: 'bg-purple-100 text-purple-800',
}
const serviceTypeLabels = {
vps: 'VPS Server',
kubernetes: 'Kubernetes',
developer_hire: 'Developer Hire',
vpn: 'VPN Service',
hosting: 'Web Hosting',
storage: 'Cloud Storage',
database: 'Database',
ai_service: 'AI Service',
custom: 'Custom Service',
}
export default function BillingHistory({ className }: BillingHistoryProps) {
const [billings, setBillings] = useState<BillingRecord[]>([])
const [stats, setStats] = useState<BillingStats | null>(null)
const [loading, setLoading] = useState(true)
const [filter, setFilter] = useState<{
serviceType?: string
status?: string
}>({})
const [pagination, setPagination] = useState({
limit: 20,
offset: 0,
total: 0,
})
const { toast } = useToast()
const fetchBillingData = async () => {
try {
setLoading(true)
const params = new URLSearchParams({
limit: pagination.limit.toString(),
offset: pagination.offset.toString(),
})
if (filter.serviceType) params.append('serviceType', filter.serviceType)
if (filter.status) params.append('status', filter.status)
const response = await fetch(`/api/billing?${params}`)
const data = await response.json()
if (data.success) {
setBillings(data.data.billings)
setStats(data.data.stats)
setPagination((prev) => ({
...prev,
total: data.data.pagination.total,
}))
} else {
throw new Error(data.error?.message || 'Failed to fetch billing data')
}
} catch (error) {
console.error('Error fetching billing data:', error)
toast({
title: 'Error',
description: 'Failed to load billing history',
variant: 'destructive',
})
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchBillingData()
}, [filter, pagination.limit, pagination.offset])
const handleFilterChange = (key: string, value: string) => {
setFilter((prev) => ({
...prev,
[key]: value === 'all' ? undefined : value,
}))
setPagination((prev) => ({ ...prev, offset: 0 }))
}
const loadMore = () => {
setPagination((prev) => ({
...prev,
offset: prev.offset + prev.limit,
}))
}
if (loading && billings.length === 0) {
return (
<div className={`space-y-4 ${className}`}>
<Skeleton className="h-32 w-full" />
<Skeleton className="h-24 w-full" />
<Skeleton className="h-24 w-full" />
<Skeleton className="h-24 w-full" />
</div>
)
}
return (
<div className={`space-y-6 ${className}`}>
{/* Statistics Cards */}
{stats && (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Total Spent</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{formatCurrency(stats.totalSpent)}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Active Services</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.activeServices}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Total Services</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.totalServices}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Service Types</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{Object.keys(stats.serviceBreakdown).length}</div>
</CardContent>
</Card>
</div>
)}
{/* Filters */}
<Card>
<CardHeader>
<CardTitle>Billing History</CardTitle>
<CardDescription>View and manage your service billing records</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col sm:flex-row gap-4 mb-6">
<Select
value={filter.serviceType || 'all'}
onValueChange={(value) => handleFilterChange('serviceType', value)}
>
<SelectTrigger className="w-full sm:w-48">
<SelectValue placeholder="Filter by service" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Services</SelectItem>
{Object.entries(serviceTypeLabels).map(([key, label]) => (
<SelectItem key={key} value={key}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={filter.status || 'all'}
onValueChange={(value) => handleFilterChange('status', value)}
>
<SelectTrigger className="w-full sm:w-48">
<SelectValue placeholder="Filter by status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="pending">Pending</SelectItem>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="completed">Completed</SelectItem>
<SelectItem value="cancelled">Cancelled</SelectItem>
<SelectItem value="failed">Failed</SelectItem>
<SelectItem value="refunded">Refunded</SelectItem>
</SelectContent>
</Select>
</div>
{/* Billing Records */}
<div className="space-y-4">
{billings.length === 0 ? (
<div className="text-center py-8 text-gray-500">No billing records found</div>
) : (
billings.map((billing) => (
<Card key={billing.billing_id} className="border-l-4 border-l-blue-500">
<CardContent className="pt-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<h3 className="font-semibold">{billing.service}</h3>
<Badge variant="outline">
{serviceTypeLabels[
billing.service_type as keyof typeof serviceTypeLabels
] || billing.service_type}
</Badge>
</div>
<div className="text-sm text-gray-600 space-y-1">
<div>Billing ID: {billing.billing_id}</div>
<div>Cycle: {billing.cycle}</div>
{billing.remarks && <div>Remarks: {billing.remarks}</div>}
<div>Created: {new Date(billing.createdAt).toLocaleDateString()}</div>
</div>
</div>
<div className="flex flex-col sm:items-end gap-2">
<div className="text-lg font-bold">
{formatCurrency(
billing.total_amount || billing.amount,
billing.currency as 'INR' | 'USD'
)}
</div>
<div className="flex gap-2">
<Badge
className={statusColors[billing.status as keyof typeof statusColors]}
>
{billing.status}
</Badge>
<Badge
className={
paymentStatusColors[
billing.payment_status as keyof typeof paymentStatusColors
]
}
>
{billing.payment_status}
</Badge>
</div>
</div>
</div>
</CardContent>
</Card>
))
)}
</div>
{/* Load More Button */}
{billings.length > 0 && billings.length < pagination.total && (
<div className="flex justify-center mt-6">
<Button onClick={loadMore} disabled={loading}>
{loading ? 'Loading...' : 'Load More'}
</Button>
</div>
)}
</CardContent>
</Card>
</div>
)
}