diff --git a/src/app/(dashboard)/influencers/page.tsx b/src/app/(dashboard)/influencers/page.tsx index 171a423..63c331d 100644 --- a/src/app/(dashboard)/influencers/page.tsx +++ b/src/app/(dashboard)/influencers/page.tsx @@ -40,6 +40,11 @@ import { estimateRevenueFromViews, formatEstimatedRevenue, } from '@/lib/revenue-estimate'; +import { + DEFAULT_TAX_PERIOD_DAYS, + estimateTaxForPeriod, + TAX_PERIOD_OPTIONS, +} from '@/lib/tax-period-estimate'; const COMPLIANCE_STATUSES = ['compliant', 'non-compliant', 'pending', 'under-review'] as const; @@ -80,6 +85,7 @@ export default function InfluencersPage() { ); const [showAddDialog, setShowAddDialog] = useState(false); const [isCreating, setIsCreating] = useState(false); + const [taxPeriodDays, setTaxPeriodDays] = useState(DEFAULT_TAX_PERIOD_DAYS); const [form, setForm] = useState({ name: '', handle: '', @@ -154,6 +160,9 @@ export default function InfluencersPage() { filtered = filtered.filter((channel) => channel.complianceStatus === filterStatus); } + const selectedTaxPeriod = + TAX_PERIOD_OPTIONS.find((option) => option.days === taxPeriodDays) ?? TAX_PERIOD_OPTIONS[0]; + const handleCreate = async (event: React.FormEvent) => { event.preventDefault(); if (!form.name || !form.handle) return; @@ -261,6 +270,24 @@ export default function InfluencersPage() { ))} + +
@@ -298,7 +325,9 @@ export default function InfluencersPage() { ) : ( - filtered.map((channel) => ( + filtered.map((channel) => { + const periodTax = estimateTaxForPeriod(channel, taxPeriodDays); + return (

- {channel.estimatedTax !== undefined ? formatCurrency(channel.estimatedTax) : '--'} + {periodTax !== undefined ? formatCurrency(periodTax) : '--'}

- {channel.taxEstimateSource === 'none' - ? 'No estimate yet' - : formatRevenueSource(channel.taxEstimateSource)} + {periodTax !== undefined + ? `Projected for ${selectedTaxPeriod.label}` + : 'No estimate yet'}

@@ -413,7 +442,8 @@ export default function InfluencersPage() {
- )) + ); + }) )} diff --git a/src/app/(dashboard)/page.tsx b/src/app/(dashboard)/page.tsx index 7af2578..847ff7e 100644 --- a/src/app/(dashboard)/page.tsx +++ b/src/app/(dashboard)/page.tsx @@ -13,11 +13,24 @@ import { import { api } from '~convex/_generated/api'; import Link from 'next/link'; +import { useMemo, useState } from 'react'; import { Button } from '@/components/ui/button'; import { Card, CardAction, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; import { Skeleton } from '@/components/ui/skeleton'; import { formatCompactNumber, formatCurrency, formatRevenueSource } from '@/lib/product'; +import { + DEFAULT_TAX_PERIOD_DAYS, + estimateTaxForPeriod, + TAX_PERIOD_OPTIONS, +} from '@/lib/tax-period-estimate'; function MetricCard({ label, @@ -67,12 +80,26 @@ function DashboardSkeleton() { } export default function OverviewPage() { + const [taxPeriodDays, setTaxPeriodDays] = useState(DEFAULT_TAX_PERIOD_DAYS); const stats = useQuery(api.influencers.getInfluencerStats); + const channels = useQuery(api.influencers.getChannels, {}); const revenueData = useQuery(api.analytics.getRevenueByMonth); const topChannels = useQuery(api.analytics.getTopInfluencers); const recentLogs = useQuery(api.auditLogs.getRecentLogs, { limit: 5 }); + const selectedTaxPeriod = useMemo( + () => TAX_PERIOD_OPTIONS.find((option) => option.days === taxPeriodDays) ?? TAX_PERIOD_OPTIONS[0], + [taxPeriodDays], + ); + const totalTaxForPeriod = useMemo( + () => + (channels ?? []).reduce( + (sum, channel) => sum + (estimateTaxForPeriod(channel, taxPeriodDays) ?? 0), + 0, + ), + [channels, taxPeriodDays], + ); - if (stats === undefined) { + if (stats === undefined || channels === undefined) { return ; } @@ -86,8 +113,8 @@ export default function OverviewPage() { /> -
+
Revenue inputs Tax estimates +
diff --git a/src/lib/tax-period-estimate.ts b/src/lib/tax-period-estimate.ts new file mode 100644 index 0000000..37907f5 --- /dev/null +++ b/src/lib/tax-period-estimate.ts @@ -0,0 +1,88 @@ +import { estimateRevenueFromViews } from './revenue-estimate'; + +export const TAX_PERIOD_OPTIONS = [ + { days: 30, label: '30 days' }, + { days: 60, label: '60 days' }, + { days: 90, label: '90 days' }, + { days: 365, label: '1 year' }, +] as const; + +export const DEFAULT_TAX_PERIOD_DAYS = TAX_PERIOD_OPTIONS[0].days; +const DAYS_IN_YEAR = 365; + +// Ghana Revenue Authority progressive personal income tax brackets (annual, GHS). +const GHA_TAX_BRACKETS: Array<{ limit: number; rate: number }> = [ + { limit: 5_880, rate: 0 }, + { limit: 1_320, rate: 0.05 }, + { limit: 1_560, rate: 0.1 }, + { limit: 38_000, rate: 0.175 }, + { limit: 192_000, rate: 0.25 }, + { limit: 366_240, rate: 0.3 }, + { limit: Infinity, rate: 0.35 }, +]; + +function calculateProgressiveTax( + income: number, + brackets: Array<{ limit: number; rate: number }>, +): number { + if (income <= 0) return 0; + + let tax = 0; + let remaining = income; + + for (const bracket of brackets) { + if (remaining <= 0) break; + const taxable = Number.isFinite(bracket.limit) ? Math.min(remaining, bracket.limit) : remaining; + tax += taxable * bracket.rate; + remaining -= taxable; + } + + return Math.round(tax); +} + +function estimateAnnualRevenue(channel: { + estimatedAnnualRevenue?: number; + estimatedMonthlyRevenue?: number; + totalViews?: number; + topicCategories?: string[]; + channelCreatedAt?: number; +}) { + if (channel.estimatedAnnualRevenue !== undefined) { + return channel.estimatedAnnualRevenue; + } + + if (channel.estimatedMonthlyRevenue !== undefined) { + return channel.estimatedMonthlyRevenue * 12; + } + + if (channel.totalViews !== undefined) { + const lifetimeRevenue = estimateRevenueFromViews(channel.totalViews, channel.topicCategories ?? []); + if (!channel.channelCreatedAt) { + return lifetimeRevenue; + } + + const ageDays = Math.max(30, (Date.now() - channel.channelCreatedAt) / (1000 * 60 * 60 * 24)); + return lifetimeRevenue * (DAYS_IN_YEAR / ageDays); + } + + return undefined; +} + +export function estimateTaxForPeriod(channel: { + estimatedAnnualRevenue?: number; + estimatedMonthlyRevenue?: number; + totalViews?: number; + topicCategories?: string[]; + channelCreatedAt?: number; +}, periodDays: number): number | undefined { + const annualRevenue = estimateAnnualRevenue(channel); + if (annualRevenue === undefined) return undefined; + + const periodRevenue = (annualRevenue * periodDays) / DAYS_IN_YEAR; + const periodBrackets = GHA_TAX_BRACKETS.map((bracket) => ({ + limit: Number.isFinite(bracket.limit) ? (bracket.limit * periodDays) / DAYS_IN_YEAR : Infinity, + rate: bracket.rate, + })); + + return calculateProgressiveTax(periodRevenue, periodBrackets); +}