214 lines
9.8 KiB
TypeScript
214 lines
9.8 KiB
TypeScript
// src/app/(dashboard)/billing/page.tsx
|
|
'use client';
|
|
import { useState } from 'react';
|
|
import { Pencil } from 'lucide-react';
|
|
import { Button } from '@/components/ui/common/Button';
|
|
import { Badge } from '@/components/ui/common/Badge';
|
|
import { Table } from '@/components/ui/common/Table';
|
|
import { SearchBar } from '@/components/ui/common/SearchBar';
|
|
import { useAuth } from '@/lib/hooks/useAuth';
|
|
import { hasPermission } from '@/shared/permissions';
|
|
// Mock data matching your design
|
|
const invoices = [
|
|
{ id: '#123456', patient: 'Ali Rahmani', date: '24/9/2026', service: 'Hygiene', amount: 300, paid: 0, status: 'unpaid' },
|
|
{ id: '#123457', patient: 'Neda Akbari', date: '01/10/2026', service: 'Filling', amount: 700, paid: 400, status: 'overdue' },
|
|
{ id: '#123458', patient: 'Nima Haghi', date: '09/12/2026', service: 'Extraction', amount: 450, paid: 450, status: 'paid' },
|
|
];
|
|
const statusColors = {
|
|
paid: 'success',
|
|
unpaid: 'warning',
|
|
overdue: 'danger',
|
|
} as const;
|
|
type StatCardColor = 'blue' | 'yellow' | 'green' | 'red';
|
|
|
|
interface StatCardProps {
|
|
title: string;
|
|
count: number;
|
|
amount: number;
|
|
color: StatCardColor;
|
|
}
|
|
export default function BillingPage() {
|
|
const { currentOrganization } = useAuth();
|
|
const [search, setSearch] = useState('');
|
|
const [statusFilter, setStatusFilter] = useState('all');
|
|
const canEditBilling = hasPermission(currentOrganization, 'TAB_BILLING_EDIT');
|
|
const stats = {
|
|
total: { count: 235, amount: 80900 },
|
|
unpaid: { count: 30, amount: 2800 },
|
|
paid: { count: 190, amount: 80900 },
|
|
overdue: { count: 235, amount: 80900 },
|
|
};
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex justify-between items-center">
|
|
<h1 className="text-2xl font-semibold text-text-primary">Billing</h1>
|
|
<Button
|
|
variant="primary"
|
|
disabled={!canEditBilling}
|
|
title={!canEditBilling ? 'Read-only access for this organization.' : undefined}
|
|
>
|
|
New Invoice
|
|
</Button>
|
|
</div>
|
|
{/* Stats Cards - Matching your design */}
|
|
<div className="grid grid-cols-4 gap-4">
|
|
<StatCard
|
|
title="Total Invoices"
|
|
count={stats.total.count}
|
|
amount={stats.total.amount}
|
|
color="blue"
|
|
/>
|
|
<StatCard
|
|
title="Unpaid Invoices"
|
|
count={stats.unpaid.count}
|
|
amount={stats.unpaid.amount}
|
|
color="yellow"
|
|
/>
|
|
<StatCard
|
|
title="Paid Invoices"
|
|
count={stats.paid.count}
|
|
amount={stats.paid.amount}
|
|
color="green"
|
|
/>
|
|
<StatCard
|
|
title="Overdue Invoices"
|
|
count={stats.overdue.count}
|
|
amount={stats.overdue.amount}
|
|
color="red"
|
|
/>
|
|
</div>
|
|
{/* Filters */}
|
|
<SearchBar
|
|
value={search}
|
|
onChange={setSearch}
|
|
placeholder="Search patients..."
|
|
actions={(
|
|
<>
|
|
{['all', 'paid', 'unpaid', 'overdue'].map((status) => (
|
|
<button
|
|
key={status}
|
|
onClick={() => setStatusFilter(status)}
|
|
className={`px-4 py-2 rounded-[var(--radius-sm)] text-sm font-medium capitalize border ${statusFilter === status
|
|
? 'bg-primary-soft text-primary border-primary/50'
|
|
: 'text-text-secondary border-border/40 hover:bg-background-card/70 hover:border-border'
|
|
}`}
|
|
>
|
|
{status}
|
|
</button>
|
|
))}
|
|
</>
|
|
)}
|
|
/>
|
|
{/* Invoices Table - Matching your design */}
|
|
<Table
|
|
headers={
|
|
<tr>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider">
|
|
Invoice ID
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider">
|
|
Patient name
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider">
|
|
Date
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider">
|
|
Service
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider">
|
|
Total amount
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider">
|
|
Paid
|
|
</th>
|
|
<th className="px-6 py-3 text-center text-xs font-medium text-text-muted uppercase tracking-wider">
|
|
Status
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider">
|
|
Action
|
|
</th>
|
|
</tr>
|
|
}
|
|
body={
|
|
<>
|
|
{invoices.map((invoice) => (
|
|
<tr key={invoice.id} className="hover:bg-background-secondary/45">
|
|
<td className="px-6 py-1.5 text-sm font-medium text-text-primary">
|
|
{invoice.id}
|
|
</td>
|
|
<td className="px-6 py-1.5 text-sm text-text-primary">
|
|
{invoice.patient}
|
|
</td>
|
|
<td className="px-6 py-1.5 text-sm text-text-secondary">
|
|
{invoice.date}
|
|
</td>
|
|
<td className="px-6 py-1.5 text-sm text-text-primary">
|
|
{invoice.service}
|
|
</td>
|
|
<td className="px-6 py-1.5 text-sm text-text-primary">
|
|
${invoice.amount}
|
|
</td>
|
|
<td className="px-6 py-1.5 text-sm text-text-primary">
|
|
${invoice.paid}
|
|
</td>
|
|
<td className="px-6 py-1.5 text-center align-middle">
|
|
<Badge
|
|
variant={statusColors[invoice.status as keyof typeof statusColors]}
|
|
className="capitalize"
|
|
>
|
|
{invoice.status}
|
|
</Badge>
|
|
</td>
|
|
<td className="px-6 py-1.5">
|
|
<button
|
|
className={`p-2 rounded-md ${canEditBilling
|
|
? 'text-text-secondary hover:bg-background-card/80 hover:text-text-primary'
|
|
: 'text-text-muted cursor-not-allowed opacity-50'}`}
|
|
disabled={!canEditBilling}
|
|
title={!canEditBilling ? 'Read-only access for this organization.' : undefined}
|
|
aria-label="Edit invoice"
|
|
>
|
|
<Pencil className="w-4 h-4" />
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</>
|
|
}
|
|
footer={(
|
|
<>
|
|
<button className="text-sm text-text-secondary hover:text-text-primary">
|
|
← Previous
|
|
</button>
|
|
<div className="text-sm text-text-secondary">
|
|
Page 1 of 10
|
|
</div>
|
|
<button className="text-sm text-text-secondary hover:text-text-primary">
|
|
Next →
|
|
</button>
|
|
</>
|
|
)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
function StatCard({ title, count, amount, color }: StatCardProps) {
|
|
const colors: Record<StatCardColor, string> = {
|
|
blue: 'bg-sky-900/30 text-sky-300 border-sky-700/60',
|
|
yellow: 'bg-amber-900/30 text-amber-300 border-amber-700/60',
|
|
green: 'bg-emerald-900/30 text-emerald-300 border-emerald-700/60',
|
|
red: 'bg-red-950/30 text-red-300 border-red-700/60',
|
|
};
|
|
|
|
return (
|
|
<div className={`p-4 rounded-xl border ${colors[color]}`}>
|
|
<p className="text-sm font-medium opacity-80">{title}</p>
|
|
<p className="text-2xl font-bold mt-1">{count}</p>
|
|
<p className="text-sm font-medium mt-1">
|
|
${amount.toLocaleString()}
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|