309 lines
10 KiB
TypeScript
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>
|
|
)
|
|
}
|