Skip to content

feat: add CostTimeline component and integrate into the main navigation view#2

Merged
hardinxcore merged 2 commits into
mainfrom
staging
Apr 2, 2026
Merged

feat: add CostTimeline component and integrate into the main navigation view#2
hardinxcore merged 2 commits into
mainfrom
staging

Conversation

@hardinxcore
Copy link
Copy Markdown
Owner

This pull request introduces a new "Cost Timeline" view to the application. The main changes add navigation, UI elements, and logic to support the new timeline feature, integrating it alongside existing views like Dashboard, Azure, and Renewals.

New Feature: Cost Timeline View

  • Added the CostTimeline component and integrated it into the navigation and view logic, allowing users to switch to and display the new timeline view. [1] [2] [3]

UI and Navigation Updates

  • Added a new navigation button with a LineChart icon for accessing the Cost Timeline, styled consistently with other navigation buttons.
  • Updated navigation state and conditionals throughout the UI to include the timeline view, ensuring correct display and interaction for search, history, and dashboard elements. [1] [2] [3]

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new “Cost Timeline” analytics view to the billing area of the app, exposing month-by-month and day-by-day cost trends and integrating it into the existing dashboard navigation alongside Azure/NCE/Renewals.

Changes:

  • Introduces CostTimeline component with monthly stacked breakdown, drill-down daily chart, top customers, and a sortable/filterable records table.
  • Adds a new “Cost Timeline” navigation tab (with LineChart icon) and routes the main view rendering to the new component.
  • Updates header/search/history visibility logic to account for the new timeline view.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/components/CostTimeline.tsx New timeline view with aggregation + charts + drill-down UI for billing records.
src/App.tsx Adds timeline to navigation state and renders CostTimeline via the main dashboard tab strip.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +174 to +178
.map(([day, total]) => ({
day,
label: new Date(day).toLocaleDateString('en-GB', { day: '2-digit', month: 'short' }),
total,
}));
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dailyData builds labels from a day string in YYYY-MM-DD form and then uses new Date(day). Date-only ISO strings are parsed as UTC, which can shift the displayed day in non-UTC timezones (and cause off-by-one labels). Consider constructing the date in local time (e.g., split day into y/m/d and use new Date(y, m-1, d)), or keep the original Date object from parseDay/aggregation for labeling.

Suggested change
.map(([day, total]) => ({
day,
label: new Date(day).toLocaleDateString('en-GB', { day: '2-digit', month: 'short' }),
total,
}));
.map(([day, total]) => {
const [year, month, date] = day.split('-').map(Number);
const localDate = new Date(year, month - 1, date);
return {
day,
label: localDate.toLocaleDateString('en-GB', { day: '2-digit', month: 'short' }),
total,
};
});

Copilot uses AI. Check for mistakes.
Comment thread src/components/CostTimeline.tsx Outdated
Comment on lines +342 to +348
{CATEGORIES.filter(cat => selectedData[cat] > 0).map(cat => (
<div key={cat} style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', padding: '0.3rem 0.75rem', borderRadius: '20px', border: `1px solid ${CAT_COLORS[cat]}44`, background: `${CAT_COLORS[cat]}11`, fontSize: '0.8rem', color: 'var(--text-secondary)' }}>
<span style={{ width: 8, height: 8, borderRadius: '50%', background: CAT_COLORS[cat], display: 'inline-block' }} />
{cat}: <strong style={{ color: 'var(--text-primary)' }}>{fmt(selectedData[cat], currency)}</strong>
<span style={{ color: 'var(--text-tertiary)' }}>({((selectedData[cat] / selectedData.total) * 100).toFixed(0)}%)</span>
</div>
))}
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The category percentage uses (selectedData[cat] / selectedData.total) * 100 without guarding against selectedData.total === 0. With mixed positive/negative category totals, total can be 0 even when a category is > 0, resulting in Infinity%/NaN% in the UI. Add a zero check (and ideally decide how to handle negative totals) before computing the percentage.

Suggested change
{CATEGORIES.filter(cat => selectedData[cat] > 0).map(cat => (
<div key={cat} style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', padding: '0.3rem 0.75rem', borderRadius: '20px', border: `1px solid ${CAT_COLORS[cat]}44`, background: `${CAT_COLORS[cat]}11`, fontSize: '0.8rem', color: 'var(--text-secondary)' }}>
<span style={{ width: 8, height: 8, borderRadius: '50%', background: CAT_COLORS[cat], display: 'inline-block' }} />
{cat}: <strong style={{ color: 'var(--text-primary)' }}>{fmt(selectedData[cat], currency)}</strong>
<span style={{ color: 'var(--text-tertiary)' }}>({((selectedData[cat] / selectedData.total) * 100).toFixed(0)}%)</span>
</div>
))}
{CATEGORIES.filter(cat => selectedData[cat] > 0).map(cat => {
const percentage = selectedData.total === 0
? 0
: (selectedData[cat] / selectedData.total) * 100;
return (
<div key={cat} style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', padding: '0.3rem 0.75rem', borderRadius: '20px', border: `1px solid ${CAT_COLORS[cat]}44`, background: `${CAT_COLORS[cat]}11`, fontSize: '0.8rem', color: 'var(--text-secondary)' }}>
<span style={{ width: 8, height: 8, borderRadius: '50%', background: CAT_COLORS[cat], display: 'inline-block' }} />
{cat}: <strong style={{ color: 'var(--text-primary)' }}>{fmt(selectedData[cat], currency)}</strong>
<span style={{ color: 'var(--text-tertiary)' }}>({percentage.toFixed(0)}%)</span>
</div>
);
})}

Copilot uses AI. Check for mistakes.
Comment thread src/components/CostTimeline.tsx Outdated
Comment on lines +380 to +390
{topCustomers.map((c, i) => {
const pct = selectedData.total > 0 ? (c.value / selectedData.total) * 100 : 0;
return (
<div key={c.name}>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.82rem', marginBottom: '0.2rem' }}>
<span style={{ color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '60%' }}>{c.name}</span>
<span style={{ color: 'var(--text-secondary)', flexShrink: 0 }}>{fmt(c.value, currency)}</span>
</div>
<div style={{ height: 4, borderRadius: 2, background: 'var(--bg-tertiary)', overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${pct}%`, background: `hsl(${(i * 47) % 360}, 65%, 55%)`, borderRadius: 2, transition: 'width 0.4s ease' }} />
</div>
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Top customer bar width is computed directly from pct = (c.value / selectedData.total) * 100 and used as a CSS percentage. If selectedData.total is reduced by credits/refunds, pct can be > 100 (or negative), causing bars to overflow/underflow the container. Clamping the computed percentage (and/or basing it on sum of positive values) will keep the visualization stable.

Copilot uses AI. Check for mistakes.
@hardinxcore hardinxcore merged commit 7521955 into main Apr 2, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants