Skip to content

Commit 40cfd2d

Browse files
authored
Add spending insights page with filtering and summary features. (#269)
Introduced a new "Spending Insights" page with filtering and summary capabilities for insights and patterns. Added supporting components, utility functions, and API integrations to fetch and display the insights and patterns data dynamically.
1 parent 879767b commit 40cfd2d

File tree

12 files changed

+772
-38
lines changed

12 files changed

+772
-38
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import React from "react";
2+
import { Badge } from "primereact/badge";
3+
import { SpendingInsight } from "../../../types/types";
4+
import DateComponent from "../../../components/format/date.component";
5+
import { i10n } from "../../../config/prime-locale";
6+
import { getSeverityClass, getInsightTypeLabel } from "./utils";
7+
8+
interface InsightCardProps {
9+
insight: SpendingInsight;
10+
}
11+
12+
/**
13+
* Component for displaying a single insight card
14+
*/
15+
const InsightCard: React.FC<InsightCardProps> = ({ insight }) => {
16+
return (
17+
<div className="p-4 border rounded-lg shadow-sm hover:shadow-md transition-shadow duration-200">
18+
<div className="flex justify-between items-start mb-2">
19+
<Badge
20+
value={getInsightTypeLabel(insight.type)}
21+
className={`${getSeverityClass(insight.severity)} px-2 py-1 text-xs font-medium rounded-full`}
22+
/>
23+
<span className="text-sm text-gray-500">
24+
<DateComponent date={insight.detectedDate} />
25+
</span>
26+
</div>
27+
<h3 className="font-medium text-lg mb-2">{insight.category}</h3>
28+
<p className="text-gray-700 mb-3">{i10n(insight.message)}</p>
29+
<div className="mt-2 flex items-center">
30+
<span className="text-sm text-gray-500 mr-2">{i10n('insight.score')}:</span>
31+
<div className="w-full bg-gray-200 rounded-full h-2.5">
32+
<div
33+
className="bg-blue-600 h-2.5 rounded-full"
34+
style={{ width: `${Math.round(insight.score * 100)}%` }}
35+
></div>
36+
</div>
37+
<span className="ml-2 text-sm font-medium">{Math.round(insight.score * 100)}%</span>
38+
</div>
39+
40+
{/* Metadata section */}
41+
{insight.metadata && Object.keys(insight.metadata).length > 0 && (
42+
<div className="mt-3 pt-3 border-t border-gray-200">
43+
<h4 className="text-sm font-medium text-gray-700 mb-2">{i10n('insight.metadata') || 'Metadata'}</h4>
44+
<div className="grid grid-cols-2 gap-2">
45+
{Object.entries(insight.metadata).map(([key, value]) => (
46+
<div key={key} className="flex flex-col">
47+
<span className="text-xs text-gray-500">{i10n('insight.metadata.' + key)}</span>
48+
<span className="text-sm">{String(value)}</span>
49+
</div>
50+
))}
51+
</div>
52+
</div>
53+
)}
54+
</div>
55+
);
56+
};
57+
58+
export default InsightCard;
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import React from "react";
2+
import { InputText } from "primereact/inputtext";
3+
import { Dropdown } from "primereact/dropdown";
4+
import { Slider } from "primereact/slider";
5+
import { Button } from "primereact/button";
6+
import { getInsightTypeOptions, getSeverityOptions, i10nWithFallback } from "./utils";
7+
8+
interface InsightFiltersProps {
9+
searchTerm: string;
10+
setSearchTerm: (value: string) => void;
11+
insightTypeFilter: string | null;
12+
setInsightTypeFilter: (value: string | null) => void;
13+
insightSeverityFilter: string | null;
14+
setInsightSeverityFilter: (value: string | null) => void;
15+
insightScoreFilter: number;
16+
setInsightScoreFilter: (value: number) => void;
17+
resetFilters: () => void;
18+
}
19+
20+
/**
21+
* Component for filtering insights
22+
*/
23+
const InsightFilters: React.FC<InsightFiltersProps> = ({
24+
searchTerm,
25+
setSearchTerm,
26+
insightTypeFilter,
27+
setInsightTypeFilter,
28+
insightSeverityFilter,
29+
setInsightSeverityFilter,
30+
insightScoreFilter,
31+
setInsightScoreFilter,
32+
resetFilters
33+
}) => {
34+
return (
35+
<div className="mb-4 p-3 bg-gray-50 rounded-lg">
36+
<div className="flex flex-col md:flex-row gap-3 mb-3">
37+
<div className="flex-1">
38+
<span className="p-input-icon-left w-full">
39+
<i className="pi pi-search" />
40+
<InputText
41+
value={searchTerm}
42+
onChange={(e) => setSearchTerm(e.target.value)}
43+
placeholder={i10nWithFallback('search')}
44+
className="w-full"
45+
/>
46+
</span>
47+
</div>
48+
<div className="flex flex-1 gap-2">
49+
<Dropdown
50+
value={insightTypeFilter}
51+
options={getInsightTypeOptions()}
52+
onChange={(e) => setInsightTypeFilter(e.value)}
53+
placeholder={i10nWithFallback('filter.by.type')}
54+
className="w-full"
55+
/>
56+
<Dropdown
57+
value={insightSeverityFilter}
58+
options={getSeverityOptions()}
59+
onChange={(e) => setInsightSeverityFilter(e.value)}
60+
placeholder={i10nWithFallback('filter.by.severity')}
61+
className="w-full"
62+
/>
63+
</div>
64+
</div>
65+
<div className="flex flex-col md:flex-row items-center gap-3">
66+
<div className="flex-1">
67+
<label htmlFor="score-filter" className="block mb-1">
68+
{i10nWithFallback('filter.by.score')}: {insightScoreFilter}%
69+
</label>
70+
<Slider
71+
id="score-filter"
72+
value={insightScoreFilter}
73+
onChange={(e) => setInsightScoreFilter(e.value as number)}
74+
className="w-full"
75+
step={5}
76+
max={100}
77+
/>
78+
</div>
79+
<Button
80+
icon="pi pi-filter-slash"
81+
label={i10nWithFallback('reset.filters')}
82+
className="p-button-outlined"
83+
onClick={resetFilters}
84+
/>
85+
</div>
86+
</div>
87+
);
88+
};
89+
90+
export default InsightFilters;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import React from "react";
2+
import { Card } from "primereact/card";
3+
import { Panel } from "primereact/panel";
4+
import { ProgressBar } from "primereact/progressbar";
5+
import { i10n } from "../../../config/prime-locale";
6+
7+
/**
8+
* Component that displays a loading state for the insights page
9+
*/
10+
const LoadingComponent: React.FC = () => {
11+
return (
12+
<div className="mt-4">
13+
<Card title={i10n('page.reports.insights.title')}>
14+
<div className="flex items-center justify-center p-6">
15+
<div className="text-center">
16+
<ProgressBar mode="indeterminate" style={{ height: '6px' }} className="mb-3 w-64" />
17+
<p className="text-gray-600">{i10n('loading.data')}</p>
18+
</div>
19+
</div>
20+
</Card>
21+
22+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
23+
<div className="col-span-1">
24+
<Panel header={i10n('page.reports.insights.insights')}>
25+
<div className="animate-pulse">
26+
<div className="h-24 bg-gray-200 rounded-lg mb-3"></div>
27+
<div className="h-24 bg-gray-200 rounded-lg mb-3"></div>
28+
<div className="h-24 bg-gray-200 rounded-lg"></div>
29+
</div>
30+
</Panel>
31+
</div>
32+
<div className="col-span-1">
33+
<Panel header={i10n('page.reports.insights.patterns')}>
34+
<div className="animate-pulse">
35+
<div className="h-24 bg-gray-200 rounded-lg mb-3"></div>
36+
<div className="h-24 bg-gray-200 rounded-lg mb-3"></div>
37+
<div className="h-24 bg-gray-200 rounded-lg"></div>
38+
</div>
39+
</Panel>
40+
</div>
41+
</div>
42+
</div>
43+
);
44+
};
45+
46+
export default LoadingComponent;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import React from "react";
2+
import { Badge } from "primereact/badge";
3+
import { SpendingPattern } from "../../../types/types";
4+
import DateComponent from "../../../components/format/date.component";
5+
import { i10n } from "../../../config/prime-locale";
6+
import { getPatternTypeLabel } from "./utils";
7+
8+
interface PatternCardProps {
9+
pattern: SpendingPattern;
10+
}
11+
12+
/**
13+
* Component for displaying a single pattern card
14+
*/
15+
const PatternCard: React.FC<PatternCardProps> = ({ pattern }) => {
16+
return (
17+
<div className="p-4 border rounded-lg shadow-sm hover:shadow-md transition-shadow duration-200">
18+
<div className="flex justify-between items-start mb-2">
19+
<Badge
20+
value={getPatternTypeLabel(pattern.type)}
21+
className="bg-green-100 text-green-800 px-2 py-1 text-xs font-medium rounded-full"
22+
/>
23+
<span className="text-sm text-gray-500">
24+
<DateComponent date={pattern.detectedDate} />
25+
</span>
26+
</div>
27+
<h3 className="font-medium text-lg mb-2">{pattern.category}</h3>
28+
<div className="mt-2 flex items-center">
29+
<span className="text-sm text-gray-500 mr-2">{i10n('pattern.confidence')}:</span>
30+
<div className="w-full bg-gray-200 rounded-full h-2.5">
31+
<div
32+
className="bg-green-600 h-2.5 rounded-full"
33+
style={{ width: `${pattern.confidence * 100}%` }}
34+
></div>
35+
</div>
36+
<span className="ml-2 text-sm font-medium">{Math.round(pattern.confidence * 100)}%</span>
37+
</div>
38+
39+
{/* Metadata section */}
40+
{pattern.metadata && Object.keys(pattern.metadata).length > 0 && (
41+
<div className="mt-3 pt-3 border-t border-gray-200">
42+
<h4 className="text-sm font-medium text-gray-700 mb-2">{i10n('pattern.metadata') || 'Metadata'}</h4>
43+
<div className="grid grid-cols-2 gap-2">
44+
{Object.entries(pattern.metadata).map(([key, value]) => (
45+
<div key={key} className="flex flex-col">
46+
<span className="text-xs text-gray-500">{i10n('pattern.metadata.' + key)}</span>
47+
<span className="text-sm">{String(value)}</span>
48+
</div>
49+
))}
50+
</div>
51+
</div>
52+
)}
53+
</div>
54+
);
55+
};
56+
57+
export default PatternCard;
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import React from "react";
2+
import { InputText } from "primereact/inputtext";
3+
import { Dropdown } from "primereact/dropdown";
4+
import { Slider } from "primereact/slider";
5+
import { Button } from "primereact/button";
6+
import { getPatternTypeOptions, i10nWithFallback } from "./utils";
7+
8+
interface PatternFiltersProps {
9+
searchTerm: string;
10+
setSearchTerm: (value: string) => void;
11+
patternTypeFilter: string | null;
12+
setPatternTypeFilter: (value: string | null) => void;
13+
patternConfidenceFilter: number;
14+
setPatternConfidenceFilter: (value: number) => void;
15+
resetFilters: () => void;
16+
}
17+
18+
/**
19+
* Component for filtering patterns
20+
*/
21+
const PatternFilters: React.FC<PatternFiltersProps> = ({
22+
searchTerm,
23+
setSearchTerm,
24+
patternTypeFilter,
25+
setPatternTypeFilter,
26+
patternConfidenceFilter,
27+
setPatternConfidenceFilter,
28+
resetFilters
29+
}) => {
30+
return (
31+
<div className="mb-4 p-3 bg-gray-50 rounded-lg">
32+
<div className="flex flex-col md:flex-row gap-3 mb-3">
33+
<div className="flex-1">
34+
<span className="p-input-icon-left w-full">
35+
<i className="pi pi-search" />
36+
<InputText
37+
value={searchTerm}
38+
onChange={(e) => setSearchTerm(e.target.value)}
39+
placeholder={i10nWithFallback('search')}
40+
className="w-full"
41+
/>
42+
</span>
43+
</div>
44+
<div className="flex-1">
45+
<Dropdown
46+
value={patternTypeFilter}
47+
options={getPatternTypeOptions()}
48+
onChange={(e) => setPatternTypeFilter(e.value)}
49+
placeholder={i10nWithFallback('filter.by.type')}
50+
className="w-full"
51+
/>
52+
</div>
53+
</div>
54+
<div className="flex flex-col md:flex-row items-center gap-3">
55+
<div className="flex-1">
56+
<label htmlFor="confidence-filter" className="block mb-1">
57+
{i10nWithFallback('filter.by.confidence')}: {patternConfidenceFilter}%
58+
</label>
59+
<Slider
60+
id="confidence-filter"
61+
value={patternConfidenceFilter}
62+
onChange={(e) => setPatternConfidenceFilter(e.value as number)}
63+
className="w-full"
64+
step={5}
65+
max={100}
66+
/>
67+
</div>
68+
<Button
69+
icon="pi pi-filter-slash"
70+
label={i10nWithFallback('reset.filters')}
71+
className="p-button-outlined"
72+
onClick={resetFilters}
73+
/>
74+
</div>
75+
</div>
76+
);
77+
};
78+
79+
export default PatternFilters;

0 commit comments

Comments
 (0)