initial commit
This commit is contained in:
308
components/billing/BillingHistory.tsx
Normal file
308
components/billing/BillingHistory.tsx
Normal file
@@ -0,0 +1,308 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user