Files
gridpilot.gg/apps/website/templates/SponsorBillingTemplate.tsx
2026-01-26 17:56:11 +01:00

217 lines
8.4 KiB
TypeScript

import { BillingSummaryPanel } from '@/components/sponsors/BillingSummaryPanel';
import { SponsorDashboardHeader } from '@/components/sponsors/SponsorDashboardHeader';
import { PayoutItem, SponsorPayoutQueueTable } from '@/components/sponsors/SponsorPayoutQueueTable';
import { ViewData } from '@/lib/contracts/view-data/ViewData';
import { siteConfig } from '@/lib/siteConfig';
import type { InvoiceDTO, PaymentMethodDTO } from '@/lib/types/tbd/SponsorBillingDTO';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Card } from '@/ui/Card';
import { Container } from '@/ui/Container';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { InfoBanner } from '@/ui/InfoBanner';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import {
Building2,
CreditCard,
Download,
ExternalLink,
LucideIcon,
Percent,
Receipt
} from 'lucide-react';
export interface SponsorBillingViewData extends ViewData {
stats: {
totalSpent: number;
pendingAmount: number;
nextPaymentDate: string;
nextPaymentAmount: number;
averageMonthlySpend: number;
};
paymentMethods: PaymentMethodDTO[];
invoices: InvoiceDTO[];
}
interface SponsorBillingTemplateProps {
viewData: SponsorBillingViewData;
billingStats: Array<{
label: string;
value: string;
subValue?: string;
icon: LucideIcon;
variant: 'success' | 'warning' | 'info' | 'default';
}>;
transactions: Array<{
id: string;
date: string;
amount: number | string;
status: string;
recipient?: string;
description?: string;
invoiceNumber?: string;
}>;
onSetDefaultPaymentMethod: (id: string) => void;
onDownloadInvoice: (id: string) => void;
}
export function SponsorBillingTemplate({
viewData,
billingStats,
transactions,
onSetDefaultPaymentMethod,
onDownloadInvoice
}: SponsorBillingTemplateProps) {
// Map transactions to PayoutItems for the new table
const payoutItems: PayoutItem[] = transactions.map(t => ({
id: t.id,
date: t.date,
amount: typeof t.amount === 'number' ? t.amount.toFixed(2) : t.amount,
status: t.status === 'paid' ? 'completed' : t.status === 'pending' ? 'pending' : 'failed',
recipient: t.recipient || 'GridPilot Platform',
description: t.description || `Invoice ${t.invoiceNumber}`
}));
return (
<Container size="lg" spacing="md">
<Stack gap={8}>
<SponsorDashboardHeader
sponsorName="Sponsor"
onRefresh={() => console.log('Refresh')}
/>
<BillingSummaryPanel stats={billingStats} />
<Box display="grid" gridCols={{ base: 1, lg: 12 }} gap={8}>
<Box colSpan={{ base: 1, lg: 8 }}>
<Stack gap={8}>
{/* Billing History */}
<Box>
<Stack direction="row" align="center" justify="between" mb={4}>
<Heading level={3}>Billing History</Heading>
<Button variant="secondary" size="sm" icon={<Icon icon={Download} size={4} />} onClick={() => onDownloadInvoice('all')}>
Export All
</Button>
</Stack>
<Card p={0} overflow="hidden">
<SponsorPayoutQueueTable payouts={payoutItems} />
</Card>
</Box>
{/* Platform Fees & VAT */}
<Box display="grid" gridCols={{ base: 1, md: 2 }} gap={6}>
<Card overflow="hidden">
<Box p={4} borderBottom borderColor="border-charcoal-outline" bg="bg-iron-gray/10">
<Stack direction="row" align="center" gap={2}>
<Icon icon={Percent} size={4} color="text-primary-blue" />
<Text size="xs" weight="bold" uppercase letterSpacing="wider" color="text-gray-400">
Platform Fee
</Text>
</Stack>
</Box>
<Box p={5}>
<Text size="3xl" weight="bold" color="text-white" block mb={2}>
{siteConfig.fees.platformFeePercent}%
</Text>
<Text size="sm" color="text-gray-400" block>
{siteConfig.fees.description}
</Text>
</Box>
</Card>
<Card overflow="hidden">
<Box p={4} borderBottom borderColor="border-charcoal-outline" bg="bg-iron-gray/10">
<Stack direction="row" align="center" gap={2}>
<Icon icon={Receipt} size={4} color="text-performance-green" />
<Text size="xs" weight="bold" uppercase letterSpacing="wider" color="text-gray-400">
VAT Information
</Text>
</Stack>
</Box>
<Box p={5}>
<Text size="sm" color="text-gray-400" block mb={4}>
{siteConfig.vat.notice}
</Text>
<Stack gap={2}>
<Box display="flex" justifyContent="between">
<Text size="sm" color="text-gray-500">Standard Rate</Text>
<Text size="sm" color="text-white" weight="medium">{siteConfig.vat.standardRate}%</Text>
</Box>
<Box display="flex" justifyContent="between">
<Text size="sm" color="text-gray-500">B2B Reverse Charge</Text>
<Text size="sm" color="text-performance-green" weight="medium">Available</Text>
</Box>
</Stack>
</Box>
</Card>
</Box>
</Stack>
</Box>
<Box colSpan={{ base: 1, lg: 4 }}>
<Stack gap={6}>
{/* Payment Methods */}
<Card>
<Stack gap={4}>
<Heading level={3}>Payment Methods</Heading>
<Stack gap={3}>
{viewData.paymentMethods.map((method: PaymentMethodDTO) => (
<Box
key={method.id}
p={3}
rounded="lg"
border
borderColor={method.isDefault ? 'border-primary-blue/50' : 'border-charcoal-outline/50'}
bg={method.isDefault ? 'bg-primary-blue/5' : 'bg-iron-gray/5'}
>
<Stack direction="row" align="center" justify="between">
<Stack direction="row" align="center" gap={3}>
<Icon icon={method.type === 'sepa' ? Building2 : CreditCard} size={5} color={method.isDefault ? 'text-primary-blue' : 'text-gray-400'} />
<Box>
<Text size="sm" weight="medium" color="text-white">
{method.brand || 'Bank Account'} {method.last4}
</Text>
{method.isDefault && (
<Text size="xs" color="text-primary-blue" weight="medium" block>Default</Text>
)}
</Box>
</Stack>
{!method.isDefault && (
<Button variant="ghost" size="sm" onClick={() => onSetDefaultPaymentMethod(method.id)}>
Set Default
</Button>
)}
</Stack>
</Box>
))}
</Stack>
<InfoBanner type="info">
<Text size="xs">All payments are securely processed by our payment provider.</Text>
</InfoBanner>
</Stack>
</Card>
{/* Support */}
<Card>
<Stack gap={4}>
<Heading level={3}>Billing Support</Heading>
<Text size="sm" color="text-gray-400">
Need help with an invoice or have questions about your plan?
</Text>
<Button variant="secondary" fullWidth icon={<Icon icon={ExternalLink} size={4} />}>
Contact Support
</Button>
</Stack>
</Card>
</Stack>
</Box>
</Box>
</Stack>
</Container>
);
}