localgreenchain/components/analytics/FilterPanel.tsx
Claude 816c3b3f2e
Implement Agent 7: Advanced Analytics Dashboard
Add comprehensive analytics system with:
- Analytics data layer (aggregator, metrics, trends, cache)
- 6 API endpoints (overview, plants, transport, farms, sustainability, export)
- 6 chart components (LineChart, BarChart, PieChart, AreaChart, Gauge, Heatmap)
- 5 dashboard widgets (KPICard, TrendIndicator, DataTable, DateRangePicker, FilterPanel)
- 5 dashboard pages (overview, plants, transport, farms, sustainability)
- Export functionality (CSV, JSON)

Dependencies added: recharts, d3, date-fns

Also includes minor fixes:
- Fix EnvironmentalForm spread type error
- Fix AgentOrchestrator Map iteration issues
- Fix next.config.js image domains undefined error
- Add downlevelIteration to tsconfig
2025-11-23 04:02:07 +00:00

165 lines
5.6 KiB
TypeScript

/**
* Filter Panel Component
* Provides filtering options for analytics data
*/
import { useState } from 'react';
interface FilterOption {
value: string;
label: string;
}
interface FilterConfig {
key: string;
label: string;
type: 'select' | 'multiselect' | 'search';
options?: FilterOption[];
}
interface FilterPanelProps {
filters: FilterConfig[];
values: Record<string, any>;
onChange: (values: Record<string, any>) => void;
onReset?: () => void;
}
export default function FilterPanel({
filters,
values,
onChange,
onReset,
}: FilterPanelProps) {
const [isExpanded, setIsExpanded] = useState(false);
const handleChange = (key: string, value: any) => {
onChange({ ...values, [key]: value });
};
const handleMultiSelect = (key: string, value: string) => {
const current = values[key] || [];
const updated = current.includes(value)
? current.filter((v: string) => v !== value)
: [...current, value];
handleChange(key, updated);
};
const activeFilterCount = Object.values(values).filter(
(v) => v && (Array.isArray(v) ? v.length > 0 : true)
).length;
return (
<div className="bg-white rounded-lg shadow border border-gray-200">
{/* Header */}
<button
onClick={() => setIsExpanded(!isExpanded)}
className="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50"
>
<div className="flex items-center space-x-2">
<svg
className="w-5 h-5 text-gray-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
/>
</svg>
<span className="font-medium text-gray-700">Filters</span>
{activeFilterCount > 0 && (
<span className="px-2 py-0.5 bg-green-100 text-green-700 text-xs rounded-full">
{activeFilterCount} active
</span>
)}
</div>
<svg
className={`w-5 h-5 text-gray-400 transform transition-transform ${
isExpanded ? 'rotate-180' : ''
}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{/* Filter content */}
{isExpanded && (
<div className="px-4 py-4 border-t border-gray-200 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filters.map((filter) => (
<div key={filter.key}>
<label className="block text-sm font-medium text-gray-700 mb-1">
{filter.label}
</label>
{filter.type === 'select' && filter.options && (
<select
value={values[filter.key] || ''}
onChange={(e) => handleChange(filter.key, e.target.value || null)}
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
>
<option value="">All</option>
{filter.options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
)}
{filter.type === 'multiselect' && filter.options && (
<div className="flex flex-wrap gap-2">
{filter.options.map((opt) => (
<button
key={opt.value}
onClick={() => handleMultiSelect(filter.key, opt.value)}
className={`px-3 py-1 text-sm rounded-full border transition-colors ${
(values[filter.key] || []).includes(opt.value)
? 'bg-green-500 text-white border-green-500'
: 'bg-white text-gray-600 border-gray-200 hover:border-green-300'
}`}
>
{opt.label}
</button>
))}
</div>
)}
{filter.type === 'search' && (
<input
type="text"
value={values[filter.key] || ''}
onChange={(e) => handleChange(filter.key, e.target.value || null)}
placeholder={`Search ${filter.label.toLowerCase()}...`}
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
/>
)}
</div>
))}
</div>
{/* Actions */}
<div className="flex justify-end space-x-2 pt-2 border-t border-gray-100">
{onReset && (
<button
onClick={onReset}
className="px-4 py-2 text-sm text-gray-600 hover:text-gray-800"
>
Reset filters
</button>
)}
<button
onClick={() => setIsExpanded(false)}
className="px-4 py-2 text-sm bg-green-500 text-white rounded-lg hover:bg-green-600"
>
Apply
</button>
</div>
</div>
)}
</div>
);
}