unzip frontend
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { TrendingUp, TrendingDown, DollarSign, AlertTriangle, Clock, CheckCircle } from "lucide-react"
|
||||
import type { Client, Supplier } from "@/types/business"
|
||||
|
||||
interface CreditOverviewProps {
|
||||
clients: Client[]
|
||||
suppliers: Supplier[]
|
||||
}
|
||||
|
||||
export function CreditOverview({ clients, suppliers }: CreditOverviewProps) {
|
||||
// Calculate receivables (money owed to us by clients)
|
||||
const totalReceivables = clients.reduce((sum, client) => sum + client.outstandingAmount, 0)
|
||||
const clientsWithCredit = clients.filter((client) => client.outstandingAmount > 0)
|
||||
const overLimitClients = clients.filter((client) => client.outstandingAmount > client.creditLimit)
|
||||
const totalCreditLimit = clients.reduce((sum, client) => sum + client.creditLimit, 0)
|
||||
const creditUtilization = totalCreditLimit > 0 ? (totalReceivables / totalCreditLimit) * 100 : 0
|
||||
|
||||
// Calculate payables (money we owe to suppliers)
|
||||
const totalPayables = suppliers.reduce((sum, supplier) => sum + supplier.amountOwed, 0)
|
||||
const suppliersWithBalance = suppliers.filter((supplier) => supplier.amountOwed > 0)
|
||||
|
||||
// Net credit position (positive means we're owed more than we owe)
|
||||
const netCreditPosition = totalReceivables - totalPayables
|
||||
|
||||
// Aging analysis (simplified - in real app would use actual dates)
|
||||
const currentReceivables = totalReceivables * 0.6
|
||||
const thirtyDayReceivables = totalReceivables * 0.25
|
||||
const sixtyDayReceivables = totalReceivables * 0.1
|
||||
const ninetyPlusReceivables = totalReceivables * 0.05
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Receivables</CardTitle>
|
||||
<TrendingUp className="h-4 w-4 text-green-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-green-600">${totalReceivables.toLocaleString()}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{clientsWithCredit.length} clients with outstanding balances
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Payables</CardTitle>
|
||||
<TrendingDown className="h-4 w-4 text-red-600" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-red-600">${totalPayables.toLocaleString()}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{suppliersWithBalance.length} suppliers with outstanding balances
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Net Position</CardTitle>
|
||||
<DollarSign className={`h-4 w-4 ${netCreditPosition >= 0 ? "text-green-600" : "text-red-600"}`} />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className={`text-2xl font-bold ${netCreditPosition >= 0 ? "text-green-600" : "text-red-600"}`}>
|
||||
${Math.abs(netCreditPosition).toLocaleString()}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{netCreditPosition >= 0 ? "Net receivable position" : "Net payable position"}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Credit Utilization</CardTitle>
|
||||
<AlertTriangle className={`h-4 w-4 ${creditUtilization > 80 ? "text-red-600" : "text-yellow-600"}`} />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{creditUtilization.toFixed(1)}%</div>
|
||||
<Progress value={creditUtilization} className="mt-2" />
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
${totalReceivables.toLocaleString()} / ${totalCreditLimit.toLocaleString()} limit
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Alerts */}
|
||||
{(overLimitClients.length > 0 || creditUtilization > 90) && (
|
||||
<Card className="border-red-200 bg-red-50 dark:bg-red-900/20">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-red-800 dark:text-red-200">
|
||||
<AlertTriangle className="h-5 w-5" />
|
||||
Credit Alerts
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{overLimitClients.length > 0 && (
|
||||
<p className="text-sm text-red-700 dark:text-red-300">
|
||||
<strong>{overLimitClients.length}</strong> clients are over their credit limit
|
||||
</p>
|
||||
)}
|
||||
{creditUtilization > 90 && (
|
||||
<p className="text-sm text-red-700 dark:text-red-300">
|
||||
Overall credit utilization is critically high at {creditUtilization.toFixed(1)}%
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Aging Analysis */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Clock className="h-5 w-5" />
|
||||
Receivables Aging
|
||||
</CardTitle>
|
||||
<CardDescription>Breakdown of outstanding receivables by age</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-3 border rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<CheckCircle className="h-4 w-4 text-green-600" />
|
||||
<span className="font-medium">Current (0-30 days)</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-medium">${currentReceivables.toFixed(0)}</p>
|
||||
<Badge variant="default" className="text-xs">
|
||||
60%
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 border rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<Clock className="h-4 w-4 text-yellow-600" />
|
||||
<span className="font-medium">31-60 days</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-medium">${thirtyDayReceivables.toFixed(0)}</p>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
25%
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 border rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<AlertTriangle className="h-4 w-4 text-orange-600" />
|
||||
<span className="font-medium">61-90 days</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-medium">${sixtyDayReceivables.toFixed(0)}</p>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
10%
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 border rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<AlertTriangle className="h-4 w-4 text-red-600" />
|
||||
<span className="font-medium">90+ days</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-medium text-red-600">${ninetyPlusReceivables.toFixed(0)}</p>
|
||||
<Badge variant="destructive" className="text-xs">
|
||||
5%
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<DollarSign className="h-5 w-5" />
|
||||
Cash Flow Impact
|
||||
</CardTitle>
|
||||
<CardDescription>Credit impact on cash flow</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-3 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
||||
<div>
|
||||
<p className="font-medium text-green-800 dark:text-green-200">Expected Inflow</p>
|
||||
<p className="text-sm text-green-600 dark:text-green-400">From client payments</p>
|
||||
</div>
|
||||
<p className="text-xl font-bold text-green-600">${totalReceivables.toLocaleString()}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
||||
<div>
|
||||
<p className="font-medium text-red-800 dark:text-red-200">Expected Outflow</p>
|
||||
<p className="text-sm text-red-600 dark:text-red-400">To supplier payments</p>
|
||||
</div>
|
||||
<p className="text-xl font-bold text-red-600">${totalPayables.toLocaleString()}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<div>
|
||||
<p className="font-medium text-blue-800 dark:text-blue-200">Net Cash Impact</p>
|
||||
<p className="text-sm text-blue-600 dark:text-blue-400">
|
||||
{netCreditPosition >= 0 ? "Positive impact" : "Negative impact"}
|
||||
</p>
|
||||
</div>
|
||||
<p className={`text-xl font-bold ${netCreditPosition >= 0 ? "text-green-600" : "text-red-600"}`}>
|
||||
{netCreditPosition >= 0 ? "+" : "-"}${Math.abs(netCreditPosition).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { Search, ArrowUpRight, ArrowDownLeft, Calendar } from "lucide-react"
|
||||
|
||||
interface Transaction {
|
||||
id: string
|
||||
date: string
|
||||
type: "receivable" | "payable" | "payment_received" | "payment_made"
|
||||
entity: string
|
||||
description: string
|
||||
amount: number
|
||||
status: "pending" | "completed" | "overdue"
|
||||
reference?: string
|
||||
}
|
||||
|
||||
// Mock transaction data
|
||||
const mockTransactions: Transaction[] = [
|
||||
{
|
||||
id: "1",
|
||||
date: "2024-01-15",
|
||||
type: "receivable",
|
||||
entity: "ABC Electronics",
|
||||
description: "Invoice #INV-001 - Wireless Headphones",
|
||||
amount: 2500,
|
||||
status: "completed",
|
||||
reference: "INV-001",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
date: "2024-01-14",
|
||||
type: "payment_received",
|
||||
entity: "Tech Solutions Inc",
|
||||
description: "Payment received for Invoice #INV-002",
|
||||
amount: 1800,
|
||||
status: "completed",
|
||||
reference: "PAY-001",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
date: "2024-01-13",
|
||||
type: "payable",
|
||||
entity: "Electronics Wholesale Co",
|
||||
description: "Purchase Order #PO-001 - Components",
|
||||
amount: 3200,
|
||||
status: "pending",
|
||||
reference: "PO-001",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
date: "2024-01-12",
|
||||
type: "payment_made",
|
||||
entity: "Global Tech Supplies",
|
||||
description: "Payment for Invoice #SUP-001",
|
||||
amount: 1500,
|
||||
status: "completed",
|
||||
reference: "PAY-002",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
date: "2024-01-11",
|
||||
type: "receivable",
|
||||
entity: "Mobile World",
|
||||
description: "Invoice #INV-003 - Phone Cases",
|
||||
amount: 950,
|
||||
status: "overdue",
|
||||
reference: "INV-003",
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
date: "2024-01-10",
|
||||
type: "payable",
|
||||
entity: "Premium Components Ltd",
|
||||
description: "Purchase Order #PO-002 - Premium Parts",
|
||||
amount: 4500,
|
||||
status: "pending",
|
||||
reference: "PO-002",
|
||||
},
|
||||
]
|
||||
|
||||
export function TransactionHistory() {
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [typeFilter, setTypeFilter] = useState<string>("all")
|
||||
const [statusFilter, setStatusFilter] = useState<string>("all")
|
||||
|
||||
const filteredTransactions = mockTransactions.filter((transaction) => {
|
||||
const matchesSearch =
|
||||
transaction.entity.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
transaction.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(transaction.reference && transaction.reference.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||
|
||||
const matchesType = typeFilter === "all" || transaction.type === typeFilter
|
||||
const matchesStatus = statusFilter === "all" || transaction.status === statusFilter
|
||||
|
||||
return matchesSearch && matchesType && matchesStatus
|
||||
})
|
||||
|
||||
const getTransactionIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "receivable":
|
||||
case "payment_received":
|
||||
return <ArrowUpRight className="h-4 w-4 text-green-600" />
|
||||
case "payable":
|
||||
case "payment_made":
|
||||
return <ArrowDownLeft className="h-4 w-4 text-red-600" />
|
||||
default:
|
||||
return <Calendar className="h-4 w-4 text-muted-foreground" />
|
||||
}
|
||||
}
|
||||
|
||||
const getTransactionColor = (type: string) => {
|
||||
switch (type) {
|
||||
case "receivable":
|
||||
case "payment_received":
|
||||
return "text-green-600"
|
||||
case "payable":
|
||||
case "payment_made":
|
||||
return "text-red-600"
|
||||
default:
|
||||
return "text-foreground"
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case "completed":
|
||||
return (
|
||||
<Badge variant="default" className="text-xs">
|
||||
Completed
|
||||
</Badge>
|
||||
)
|
||||
case "pending":
|
||||
return (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Pending
|
||||
</Badge>
|
||||
)
|
||||
case "overdue":
|
||||
return (
|
||||
<Badge variant="destructive" className="text-xs">
|
||||
Overdue
|
||||
</Badge>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{status}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Calendar className="h-5 w-5" />
|
||||
Transaction History
|
||||
</CardTitle>
|
||||
<CardDescription>Complete history of all credit-related transactions</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{/* Filters */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 mb-6">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
||||
<Input
|
||||
placeholder="Search by entity, description, or reference..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Select value={typeFilter} onValueChange={setTypeFilter}>
|
||||
<SelectTrigger className="w-full sm:w-[180px]">
|
||||
<SelectValue placeholder="Filter by type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Types</SelectItem>
|
||||
<SelectItem value="receivable">Receivables</SelectItem>
|
||||
<SelectItem value="payable">Payables</SelectItem>
|
||||
<SelectItem value="payment_received">Payments Received</SelectItem>
|
||||
<SelectItem value="payment_made">Payments Made</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-full sm:w-[180px]">
|
||||
<SelectValue placeholder="Filter by status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Status</SelectItem>
|
||||
<SelectItem value="completed">Completed</SelectItem>
|
||||
<SelectItem value="pending">Pending</SelectItem>
|
||||
<SelectItem value="overdue">Overdue</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Transaction Table */}
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Entity</TableHead>
|
||||
<TableHead>Description</TableHead>
|
||||
<TableHead>Amount</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Reference</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredTransactions.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="text-center py-8 text-muted-foreground">
|
||||
{searchTerm || typeFilter !== "all" || statusFilter !== "all"
|
||||
? "No transactions found matching your filters."
|
||||
: "No transactions recorded yet."}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredTransactions.map((transaction) => (
|
||||
<TableRow key={transaction.id}>
|
||||
<TableCell>
|
||||
<div className="font-medium">{new Date(transaction.date).toLocaleDateString()}</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
{getTransactionIcon(transaction.type)}
|
||||
<span className="capitalize text-sm">{transaction.type.replace("_", " ")}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="font-medium">{transaction.entity}</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="max-w-[300px] truncate" title={transaction.description}>
|
||||
{transaction.description}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className={`font-medium ${getTransactionColor(transaction.type)}`}>
|
||||
{transaction.type.includes("receivable") || transaction.type.includes("payment_received")
|
||||
? "+"
|
||||
: "-"}
|
||||
${transaction.amount.toLocaleString()}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{getStatusBadge(transaction.status)}</TableCell>
|
||||
<TableCell>
|
||||
{transaction.reference && (
|
||||
<code className="text-xs bg-muted px-2 py-1 rounded">{transaction.reference}</code>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user