feat: WebSocket temps réel + Finance Workflow + corrections
- Task #6: WebSocket /ws/dashboard + Kafka events (5 topics) * Backend: KafkaEventProducer, KafkaEventConsumer * Mobile: WebSocketService (reconnection, heartbeat, typed events) * DashboardBloc: Auto-refresh depuis WebSocket events - Finance Workflow: approbations + budgets (backend + mobile) * Backend: entities, services, resources, migrations Flyway V6 * Mobile: features finance_workflow complète avec BLoC - Corrections DI: interfaces IRepository partout * IProfileRepository, IOrganizationRepository, IMembreRepository * GetIt configuré avec @injectable - Spec-Kit: constitution + templates mis à jour * .specify/memory/constitution.md enrichie * Templates agent, plan, spec, tasks, checklist - Nettoyage: fichiers temporaires supprimés Signed-off-by: lions dev Team
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../../shared/design_system/dashboard_theme.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 {
|
||||
@@ -26,13 +31,14 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
|
||||
late Animation<double> _scaleAnimation;
|
||||
bool _isExpanded = false;
|
||||
List<SearchSuggestion> _filteredSuggestions = [];
|
||||
List<SearchSuggestion>? _defaultSuggestions;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_setupAnimations();
|
||||
_setupListeners();
|
||||
_filteredSuggestions = widget.suggestions ?? _getDefaultSuggestions();
|
||||
_filteredSuggestions = widget.suggestions ?? [];
|
||||
}
|
||||
|
||||
void _setupAnimations() {
|
||||
@@ -71,12 +77,13 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
|
||||
void _filterSuggestions(String query) {
|
||||
if (query.isEmpty) {
|
||||
setState(() {
|
||||
_filteredSuggestions = widget.suggestions ?? _getDefaultSuggestions();
|
||||
_filteredSuggestions = widget.suggestions ?? _defaultSuggestions ?? [];
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final filtered = (widget.suggestions ?? _getDefaultSuggestions())
|
||||
final defaultList = widget.suggestions ?? _defaultSuggestions ?? [];
|
||||
final filtered = defaultList
|
||||
.where((suggestion) =>
|
||||
suggestion.title.toLowerCase().contains(query.toLowerCase()) ||
|
||||
suggestion.subtitle.toLowerCase().contains(query.toLowerCase()))
|
||||
@@ -89,11 +96,19 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
|
||||
|
||||
@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: DashboardTheme.spacing8),
|
||||
const SizedBox(height: 8),
|
||||
_buildSuggestions(),
|
||||
],
|
||||
],
|
||||
@@ -108,9 +123,11 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
|
||||
scale: _scaleAnimation.value,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusLarge),
|
||||
boxShadow: _isExpanded ? DashboardTheme.elevatedShadow : DashboardTheme.subtleShadow,
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: _isExpanded
|
||||
? [BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, 4))]
|
||||
: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5, offset: const Offset(0, 2))],
|
||||
),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
@@ -123,12 +140,13 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hintText ?? 'Rechercher...',
|
||||
hintStyle: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.grey400,
|
||||
hintStyle: AppTypography.bodyTextSmall.copyWith(
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: _isExpanded ? DashboardTheme.royalBlue : DashboardTheme.grey400,
|
||||
Icons.search_outlined,
|
||||
color: _isExpanded ? AppColors.primaryGreen : AppColors.textSecondaryLight,
|
||||
size: 20,
|
||||
),
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
@@ -137,30 +155,31 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
|
||||
_focusNode.unfocus();
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.clear,
|
||||
color: DashboardTheme.grey400,
|
||||
Icons.close_outlined,
|
||||
color: AppColors.textSecondaryLight,
|
||||
size: 18,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusLarge),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusLarge),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(
|
||||
color: DashboardTheme.royalBlue,
|
||||
width: 2,
|
||||
color: AppColors.primaryGreen,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: DashboardTheme.spacing16,
|
||||
vertical: DashboardTheme.spacing12,
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: DashboardTheme.white,
|
||||
fillColor: Theme.of(context).cardColor,
|
||||
),
|
||||
style: DashboardTheme.bodyMedium,
|
||||
style: AppTypography.bodyTextSmall,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -172,9 +191,15 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxHeight: 300),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white,
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
boxShadow: DashboardTheme.elevatedShadow,
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
@@ -196,13 +221,13 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
|
||||
suggestion.onTap?.call();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: isLast
|
||||
? null
|
||||
: const Border(
|
||||
bottom: BorderSide(
|
||||
color: DashboardTheme.grey200,
|
||||
color: AppColors.lightBorder,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
@@ -210,34 +235,36 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing8),
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: suggestion.color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Icon(
|
||||
suggestion.icon,
|
||||
color: suggestion.color,
|
||||
size: 20,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
suggestion.title,
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
style: AppTypography.actionText.copyWith(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (suggestion.subtitle.isNotEmpty) ...[
|
||||
const SizedBox(height: DashboardTheme.spacing2),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
suggestion.subtitle,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.grey600,
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
color: AppColors.textSecondaryLight,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -245,8 +272,8 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
color: DashboardTheme.grey400,
|
||||
Icons.chevron_right_outlined,
|
||||
color: AppColors.textSecondaryLight,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
@@ -255,42 +282,42 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
|
||||
);
|
||||
}
|
||||
|
||||
List<SearchSuggestion> _getDefaultSuggestions() {
|
||||
List<SearchSuggestion> _getDefaultSuggestions(BuildContext context) {
|
||||
return [
|
||||
SearchSuggestion(
|
||||
title: 'Membres',
|
||||
subtitle: 'Rechercher des membres',
|
||||
icon: Icons.people,
|
||||
color: DashboardTheme.royalBlue,
|
||||
onTap: () {},
|
||||
icon: Icons.people_outline,
|
||||
color: AppColors.primaryGreen,
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const MembersPageWrapper())),
|
||||
),
|
||||
SearchSuggestion(
|
||||
title: 'Événements',
|
||||
subtitle: 'Trouver des événements',
|
||||
icon: Icons.event,
|
||||
color: DashboardTheme.tealBlue,
|
||||
onTap: () {},
|
||||
icon: Icons.event_outlined,
|
||||
color: AppColors.brandGreen,
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const EventsPageWrapper())),
|
||||
),
|
||||
SearchSuggestion(
|
||||
title: 'Contributions',
|
||||
subtitle: 'Historique des paiements',
|
||||
icon: Icons.payment,
|
||||
color: DashboardTheme.success,
|
||||
onTap: () {},
|
||||
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,
|
||||
color: DashboardTheme.warning,
|
||||
onTap: () {},
|
||||
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,
|
||||
color: DashboardTheme.grey600,
|
||||
onTap: () {},
|
||||
icon: Icons.settings_outlined,
|
||||
color: AppColors.textSecondaryLight,
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user