Skip to content

Commit c3788f4

Browse files
committed
month selection - first implementation
1 parent da7f7c1 commit c3788f4

4 files changed

Lines changed: 342 additions & 53 deletions

File tree

src/App.tsx

Lines changed: 108 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
PowerUserDailyBreakdown,
2525
ExceededRequestDetail,
2626
ProjectedUserData,
27+
MonthOption,
2728
aggregateDataByDay,
2829
parseCSV,
2930
getModelUsageSummary,
@@ -40,12 +41,18 @@ import {
4041
getTotalRequestsForUsersExceedingQuota,
4142
getProjectedUsersExceedingQuota,
4243
getProjectedUsersExceedingQuotaDetails,
44+
getAvailableMonths,
45+
filterDataByMonth,
4346
EXCESS_REQUEST_COST
4447
} from "@/lib/utils";
48+
import { MonthSelector } from "@/components/MonthSelector";
4549

4650
function App() {
4751
const [showPrivacyBanner, setShowPrivacyBanner] = useState(true);
4852
const [data, setData] = useState<CopilotUsageData[] | null>(null);
53+
const [rawData, setRawData] = useState<CopilotUsageData[] | null>(null); // Store original unfiltered data
54+
const [availableMonths, setAvailableMonths] = useState<MonthOption[]>([]);
55+
const [selectedMonth, setSelectedMonth] = useState<string>('');
4956
const [aggregatedData, setAggregatedData] = useState<AggregatedData[]>([]);
5057
const [uniqueModels, setUniqueModels] = useState<string[]>([]);
5158
const [modelSummary, setModelSummary] = useState<ModelUsageSummary[]>([]);
@@ -81,6 +88,67 @@ function App() {
8188
setProjectedUsersData(projectedDetails);
8289
}
8390
}, [selectedPlan, data]);
91+
92+
// Reprocess data when month selection changes
93+
useEffect(() => {
94+
if (rawData && selectedMonth) {
95+
processDataForMonth(rawData, selectedMonth);
96+
}
97+
}, [selectedMonth, rawData, selectedPlan]);
98+
99+
/**
100+
* Process data for a specific month and update all derived state
101+
* This function aggregates and processes data for the selected month only
102+
*/
103+
const processDataForMonth = useCallback((rawData: CopilotUsageData[], month: string) => {
104+
// Filter data by selected month
105+
const filteredData = filterDataByMonth(rawData, month);
106+
setData(filteredData);
107+
108+
// Get unique models from filtered data
109+
const models = Array.from(new Set(filteredData.map(item => item.model)));
110+
setUniqueModels(models);
111+
112+
// Aggregate data by day and model for the selected month
113+
const aggregated = aggregateDataByDay(filteredData);
114+
setAggregatedData(aggregated);
115+
116+
// Get model usage summary for the selected month
117+
const summary = getModelUsageSummary(filteredData);
118+
setModelSummary(summary);
119+
120+
// Get daily model data for bar chart for the selected month
121+
const dailyData = getDailyModelData(filteredData);
122+
setDailyModelData(dailyData);
123+
124+
// Get power users data for the selected month
125+
const powerUsers = getPowerUsers(filteredData);
126+
setPowerUserSummary(powerUsers);
127+
128+
// Get power user daily breakdown for the stacked bar chart for the selected month
129+
const powerUserNames = powerUsers.powerUsers.map(user => user.user);
130+
const powerUserBreakdown = getPowerUserDailyBreakdown(filteredData, powerUserNames);
131+
setPowerUserDailyBreakdown(powerUserBreakdown);
132+
133+
// Get count of users exceeding quota for top bar display for the selected month
134+
const exceedingUsersCount = getUniqueUsersExceedingQuota(filteredData, selectedPlan);
135+
setUsersExceedingQuota(exceedingUsersCount);
136+
137+
// Get projected count of users who will exceed quota by month-end for the selected month
138+
const projectedExceedingUsersCount = getProjectedUsersExceedingQuota(filteredData, selectedPlan);
139+
setProjectedUsersExceedingQuota(projectedExceedingUsersCount);
140+
141+
// Get projected users details for the selected month
142+
const projectedDetails = getProjectedUsersExceedingQuotaDetails(filteredData, selectedPlan);
143+
setProjectedUsersData(projectedDetails);
144+
145+
// Get the last date available in the filtered CSV for the selected month
146+
const lastDate = getLastDateFromData(filteredData);
147+
setLastDateAvailable(lastDate);
148+
149+
// Reset selected power user when month changes
150+
setSelectedPowerUser(null);
151+
}, [selectedPlan]);
84152

85153
const handlePowerUserSelect = useCallback((userName: string | null) => {
86154
setSelectedPowerUser(userName);
@@ -136,54 +204,25 @@ function App() {
136204
}
137205

138206
const parsedData = parseCSV(csvContent);
139-
setData(parsedData);
140-
141-
// Get unique models
142-
const models = Array.from(new Set(parsedData.map(item => item.model)));
143-
setUniqueModels(models);
144-
145-
// Aggregate data by day and model
146-
const aggregated = aggregateDataByDay(parsedData);
147-
setAggregatedData(aggregated);
148-
149-
// Get model usage summary
150-
const summary = getModelUsageSummary(parsedData);
151-
setModelSummary(summary);
152-
153-
// Get daily model data for bar chart
154-
const dailyData = getDailyModelData(parsedData);
155-
setDailyModelData(dailyData);
156-
157-
// Get power users data
158-
const powerUsers = getPowerUsers(parsedData);
159-
setPowerUserSummary(powerUsers);
160-
161-
// Get power user daily breakdown for the stacked bar chart
162-
const powerUserNames = powerUsers.powerUsers.map(user => user.user);
163-
const powerUserBreakdown = getPowerUserDailyBreakdown(parsedData, powerUserNames);
164-
setPowerUserDailyBreakdown(powerUserBreakdown);
165207

166-
// Get count of users exceeding quota for top bar display
167-
const exceedingUsersCount = getUniqueUsersExceedingQuota(parsedData, selectedPlan);
168-
setUsersExceedingQuota(exceedingUsersCount);
208+
// Store raw unfiltered data
209+
setRawData(parsedData);
169210

170-
// Get projected count of users who will exceed quota by month-end
171-
const projectedExceedingUsersCount = getProjectedUsersExceedingQuota(parsedData, selectedPlan);
172-
setProjectedUsersExceedingQuota(projectedExceedingUsersCount);
211+
// Extract available months (current and previous month)
212+
const months = getAvailableMonths(parsedData);
213+
setAvailableMonths(months);
173214

174-
// Get projected users details
175-
const projectedDetails = getProjectedUsersExceedingQuotaDetails(parsedData, selectedPlan);
176-
setProjectedUsersData(projectedDetails);
215+
// Auto-select current month if available, otherwise select the first (most recent) month
216+
const defaultMonth = months.find(m => m.isCurrentMonth)?.value || months[0]?.value || '';
217+
setSelectedMonth(defaultMonth);
177218

178-
// Get the last date available in the CSV
179-
const lastDate = getLastDateFromData(parsedData);
180-
setLastDateAvailable(lastDate);
181-
182-
// Reset selected power user when new data is loaded
183-
setSelectedPowerUser(null);
219+
// Process data for the default selected month
220+
if (defaultMonth) {
221+
processDataForMonth(parsedData, defaultMonth);
222+
}
184223

185224
setIsProcessing(false);
186-
toast.success(`Loaded ${parsedData.length} records successfully`);
225+
toast.success(`Loaded ${parsedData.length.toLocaleString()} records successfully`);
187226
} catch (error) {
188227
// Provide user-friendly error messages
189228
let errorMessage = "Failed to parse CSV file. Please check the format.";
@@ -217,6 +256,9 @@ function App() {
217256
setIsProcessing(false);
218257
toast.error(errorMessage);
219258
setData(null);
259+
setRawData(null);
260+
setAvailableMonths([]);
261+
setSelectedMonth('');
220262
setAggregatedData([]);
221263
setModelSummary([]);
222264
setDailyModelData([]);
@@ -229,7 +271,7 @@ function App() {
229271
};
230272

231273
reader.readAsText(file);
232-
}, []);
274+
}, [processDataForMonth]);
233275

234276
const handleFileUpload = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
235277
if (isProcessing) return;
@@ -557,7 +599,20 @@ function App() {
557599
{data && data.length > 0 && (
558600
<div className="space-y-8">
559601
<div>
560-
<h2 className="text-2xl font-semibold mb-2">Usage Statistics</h2>
602+
<div className="flex items-center justify-between mb-4">
603+
<div>
604+
<h2 className="text-2xl font-semibold mb-2">Usage Statistics</h2>
605+
</div>
606+
<div className="flex items-center gap-4">
607+
<MonthSelector
608+
availableMonths={availableMonths}
609+
selectedMonth={selectedMonth}
610+
onMonthChange={setSelectedMonth}
611+
disabled={isProcessing}
612+
data={rawData}
613+
/>
614+
</div>
615+
</div>
561616
<Separator className="mb-4" />
562617
<div className="mb-4">
563618
<Card>
@@ -588,37 +643,37 @@ function App() {
588643
</span>
589644
</div>
590645
<div
591-
className="inline-flex items-center gap-1.5 cursor-pointer hover:bg-orange-50 dark:hover:bg-orange-900/20 transition-all duration-200 rounded-md px-2 py-1 group border border-orange-200 hover:border-orange-300 hover:shadow-sm bg-orange-25 dark:bg-orange-950/10"
646+
className="xebia-action-button"
592647
onClick={() => setShowProjectedUsersDialog(true)}
593648
title="Click to see the users projected to exceed quota"
594649
>
595650
<span className="text-sm text-muted-foreground">Projected to Exceed by Month-End:</span>
596-
<span className="text-lg font-bold text-orange-600 group-hover:text-orange-700 transition-colors">
651+
<span className="value">
597652
{projectedUsersExceedingQuota.toLocaleString()}
598653
</span>
599-
<ChevronRight className="h-3 w-3 text-orange-600 opacity-60 group-hover:opacity-100 group-hover:translate-x-0.5 transition-all duration-200" />
654+
<ChevronRight className="icon" />
600655
</div>
601656
<div
602-
className="inline-flex items-center gap-1.5 cursor-pointer hover:bg-orange-50 dark:hover:bg-orange-900/20 transition-all duration-200 rounded-md px-2 py-1 group border border-orange-200 hover:border-orange-300 hover:shadow-sm bg-orange-25 dark:bg-orange-950/10"
657+
className="xebia-action-button"
603658
onClick={() => setShowPotentialCostDetails(true)}
604659
title="Click to see cost breakdown"
605660
>
606661
<span className="text-sm text-muted-foreground">Potential Cost:</span>
607-
<span className="text-lg font-bold text-orange-600 group-hover:text-orange-700 transition-colors">
662+
<span className="value">
608663
${(data.reduce((sum, item) => sum + item.requestsUsed, 0) * EXCESS_REQUEST_COST).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}
609664
</span>
610-
<ChevronRight className="h-3 w-3 text-orange-600 opacity-60 group-hover:opacity-100 group-hover:translate-x-0.5 transition-all duration-200" />
665+
<ChevronRight className="icon" />
611666
</div>
612667
{powerUserSummary && (
613668
<Sheet>
614669
<SheetTrigger asChild>
615670
<div
616-
className="inline-flex items-center gap-1.5 cursor-pointer hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-all duration-200 rounded-md px-2 py-1 group border border-blue-200 hover:border-blue-300 hover:shadow-sm bg-blue-25 dark:bg-blue-950/10"
671+
className="xebia-action-button"
617672
title="Click to view power users analysis"
618673
>
619674
<span className="text-sm text-muted-foreground">Power Users:</span>
620-
<span className="text-lg font-bold text-blue-600 group-hover:text-blue-700 transition-colors">{powerUserSummary.totalPowerUsers}</span>
621-
<ChevronRight className="h-3 w-3 text-blue-600 opacity-60 group-hover:opacity-100 group-hover:translate-x-0.5 transition-all duration-200" />
675+
<span className="value">{powerUserSummary.totalPowerUsers}</span>
676+
<ChevronRight className="icon" />
622677
</div>
623678
</SheetTrigger>
624679
<SheetContent side="bottom" className="h-[90vh] max-w-[90%] mx-auto overflow-y-auto">

src/components/MonthSelector.tsx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import React from "react";
2+
import { Calendar } from "lucide-react";
3+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
4+
import { MonthOption, getMonthCoverage, CopilotUsageData } from "@/lib/utils";
5+
6+
interface MonthSelectorProps {
7+
availableMonths: MonthOption[];
8+
selectedMonth: string;
9+
onMonthChange: (month: string) => void;
10+
disabled?: boolean;
11+
data?: CopilotUsageData[] | null; // Add data prop to calculate coverage
12+
}
13+
14+
/**
15+
* Month selector component that displays available months in a dropdown
16+
* Shows current month and previous month options with day coverage info
17+
*/
18+
export function MonthSelector({
19+
availableMonths,
20+
selectedMonth,
21+
onMonthChange,
22+
disabled = false,
23+
data = null
24+
}: MonthSelectorProps) {
25+
// Get month coverage info for the selected month
26+
const coverage = data && selectedMonth ? getMonthCoverage(data, selectedMonth) : null;
27+
28+
return (
29+
<div className="flex items-center gap-3">
30+
<Calendar className="h-4 w-4 text-muted-foreground" />
31+
<Select
32+
value={selectedMonth}
33+
onValueChange={onMonthChange}
34+
disabled={disabled || availableMonths.length === 0}
35+
>
36+
<SelectTrigger className="w-[200px]">
37+
<SelectValue placeholder="Select month" />
38+
</SelectTrigger>
39+
<SelectContent>
40+
{availableMonths.map((month) => (
41+
<SelectItem key={month.value} value={month.value}>
42+
<div className="flex items-center gap-2">
43+
<span>{month.label}</span>
44+
</div>
45+
</SelectItem>
46+
))}
47+
</SelectContent>
48+
</Select>
49+
50+
{/* Show day coverage info for selected month */}
51+
{coverage && (
52+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
53+
<span>Days with data:</span>
54+
<span className="font-medium text-foreground">
55+
{coverage.daysWithData}/{coverage.totalDays}
56+
</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>
62+
</div>
63+
)}
64+
</div>
65+
);
66+
}

0 commit comments

Comments
 (0)