Skip to content

Commit 89e3214

Browse files
committed
show all available months
1 parent 135a112 commit 89e3214

6 files changed

Lines changed: 638 additions & 99 deletions

File tree

src/components/MonthSelector.tsx

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import React from "react";
22
import { Calendar } from "lucide-react";
33
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
4-
import { MonthOption, getMonthCoverage, CopilotUsageData } from "@/lib/utils";
4+
import { getMonthCoverage, CopilotUsageData } from "@/lib/utils";
5+
import { MonthOption, MonthCoverage } from "@/types/month";
56

67
interface MonthSelectorProps {
78
availableMonths: MonthOption[];
@@ -11,9 +12,33 @@ interface MonthSelectorProps {
1112
data?: CopilotUsageData[] | null; // Add data prop to calculate coverage
1213
}
1314

15+
/**
16+
* Determines the appropriate badge text and style for a month
17+
*/
18+
function getMonthBadgeInfo(coverage: MonthCoverage, selectedMonth: string): { text: string; className: string } {
19+
if (coverage.isCurrentMonth) {
20+
return { text: 'Current Month', className: 'month-badge--current' };
21+
}
22+
23+
// Calculate how many months ago this was
24+
const now = new Date();
25+
const [year, month] = selectedMonth.split('-').map(Number);
26+
const monthDate = new Date(year, month - 1, 1);
27+
const currentDate = new Date(now.getFullYear(), now.getMonth(), 1);
28+
29+
const monthsDiff = (currentDate.getFullYear() - monthDate.getFullYear()) * 12 +
30+
(currentDate.getMonth() - monthDate.getMonth());
31+
32+
if (monthsDiff === 1) {
33+
return { text: 'Previous Month', className: 'month-badge--previous' };
34+
} else {
35+
return { text: `${Math.abs(monthsDiff)} months ago`, className: 'month-badge--historical' };
36+
}
37+
}
38+
1439
/**
1540
* Month selector component that displays available months in a dropdown
16-
* Shows current month and previous month options with day coverage info
41+
* Shows all available months with day coverage info and appropriate time badges
1742
*/
1843
export function MonthSelector({
1944
availableMonths,
@@ -23,7 +48,7 @@ export function MonthSelector({
2348
data = null
2449
}: MonthSelectorProps) {
2550
// Get month coverage info for the selected month
26-
const coverage = data && selectedMonth ? getMonthCoverage(data, selectedMonth) : null;
51+
const coverage: MonthCoverage | null = data && selectedMonth ? getMonthCoverage(data, selectedMonth) : null;
2752

2853
return (
2954
<div className="flex items-center gap-3">
@@ -54,11 +79,14 @@ export function MonthSelector({
5479
<span className="font-medium text-foreground">
5580
{coverage.daysWithData}/{coverage.totalDays}
5681
</span>
57-
<span
58-
className={`month-badge ${coverage.isCurrentMonth ? 'month-badge--current' : 'month-badge--previous'}`}
59-
>
60-
{coverage.isCurrentMonth ? 'Current Month' : 'Previous Month'}
61-
</span>
82+
{(() => {
83+
const badgeInfo = getMonthBadgeInfo(coverage, selectedMonth);
84+
return (
85+
<span className={`month-badge ${badgeInfo.className}`}>
86+
{badgeInfo.text}
87+
</span>
88+
);
89+
})()}
6290
</div>
6391
)}
6492
</div>

src/lib/month-utils.ts

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/**
2+
* Utility functions for month selection and date handling
3+
* Focused, tested utilities with proper error handling and validation
4+
*/
5+
6+
import { CopilotUsageData } from '@/lib/utils';
7+
import { MonthOption, MonthCoverage, MonthIdentifier } from '@/types/month';
8+
9+
/**
10+
* Validates if a month string is in the correct YYYY-MM format
11+
*/
12+
export function isValidMonthFormat(monthStr: string): boolean {
13+
const pattern = /^\d{4}-\d{2}$/;
14+
if (!pattern.test(monthStr)) return false;
15+
16+
const [year, month] = monthStr.split('-').map(Number);
17+
return year >= 2000 && year <= 3000 && month >= 1 && month <= 12;
18+
}
19+
20+
/**
21+
* Parses a month string into year and month numbers
22+
* @throws Error if format is invalid
23+
*/
24+
export function parseMonthString(monthStr: string): MonthIdentifier {
25+
if (!isValidMonthFormat(monthStr)) {
26+
throw new Error(`Invalid month format: ${monthStr}. Expected YYYY-MM format.`);
27+
}
28+
29+
const [year, month] = monthStr.split('-').map(Number);
30+
return { year, month };
31+
}
32+
33+
/**
34+
* Formats a MonthIdentifier into YYYY-MM string
35+
*/
36+
export function formatMonthString(identifier: MonthIdentifier): string {
37+
const { year, month } = identifier;
38+
return `${year}-${String(month).padStart(2, '0')}`;
39+
}
40+
41+
/**
42+
* Gets the current month identifier
43+
*/
44+
export function getCurrentMonth(): MonthIdentifier {
45+
const now = new Date();
46+
return {
47+
year: now.getFullYear(),
48+
month: now.getMonth() + 1 // JavaScript months are 0-indexed
49+
};
50+
}
51+
52+
/**
53+
* Gets the previous month identifier
54+
*/
55+
export function getPreviousMonth(from?: MonthIdentifier): MonthIdentifier {
56+
const base = from || getCurrentMonth();
57+
const { year, month } = base;
58+
59+
if (month === 1) {
60+
return { year: year - 1, month: 12 };
61+
}
62+
return { year, month: month - 1 };
63+
}
64+
65+
/**
66+
* Creates a human-readable label for a month
67+
*/
68+
export function createMonthLabel(identifier: MonthIdentifier): string {
69+
const { year, month } = identifier;
70+
const date = new Date(year, month - 1, 1);
71+
return date.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
72+
}
73+
74+
/**
75+
* Extracts unique month identifiers from usage data
76+
*/
77+
export function extractMonthsFromData(data: CopilotUsageData[]): MonthIdentifier[] {
78+
if (!data || data.length === 0) return [];
79+
80+
const monthsSet = new Set<string>();
81+
82+
for (const item of data) {
83+
if (!item.timestamp || !(item.timestamp instanceof Date)) continue;
84+
85+
const identifier: MonthIdentifier = {
86+
year: item.timestamp.getFullYear(),
87+
month: item.timestamp.getMonth() + 1
88+
};
89+
90+
monthsSet.add(formatMonthString(identifier));
91+
}
92+
93+
return Array.from(monthsSet)
94+
.sort()
95+
.reverse() // Most recent first
96+
.map(parseMonthString);
97+
}
98+
99+
/**
100+
* Gets available month options for the month selector
101+
* Returns all months that have data in the export
102+
*/
103+
export function getAvailableMonths(data: CopilotUsageData[]): MonthOption[] {
104+
if (!data || data.length === 0) return [];
105+
106+
const monthsWithData = extractMonthsFromData(data);
107+
const currentMonth = getCurrentMonth();
108+
const currentMonthStr = formatMonthString(currentMonth);
109+
110+
// Return all months with data (already sorted newest to oldest)
111+
return monthsWithData.map(identifier => ({
112+
value: formatMonthString(identifier),
113+
label: createMonthLabel(identifier),
114+
isCurrentMonth: formatMonthString(identifier) === currentMonthStr
115+
}));
116+
}
117+
118+
/**
119+
* Filters usage data by the specified month
120+
*/
121+
export function filterDataByMonth(data: CopilotUsageData[], selectedMonth: string): CopilotUsageData[] {
122+
if (!data || data.length === 0) return [];
123+
if (!selectedMonth) return data;
124+
125+
const monthIdentifier = parseMonthString(selectedMonth);
126+
const { year, month } = monthIdentifier;
127+
128+
return data.filter(item => {
129+
if (!item.timestamp || !(item.timestamp instanceof Date)) return false;
130+
131+
return item.timestamp.getFullYear() === year &&
132+
item.timestamp.getMonth() === month - 1;
133+
});
134+
}
135+
136+
/**
137+
* Calculates the number of days in a given month
138+
*/
139+
export function getDaysInMonth(identifier: MonthIdentifier): number {
140+
const { year, month } = identifier;
141+
return new Date(year, month, 0).getDate();
142+
}
143+
144+
/**
145+
* Gets unique days with data for a specific month
146+
*/
147+
export function getUniqueDaysWithData(data: CopilotUsageData[], monthIdentifier: MonthIdentifier): Set<number> {
148+
const { year, month } = monthIdentifier;
149+
const daysSet = new Set<number>();
150+
151+
for (const item of data) {
152+
if (!item.timestamp || !(item.timestamp instanceof Date)) continue;
153+
154+
if (item.timestamp.getFullYear() === year &&
155+
item.timestamp.getMonth() === month - 1) {
156+
daysSet.add(item.timestamp.getDate());
157+
}
158+
}
159+
160+
return daysSet;
161+
}
162+
163+
/**
164+
* Calculates month coverage information
165+
*/
166+
export function getMonthCoverage(data: CopilotUsageData[], selectedMonth: string): MonthCoverage {
167+
if (!data || data.length === 0 || !selectedMonth) {
168+
return { daysWithData: 0, totalDays: 0, isCurrentMonth: false };
169+
}
170+
171+
const monthIdentifier = parseMonthString(selectedMonth);
172+
const currentMonth = getCurrentMonth();
173+
const isCurrentMonth = formatMonthString(monthIdentifier) === formatMonthString(currentMonth);
174+
175+
const totalDays = getDaysInMonth(monthIdentifier);
176+
const uniqueDays = getUniqueDaysWithData(data, monthIdentifier);
177+
178+
return {
179+
daysWithData: uniqueDays.size,
180+
totalDays,
181+
isCurrentMonth
182+
};
183+
}

src/lib/utils.ts

Lines changed: 3 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -766,94 +766,6 @@ export function getProjectedUsersExceedingQuotaDetails(data: CopilotUsageData[],
766766
return projectedUsers.sort((a, b) => b.projectedMonthlyTotal - a.projectedMonthlyTotal);
767767
}
768768

769-
/**
770-
* Month selector interface for representing a month option
771-
*/
772-
export interface MonthOption {
773-
value: string; // YYYY-MM format
774-
label: string; // Display label like "September 2025"
775-
isCurrentMonth: boolean;
776-
}
777-
778-
/**
779-
* Extract available months from CSV data
780-
* Returns current month and previous month options
781-
*/
782-
export function getAvailableMonths(data: CopilotUsageData[]): MonthOption[] {
783-
if (!data.length) return [];
784-
785-
// Get all unique months from the data
786-
const monthsSet = new Set<string>();
787-
data.forEach(item => {
788-
const date = new Date(item.timestamp);
789-
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
790-
monthsSet.add(monthKey);
791-
});
792-
793-
const months = Array.from(monthsSet).sort().reverse(); // Most recent first
794-
const currentDate = new Date();
795-
const currentMonthKey = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}`;
796-
797-
// Convert to MonthOption objects with display labels
798-
return months.slice(0, 2).map(monthKey => {
799-
const [year, month] = monthKey.split('-');
800-
const date = new Date(parseInt(year), parseInt(month) - 1, 1);
801-
const label = date.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
802-
803-
return {
804-
value: monthKey,
805-
label,
806-
isCurrentMonth: monthKey === currentMonthKey
807-
};
808-
});
809-
}
810-
811-
/**
812-
* Filter Copilot usage data by selected month
813-
*/
814-
export function filterDataByMonth(data: CopilotUsageData[], selectedMonth: string): CopilotUsageData[] {
815-
if (!selectedMonth) return data;
816-
817-
const [year, month] = selectedMonth.split('-').map(Number);
818-
819-
return data.filter(item => {
820-
const itemDate = new Date(item.timestamp);
821-
return itemDate.getFullYear() === year && itemDate.getMonth() === month - 1;
822-
});
823-
}
824-
825-
/**
826-
* Get month coverage info - days with data vs total days in month
827-
*/
828-
export function getMonthCoverage(data: CopilotUsageData[], selectedMonth: string): { daysWithData: number; totalDays: number; isCurrentMonth: boolean } {
829-
if (!data.length || !selectedMonth) return { daysWithData: 0, totalDays: 0, isCurrentMonth: false };
830-
831-
const [year, month] = selectedMonth.split('-').map(Number);
832-
const currentDate = new Date();
833-
const currentMonthKey = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}`;
834-
const isCurrentMonth = selectedMonth === currentMonthKey;
835-
836-
// Get total days in the selected month
837-
const totalDays = new Date(year, month, 0).getDate();
838-
839-
// Get unique days with data in the selected month
840-
const daysWithData = new Set(
841-
data
842-
.filter(item => {
843-
const itemDate = new Date(item.timestamp);
844-
return itemDate.getFullYear() === year && itemDate.getMonth() === month - 1;
845-
})
846-
.map(item => new Date(item.timestamp).getDate())
847-
).size;
848-
849-
return { daysWithData, totalDays, isCurrentMonth };
850-
}
851-
852-
/**
853-
* Month selector interface for representing a month option
854-
*/
855-
export interface MonthOption {
856-
value: string; // YYYY-MM format
857-
label: string; // Display label like "September 2025"
858-
isCurrentMonth: boolean;
859-
}
769+
// Re-export month utilities for backward compatibility
770+
export { getAvailableMonths, filterDataByMonth, getMonthCoverage } from './month-utils';
771+
export type { MonthOption } from '../types/month';

src/styles/theme.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,13 @@
196196
border-color: #d1b3ff; /* --color-accent-3 */
197197
}
198198

199+
.month-badge--historical {
200+
background-color: #f3f4f6; /* Light gray background */
201+
color: #6b7280; /* Gray text */
202+
border-color: #d1d5db; /* Gray border */
203+
}
204+
205+
199206
/* Xebia action button styles */
200207
.xebia-action-button {
201208
display: inline-flex;

0 commit comments

Comments
 (0)