Enhance dashboard with selectable periodic tax estimation#13
Conversation
Agent-Logs-Url: https://github.com/lord-shola/graitld/sessions/d5511975-a155-461c-86a5-e976cf0a619b Co-authored-by: lord-shola <154298518+lord-shola@users.noreply.github.com>
Agent-Logs-Url: https://github.com/lord-shola/graitld/sessions/d5511975-a155-461c-86a5-e976cf0a619b Co-authored-by: lord-shola <154298518+lord-shola@users.noreply.github.com>
Agent-Logs-Url: https://github.com/lord-shola/graitld/sessions/d5511975-a155-461c-86a5-e976cf0a619b Co-authored-by: lord-shola <154298518+lord-shola@users.noreply.github.com>
…rkflow Add selectable periodic tax estimation in dashboard views
|
@lord-shola is attempting to deploy a commit to the mhaadi Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughThe pull request introduces tax-period selection controls to the main dashboard and influencers page, allowing users to project tax estimates across different time periods (30, 60, 90, or 365 days). A new utility module implements Ghana progressive tax bracket calculations and revenue estimation logic to compute period-specific tax projections. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/app/`(dashboard)/page.tsx:
- Around line 85-100: The page is fetching full channel summaries via
useQuery(api.influencers.getChannels) just to compute totalTaxForPeriod,
exposing PII and blocking rendering; replace that client-side aggregation with a
server-side Convex query that returns only the period tax total (or a minimal
revenue-only projection). Add/use a new API hook (e.g.
api.analytics.getTotalTaxForPeriod or similar) that accepts taxPeriodDays,
remove the useQuery call to api.influencers.getChannels and stop using channels
in totalTaxForPeriod and estimateTaxForPeriod on the client; instead call the
new server-side query and use its numeric result directly wherever
totalTaxForPeriod is referenced so the client no longer downloads channel
summaries or uses estimateTaxForPeriod for aggregation.
- Around line 141-164: The period selector (Select using
value={String(taxPeriodDays)} and setter setTaxPeriodDays) currently sits by the
chart legend but only updates the metric card while the chart still renders
revenueData.tax unchanged; either move the Select and TAX_PERIOD_OPTIONS UI next
to the metric card it controls or make the chart consume taxPeriodDays so its
series are filtered/derived from revenueData.tax by period (update the chart
rendering logic that currently reads revenueData.tax to compute series using
taxPeriodDays). Ensure the Select remains wired to
taxPeriodDays/setTaxPeriodDays and update labels/legend to reflect whether the
chart is periodized.
In `@src/lib/tax-period-estimate.ts`:
- Around line 58-65: The code currently treats lifetime revenue from
estimateRevenueFromViews(channel.totalViews, channel.topicCategories ?? []) as
annualized when channel.channelCreatedAt is missing; change the logic so that if
channel.totalViews is present but channel.channelCreatedAt is undefined you
return undefined (or otherwise mark the value as lifetime-derived) instead of
computing lifetimeRevenue * (DAYS_IN_YEAR / ageDays); update the block around
the checks for channel.totalViews and channel.channelCreatedAt (referencing
estimateRevenueFromViews, channel.totalViews, channel.channelCreatedAt, and
DAYS_IN_YEAR) to only annualize when channelCreatedAt is available and otherwise
return undefined.
- Around line 71-87: The exported function estimateTaxForPeriod should guard
against invalid periodDays (NaN, Infinity, zero or negative) to avoid producing
NaN/meaningless taxes; update estimateTaxForPeriod to validate that
Number.isFinite(periodDays) && periodDays > 0 and return undefined for invalid
input before using DAYS_IN_YEAR, computing periodRevenue, mapping
GHA_TAX_BRACKETS, or calling calculateProgressiveTax so callers get a safe
undefined instead of broken results.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 44ac4c89-61f6-4065-a57e-04095875ccaf
📒 Files selected for processing (3)
src/app/(dashboard)/influencers/page.tsxsrc/app/(dashboard)/page.tsxsrc/lib/tax-period-estimate.ts
| 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], | ||
| ); |
There was a problem hiding this comment.
Aggregate the tax total server-side instead of fetching every channel.
This page now downloads full channel summaries just to compute totalTaxForPeriod, and it blocks the entire dashboard until that query resolves. Per convex/channelData.ts, those summaries include fields like email, phone, taxIdNumber, and notes, so this also broadens client-side PII exposure for an aggregate metric. Prefer a Convex query that returns only the period tax total, or at least a minimal revenue-only projection.
♻️ Directional refactor
- const channels = useQuery(api.influencers.getChannels, {});
+ const totalTaxForPeriod = useQuery(api.influencers.getTaxEstimateForPeriod, {
+ periodDays: taxPeriodDays,
+ });
...
- const totalTaxForPeriod = useMemo(
- () =>
- (channels ?? []).reduce(
- (sum, channel) => sum + (estimateTaxForPeriod(channel, taxPeriodDays) ?? 0),
- 0,
- ),
- [channels, taxPeriodDays],
- );
...
- if (stats === undefined || channels === undefined) {
+ if (stats === undefined || totalTaxForPeriod === undefined) {
return <DashboardSkeleton />;
}Also applies to: 102-104
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/`(dashboard)/page.tsx around lines 85 - 100, The page is fetching
full channel summaries via useQuery(api.influencers.getChannels) just to compute
totalTaxForPeriod, exposing PII and blocking rendering; replace that client-side
aggregation with a server-side Convex query that returns only the period tax
total (or a minimal revenue-only projection). Add/use a new API hook (e.g.
api.analytics.getTotalTaxForPeriod or similar) that accepts taxPeriodDays,
remove the useQuery call to api.influencers.getChannels and stop using channels
in totalTaxForPeriod and estimateTaxForPeriod on the client; instead call the
new server-side query and use its numeric result directly wherever
totalTaxForPeriod is referenced so the client no longer downloads channel
summaries or uses estimateTaxForPeriod for aggregation.
| <div className='flex flex-wrap items-center gap-2'> | ||
| <span className='flex items-center gap-1.5 text-[10px] font-medium tracking-wider text-muted-foreground uppercase'> | ||
| <span className='h-2 w-2 rounded-full bg-[oklch(0.6_0.18_250)]' /> Revenue inputs | ||
| </span> | ||
| <span className='flex items-center gap-1.5 text-[10px] font-medium tracking-wider text-muted-foreground uppercase'> | ||
| <span className='h-2 w-2 rounded-full bg-[oklch(0.65_0.18_150)]' /> Tax estimates | ||
| </span> | ||
| <Select | ||
| value={String(taxPeriodDays)} | ||
| onValueChange={(value) => { | ||
| setTaxPeriodDays(Number(value)); | ||
| }} | ||
| > | ||
| <SelectTrigger className='h-8 w-[110px] text-[10px] tracking-wider uppercase'> | ||
| <SelectValue placeholder='Tax period' /> | ||
| </SelectTrigger> | ||
| <SelectContent> | ||
| {TAX_PERIOD_OPTIONS.map((option) => ( | ||
| <SelectItem key={option.days} value={String(option.days)}> | ||
| {option.label} | ||
| </SelectItem> | ||
| ))} | ||
| </SelectContent> | ||
| </Select> |
There was a problem hiding this comment.
Keep the period selector scoped to data it actually changes.
The selector sits in the chart header next to the “Tax estimates” legend, but the chart still renders revenueData.tax unchanged. Selecting “90 days” changes the metric card, not the chart, which makes the chart appear period-aware when it is not. Either move the selector next to the metric it controls or derive/label a periodized chart series.
Also applies to: 170-232
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/`(dashboard)/page.tsx around lines 141 - 164, The period selector
(Select using value={String(taxPeriodDays)} and setter setTaxPeriodDays)
currently sits by the chart legend but only updates the metric card while the
chart still renders revenueData.tax unchanged; either move the Select and
TAX_PERIOD_OPTIONS UI next to the metric card it controls or make the chart
consume taxPeriodDays so its series are filtered/derived from revenueData.tax by
period (update the chart rendering logic that currently reads revenueData.tax to
compute series using taxPeriodDays). Ensure the Select remains wired to
taxPeriodDays/setTaxPeriodDays and update labels/legend to reflect whether the
chart is periodized.
| 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); |
There was a problem hiding this comment.
Don’t annualize lifetime view revenue without an age signal.
When totalViews exists but channelCreatedAt is missing, Line 61 treats lifetime estimated revenue as annual revenue. That makes the downstream “Projected for 30/60/90 days” tax label look precise even though the period basis is unknown. Prefer returning undefined until an age/source period is available, or label this path explicitly as lifetime-derived.
🐛 Proposed fix
if (channel.totalViews !== undefined) {
const lifetimeRevenue = estimateRevenueFromViews(channel.totalViews, channel.topicCategories ?? []);
- if (!channel.channelCreatedAt) {
- return lifetimeRevenue;
+ if (channel.channelCreatedAt === undefined) {
+ return undefined;
}
- const ageDays = Math.max(30, (Date.now() - channel.channelCreatedAt) / (1000 * 60 * 60 * 24));
+ const elapsedMs = Date.now() - channel.channelCreatedAt;
+ if (elapsedMs <= 0) {
+ return undefined;
+ }
+
+ const ageDays = Math.max(30, elapsedMs / (1000 * 60 * 60 * 24));
return lifetimeRevenue * (DAYS_IN_YEAR / ageDays);
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/tax-period-estimate.ts` around lines 58 - 65, The code currently
treats lifetime revenue from estimateRevenueFromViews(channel.totalViews,
channel.topicCategories ?? []) as annualized when channel.channelCreatedAt is
missing; change the logic so that if channel.totalViews is present but
channel.channelCreatedAt is undefined you return undefined (or otherwise mark
the value as lifetime-derived) instead of computing lifetimeRevenue *
(DAYS_IN_YEAR / ageDays); update the block around the checks for
channel.totalViews and channel.channelCreatedAt (referencing
estimateRevenueFromViews, channel.totalViews, channel.channelCreatedAt, and
DAYS_IN_YEAR) to only annualize when channelCreatedAt is available and otherwise
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); |
There was a problem hiding this comment.
Guard the exported estimator against invalid periods.
estimateTaxForPeriod accepts any number; NaN, Infinity, zero, or negative periods can return NaN or meaningless tax values. Since this is exported from the utility module, add a runtime guard even if current UI values come from TAX_PERIOD_OPTIONS.
🛡️ Proposed fix
export function estimateTaxForPeriod(channel: {
estimatedAnnualRevenue?: number;
estimatedMonthlyRevenue?: number;
totalViews?: number;
topicCategories?: string[];
channelCreatedAt?: number;
}, periodDays: number): number | undefined {
+ if (!Number.isFinite(periodDays) || periodDays <= 0) {
+ return undefined;
+ }
+
const annualRevenue = estimateAnnualRevenue(channel);
if (annualRevenue === undefined) return undefined;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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); | |
| export function estimateTaxForPeriod(channel: { | |
| estimatedAnnualRevenue?: number; | |
| estimatedMonthlyRevenue?: number; | |
| totalViews?: number; | |
| topicCategories?: string[]; | |
| channelCreatedAt?: number; | |
| }, periodDays: number): number | undefined { | |
| if (!Number.isFinite(periodDays) || periodDays <= 0) { | |
| return 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); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/tax-period-estimate.ts` around lines 71 - 87, The exported function
estimateTaxForPeriod should guard against invalid periodDays (NaN, Infinity,
zero or negative) to avoid producing NaN/meaningless taxes; update
estimateTaxForPeriod to validate that Number.isFinite(periodDays) && periodDays
> 0 and return undefined for invalid input before using DAYS_IN_YEAR, computing
periodRevenue, mapping GHA_TAX_BRACKETS, or calling calculateProgressiveTax so
callers get a safe undefined instead of broken results.
This pull request adds support for selecting a tax estimation period (e.g., 30, 60, 90 days, or 1 year) on both the Influencers dashboard and the main overview dashboard. Tax estimates are now dynamically calculated based on the selected period using a new tax estimation utility, providing more flexible and accurate projections. The UI has been updated to include a period selector and to display the projected period in tax estimate labels.
Tax estimation logic and utilities:
src/lib/tax-period-estimate.tsthat provides functions and constants for estimating tax over a selectable period using Ghana Revenue Authority tax brackets and progressive rates. This includesestimateTaxForPeriod,TAX_PERIOD_OPTIONS, andDEFAULT_TAX_PERIOD_DAYS.Influencers dashboard updates (
src/app/(dashboard)/influencers/page.tsx):Selectcomponent) allowing users to choose the tax period. The table now displays tax estimates calculated for the selected period usingestimateTaxForPeriod, and labels are updated to indicate the projection period. [1] [2] [3] [4] [5] [6] [7]Main dashboard updates (
src/app/(dashboard)/page.tsx):Summary by CodeRabbit