- adhesions : bloc complet avec events/states/model, dialogs paiement/rejet - admin : users bloc, user management list/detail pages - authentication : bloc + keycloak auth service + webview - backup : bloc complet, repository, models - contributions : bloc + widgets + export - dashboard : widgets connectés (activities, events, notifications, search) + charts + monitoring + shortcuts - epargne : repository, transactions, dialogs - events : bloc complet, pages (detail, connected, wrapper), models
349 lines
11 KiB
Dart
349 lines
11 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../../../../../shared/design_system/unionflow_design_system.dart';
|
|
import '../../../../members/presentation/pages/members_page_wrapper.dart';
|
|
import '../../../../events/presentation/pages/events_page_wrapper.dart';
|
|
import '../../../../contributions/presentation/pages/contributions_page_wrapper.dart';
|
|
import '../../../../reports/presentation/pages/reports_page_wrapper.dart';
|
|
import '../../../../settings/presentation/pages/system_settings_page.dart';
|
|
|
|
/// Widget de recherche rapide pour le dashboard
|
|
class DashboardSearchWidget extends StatefulWidget {
|
|
final Function(String)? onSearch;
|
|
final String? hintText;
|
|
final List<SearchSuggestion>? suggestions;
|
|
|
|
const DashboardSearchWidget({
|
|
super.key,
|
|
this.onSearch,
|
|
this.hintText,
|
|
this.suggestions,
|
|
});
|
|
|
|
@override
|
|
State<DashboardSearchWidget> createState() => _DashboardSearchWidgetState();
|
|
}
|
|
|
|
class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
|
|
with TickerProviderStateMixin {
|
|
final TextEditingController _searchController = TextEditingController();
|
|
final FocusNode _focusNode = FocusNode();
|
|
late AnimationController _animationController;
|
|
late Animation<double> _scaleAnimation;
|
|
bool _isExpanded = false;
|
|
List<SearchSuggestion> _filteredSuggestions = [];
|
|
List<SearchSuggestion>? _defaultSuggestions;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_setupAnimations();
|
|
_setupListeners();
|
|
_filteredSuggestions = widget.suggestions ?? [];
|
|
}
|
|
|
|
void _setupAnimations() {
|
|
_animationController = AnimationController(
|
|
duration: const Duration(milliseconds: 300),
|
|
vsync: this,
|
|
);
|
|
|
|
_scaleAnimation = Tween<double>(
|
|
begin: 1.0,
|
|
end: 1.05,
|
|
).animate(CurvedAnimation(
|
|
parent: _animationController,
|
|
curve: Curves.easeInOut,
|
|
));
|
|
}
|
|
|
|
void _setupListeners() {
|
|
_focusNode.addListener(() {
|
|
setState(() {
|
|
_isExpanded = _focusNode.hasFocus;
|
|
});
|
|
|
|
if (_focusNode.hasFocus) {
|
|
_animationController.forward();
|
|
} else {
|
|
_animationController.reverse();
|
|
}
|
|
});
|
|
|
|
_searchController.addListener(() {
|
|
_filterSuggestions(_searchController.text);
|
|
});
|
|
}
|
|
|
|
void _filterSuggestions(String query) {
|
|
if (query.isEmpty) {
|
|
setState(() {
|
|
_filteredSuggestions = widget.suggestions ?? _defaultSuggestions ?? [];
|
|
});
|
|
return;
|
|
}
|
|
|
|
final defaultList = widget.suggestions ?? _defaultSuggestions ?? [];
|
|
final filtered = defaultList
|
|
.where((suggestion) =>
|
|
suggestion.title.toLowerCase().contains(query.toLowerCase()) ||
|
|
suggestion.subtitle.toLowerCase().contains(query.toLowerCase()))
|
|
.toList();
|
|
|
|
setState(() {
|
|
_filteredSuggestions = filtered;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (_defaultSuggestions == null) {
|
|
_defaultSuggestions = _getDefaultSuggestions(context);
|
|
if (_filteredSuggestions.isEmpty && widget.suggestions == null) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (mounted) setState(() => _filteredSuggestions = _defaultSuggestions!);
|
|
});
|
|
}
|
|
}
|
|
return Column(
|
|
children: [
|
|
_buildSearchBar(),
|
|
if (_isExpanded && _filteredSuggestions.isNotEmpty) ...[
|
|
const SizedBox(height: 8),
|
|
_buildSuggestions(),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildSearchBar() {
|
|
return AnimatedBuilder(
|
|
animation: _scaleAnimation,
|
|
builder: (context, child) {
|
|
return Transform.scale(
|
|
scale: _scaleAnimation.value,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).cardColor,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: _isExpanded
|
|
? [BoxShadow(color: AppColors.shadowMedium, blurRadius: 10, offset: const Offset(0, 4))]
|
|
: [BoxShadow(color: AppColors.shadow, blurRadius: 5, offset: const Offset(0, 2))],
|
|
),
|
|
child: TextField(
|
|
controller: _searchController,
|
|
focusNode: _focusNode,
|
|
onSubmitted: (value) {
|
|
if (value.isNotEmpty) {
|
|
widget.onSearch?.call(value);
|
|
_focusNode.unfocus();
|
|
}
|
|
},
|
|
decoration: InputDecoration(
|
|
hintText: widget.hintText ?? 'Rechercher...',
|
|
hintStyle: AppTypography.bodyTextSmall.copyWith(
|
|
color: AppColors.textSecondary,
|
|
),
|
|
prefixIcon: Icon(
|
|
Icons.search_outlined,
|
|
color: _isExpanded ? AppColors.primary : AppColors.textSecondary,
|
|
size: 20,
|
|
),
|
|
suffixIcon: _searchController.text.isNotEmpty
|
|
? IconButton(
|
|
onPressed: () {
|
|
_searchController.clear();
|
|
_focusNode.unfocus();
|
|
},
|
|
icon: const Icon(
|
|
Icons.close_outlined,
|
|
color: AppColors.textSecondary,
|
|
size: 18,
|
|
),
|
|
)
|
|
: null,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: const BorderSide(
|
|
color: AppColors.primary,
|
|
width: 1.5,
|
|
),
|
|
),
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 12,
|
|
),
|
|
filled: true,
|
|
fillColor: Theme.of(context).cardColor,
|
|
),
|
|
style: AppTypography.bodyTextSmall,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildSuggestions() {
|
|
return Container(
|
|
constraints: const BoxConstraints(maxHeight: 300),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).cardColor,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: AppColors.shadowMedium,
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: ListView.builder(
|
|
shrinkWrap: true,
|
|
itemCount: _filteredSuggestions.length,
|
|
itemBuilder: (context, index) {
|
|
final suggestion = _filteredSuggestions[index];
|
|
return _buildSuggestionItem(suggestion, index == _filteredSuggestions.length - 1);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSuggestionItem(SearchSuggestion suggestion, bool isLast) {
|
|
return InkWell(
|
|
onTap: () {
|
|
_searchController.text = suggestion.title;
|
|
widget.onSearch?.call(suggestion.title);
|
|
_focusNode.unfocus();
|
|
suggestion.onTap?.call();
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
border: isLast
|
|
? null
|
|
: const Border(
|
|
bottom: BorderSide(
|
|
color: AppColors.border,
|
|
width: 1,
|
|
),
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(6),
|
|
decoration: BoxDecoration(
|
|
color: suggestion.color.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: Icon(
|
|
suggestion.icon,
|
|
color: suggestion.color,
|
|
size: 18,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
suggestion.title,
|
|
style: AppTypography.actionText.copyWith(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
if (suggestion.subtitle.isNotEmpty) ...[
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
suggestion.subtitle,
|
|
style: AppTypography.subtitleSmall.copyWith(
|
|
color: AppColors.textSecondary,
|
|
fontSize: 10,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
const Icon(
|
|
Icons.chevron_right_outlined,
|
|
color: AppColors.textSecondary,
|
|
size: 16,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
List<SearchSuggestion> _getDefaultSuggestions(BuildContext context) {
|
|
return [
|
|
SearchSuggestion(
|
|
title: 'Membres',
|
|
subtitle: 'Rechercher des membres',
|
|
icon: Icons.people_outline,
|
|
color: AppColors.primary,
|
|
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const MembersPageWrapper())),
|
|
),
|
|
SearchSuggestion(
|
|
title: 'Événements',
|
|
subtitle: 'Trouver des événements',
|
|
icon: Icons.event_outlined,
|
|
color: AppColors.primaryDark,
|
|
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const EventsPageWrapper())),
|
|
),
|
|
SearchSuggestion(
|
|
title: 'Contributions',
|
|
subtitle: 'Historique des paiements',
|
|
icon: Icons.account_balance_wallet_outlined,
|
|
color: AppColors.success,
|
|
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const ContributionsPageWrapper())),
|
|
),
|
|
SearchSuggestion(
|
|
title: 'Rapports',
|
|
subtitle: 'Consulter les rapports',
|
|
icon: Icons.assessment_outlined,
|
|
color: AppColors.warning,
|
|
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const ReportsPageWrapper())),
|
|
),
|
|
SearchSuggestion(
|
|
title: 'Paramètres',
|
|
subtitle: 'Configuration système',
|
|
icon: Icons.settings_outlined,
|
|
color: AppColors.textSecondary,
|
|
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
|
|
),
|
|
];
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_searchController.dispose();
|
|
_focusNode.dispose();
|
|
_animationController.dispose();
|
|
super.dispose();
|
|
}
|
|
}
|
|
|
|
class SearchSuggestion {
|
|
final String title;
|
|
final String subtitle;
|
|
final IconData icon;
|
|
final Color color;
|
|
final VoidCallback? onTap;
|
|
|
|
const SearchSuggestion({
|
|
required this.title,
|
|
required this.subtitle,
|
|
required this.icon,
|
|
required this.color,
|
|
this.onTap,
|
|
});
|
|
}
|