Initial commit: unionflow-mobile-apps
Application Flutter complète (sans build artifacts). Signed-off-by: lions dev Team
This commit is contained in:
@@ -0,0 +1,534 @@
|
||||
/// Page de liste des budgets
|
||||
library budgets_list_page;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../../../core/di/injection_container.dart';
|
||||
import '../../../../shared/design_system/unionflow_design_system.dart';
|
||||
import '../../domain/entities/budget.dart';
|
||||
import '../bloc/budget_bloc.dart';
|
||||
import '../bloc/budget_event.dart';
|
||||
import '../bloc/budget_state.dart';
|
||||
|
||||
class BudgetsListPage extends StatelessWidget {
|
||||
final String? organizationId;
|
||||
|
||||
const BudgetsListPage({
|
||||
super.key,
|
||||
this.organizationId,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => getIt<BudgetBloc>()
|
||||
..add(LoadBudgets(organizationId: organizationId)),
|
||||
child: _BudgetsListView(organizationId: organizationId),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BudgetsListView extends StatelessWidget {
|
||||
final String? organizationId;
|
||||
|
||||
const _BudgetsListView({this.organizationId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: ColorTokens.background,
|
||||
appBar: UFAppBar(
|
||||
title: 'BUDGETS',
|
||||
automaticallyImplyLeading: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.filter_list),
|
||||
onPressed: () => _showFilterDialog(context),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () {
|
||||
context.read<BudgetBloc>().add(
|
||||
RefreshBudgets(organizationId: organizationId),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: BlocConsumer<BudgetBloc, BudgetState>(
|
||||
listener: (context, state) {
|
||||
if (state is BudgetCreated) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
backgroundColor: AppColors.success,
|
||||
),
|
||||
);
|
||||
context.read<BudgetBloc>().add(
|
||||
RefreshBudgets(organizationId: organizationId),
|
||||
);
|
||||
} else if (state is BudgetError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
backgroundColor: AppColors.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is BudgetsLoading || state is BudgetActionInProgress) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (state is BudgetsEmpty) {
|
||||
return _buildEmptyState(state.message);
|
||||
}
|
||||
|
||||
if (state is BudgetsLoaded) {
|
||||
return _buildBudgetsList(context, state);
|
||||
}
|
||||
|
||||
if (state is BudgetError) {
|
||||
return _buildErrorState(context, state.message);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
// TODO: Navigate to create budget page
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Fonctionnalité en cours de développement'),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Nouveau budget'),
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBudgetsList(BuildContext context, BudgetsLoaded state) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
context.read<BudgetBloc>().add(
|
||||
RefreshBudgets(
|
||||
organizationId: organizationId,
|
||||
status: state.filterStatus,
|
||||
year: state.filterYear,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
if (state.filterStatus != null || state.filterYear != null)
|
||||
_buildFilterChips(context, state),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.all(SpacingTokens.md),
|
||||
itemCount: state.budgets.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: SpacingTokens.md),
|
||||
itemBuilder: (context, index) {
|
||||
final budget = state.budgets[index];
|
||||
return _BudgetCard(budget: budget);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterChips(BuildContext context, BudgetsLoaded state) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(SpacingTokens.md),
|
||||
color: AppColors.lightBackground,
|
||||
child: Wrap(
|
||||
spacing: SpacingTokens.sm,
|
||||
runSpacing: SpacingTokens.sm,
|
||||
children: [
|
||||
if (state.filterStatus != null)
|
||||
Chip(
|
||||
label: Text(_getStatusLabel(state.filterStatus!)),
|
||||
onDeleted: () {
|
||||
context.read<BudgetBloc>().add(
|
||||
FilterBudgets(
|
||||
status: null,
|
||||
year: state.filterYear,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (state.filterYear != null)
|
||||
Chip(
|
||||
label: Text('Année ${state.filterYear}'),
|
||||
onDeleted: () {
|
||||
context.read<BudgetBloc>().add(
|
||||
FilterBudgets(
|
||||
status: state.filterStatus,
|
||||
year: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(String message) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.account_balance_wallet_outlined,
|
||||
size: 80,
|
||||
color: AppColors.textSecondaryLight.withOpacity(0.5),
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.lg),
|
||||
Text(
|
||||
message,
|
||||
style: AppTypography.headerSmall.copyWith(
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorState(BuildContext context, String message) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 80,
|
||||
color: AppColors.error.withOpacity(0.5),
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.lg),
|
||||
Text(
|
||||
message,
|
||||
style: AppTypography.headerSmall.copyWith(
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.lg),
|
||||
UFPrimaryButton(
|
||||
label: 'Réessayer',
|
||||
onPressed: () {
|
||||
context.read<BudgetBloc>().add(
|
||||
LoadBudgets(organizationId: organizationId),
|
||||
);
|
||||
},
|
||||
icon: Icons.refresh,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showFilterDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => BlocProvider.value(
|
||||
value: context.read<BudgetBloc>(),
|
||||
child: _FilterDialog(organizationId: organizationId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getStatusLabel(BudgetStatus status) {
|
||||
switch (status) {
|
||||
case BudgetStatus.draft:
|
||||
return 'Brouillon';
|
||||
case BudgetStatus.active:
|
||||
return 'Actif';
|
||||
case BudgetStatus.closed:
|
||||
return 'Clos';
|
||||
case BudgetStatus.cancelled:
|
||||
return 'Annulé';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _BudgetCard extends StatelessWidget {
|
||||
final Budget budget;
|
||||
|
||||
const _BudgetCard({required this.budget});
|
||||
|
||||
String _getPeriodLabel(BudgetPeriod period) {
|
||||
switch (period) {
|
||||
case BudgetPeriod.monthly:
|
||||
return 'Mensuel';
|
||||
case BudgetPeriod.quarterly:
|
||||
return 'Trimestriel';
|
||||
case BudgetPeriod.semiannual:
|
||||
return 'Semestriel';
|
||||
case BudgetPeriod.annual:
|
||||
return 'Annuel';
|
||||
}
|
||||
}
|
||||
|
||||
Color _getStatusColor(BudgetStatus status) {
|
||||
switch (status) {
|
||||
case BudgetStatus.draft:
|
||||
return AppColors.textSecondaryLight;
|
||||
case BudgetStatus.active:
|
||||
return AppColors.brandGreen;
|
||||
case BudgetStatus.closed:
|
||||
return AppColors.textSecondaryLight;
|
||||
case BudgetStatus.cancelled:
|
||||
return AppColors.error;
|
||||
}
|
||||
}
|
||||
|
||||
String _getStatusLabel(BudgetStatus status) {
|
||||
switch (status) {
|
||||
case BudgetStatus.draft:
|
||||
return 'Brouillon';
|
||||
case BudgetStatus.active:
|
||||
return 'Actif';
|
||||
case BudgetStatus.closed:
|
||||
return 'Clos';
|
||||
case BudgetStatus.cancelled:
|
||||
return 'Annulé';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currencyFormat = NumberFormat.currency(symbol: budget.currency);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(SpacingTokens.md),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
|
||||
border: Border.all(color: AppColors.lightBorder),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0A000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
budget.name,
|
||||
style: AppTypography.actionText,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _getStatusColor(budget.status).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusSm),
|
||||
),
|
||||
child: Text(
|
||||
_getStatusLabel(budget.status),
|
||||
style: AppTypography.badgeText.copyWith(
|
||||
color: _getStatusColor(budget.status),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.sm),
|
||||
Text(
|
||||
'${_getPeriodLabel(budget.period)} ${budget.year}',
|
||||
style: AppTypography.subtitleSmall,
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Prévu',
|
||||
style: AppTypography.subtitleSmall,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
currencyFormat.format(budget.totalPlanned),
|
||||
style: AppTypography.headerSmall.copyWith(
|
||||
color: AppColors.primaryGreen,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Réalisé',
|
||||
style: AppTypography.subtitleSmall,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
currencyFormat.format(budget.totalRealized),
|
||||
style: AppTypography.headerSmall.copyWith(
|
||||
color: budget.isOverBudget
|
||||
? AppColors.error
|
||||
: AppColors.brandGreen,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.sm),
|
||||
LinearProgressIndicator(
|
||||
value: budget.realizationRate / 100,
|
||||
backgroundColor: AppColors.lightBorder,
|
||||
color: budget.isOverBudget
|
||||
? AppColors.error
|
||||
: AppColors.brandGreen,
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.xs),
|
||||
Text(
|
||||
'${budget.realizationRate.toStringAsFixed(1)}% réalisé',
|
||||
style: AppTypography.subtitleSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FilterDialog extends StatefulWidget {
|
||||
final String? organizationId;
|
||||
|
||||
const _FilterDialog({this.organizationId});
|
||||
|
||||
@override
|
||||
State<_FilterDialog> createState() => _FilterDialogState();
|
||||
}
|
||||
|
||||
class _FilterDialogState extends State<_FilterDialog> {
|
||||
BudgetStatus? _selectedStatus;
|
||||
int? _selectedYear;
|
||||
|
||||
final _currentYear = DateTime.now().year;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Filtrer les budgets'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Statut',
|
||||
style: AppTypography.actionText,
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.sm),
|
||||
Wrap(
|
||||
spacing: SpacingTokens.sm,
|
||||
children: [
|
||||
for (final status in BudgetStatus.values)
|
||||
ChoiceChip(
|
||||
label: Text(_getStatusLabel(status)),
|
||||
selected: _selectedStatus == status,
|
||||
onSelected: (selected) {
|
||||
setState(() {
|
||||
_selectedStatus = selected ? status : null;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
Text(
|
||||
'Année',
|
||||
style: AppTypography.actionText,
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.sm),
|
||||
Wrap(
|
||||
spacing: SpacingTokens.sm,
|
||||
children: [
|
||||
for (int year = _currentYear; year >= _currentYear - 5; year--)
|
||||
ChoiceChip(
|
||||
label: Text(year.toString()),
|
||||
selected: _selectedYear == year,
|
||||
onSelected: (selected) {
|
||||
setState(() {
|
||||
_selectedYear = selected ? year : null;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_selectedStatus = null;
|
||||
_selectedYear = null;
|
||||
});
|
||||
},
|
||||
child: const Text('Réinitialiser'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
context.read<BudgetBloc>().add(
|
||||
FilterBudgets(
|
||||
status: _selectedStatus,
|
||||
year: _selectedYear,
|
||||
),
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('Appliquer'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _getStatusLabel(BudgetStatus status) {
|
||||
switch (status) {
|
||||
case BudgetStatus.draft:
|
||||
return 'Brouillon';
|
||||
case BudgetStatus.active:
|
||||
return 'Actif';
|
||||
case BudgetStatus.closed:
|
||||
return 'Clos';
|
||||
case BudgetStatus.cancelled:
|
||||
return 'Annulé';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
/// Page des approbations en attente
|
||||
library pending_approvals_page;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../core/di/injection_container.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../../../core/di/injection_container.dart';
|
||||
import '../../../../shared/design_system/unionflow_design_system.dart';
|
||||
import '../../domain/entities/transaction_approval.dart';
|
||||
import '../bloc/approval_bloc.dart';
|
||||
import '../bloc/approval_event.dart';
|
||||
import '../bloc/approval_state.dart';
|
||||
import '../widgets/approve_dialog.dart';
|
||||
import '../widgets/reject_dialog.dart';
|
||||
|
||||
class PendingApprovalsPage extends StatelessWidget {
|
||||
final String? organizationId;
|
||||
|
||||
const PendingApprovalsPage({
|
||||
super.key,
|
||||
this.organizationId,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => getIt<ApprovalBloc>()
|
||||
..add(LoadPendingApprovals(organizationId: organizationId)),
|
||||
child: _PendingApprovalsView(organizationId: organizationId),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PendingApprovalsView extends StatelessWidget {
|
||||
final String? organizationId;
|
||||
|
||||
const _PendingApprovalsView({this.organizationId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: ColorTokens.background,
|
||||
appBar: UFAppBar(
|
||||
title: 'APPROBATIONS EN ATTENTE',
|
||||
automaticallyImplyLeading: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () {
|
||||
context.read<ApprovalBloc>().add(
|
||||
RefreshApprovals(organizationId: organizationId),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: BlocConsumer<ApprovalBloc, ApprovalState>(
|
||||
listener: (context, state) {
|
||||
if (state is TransactionApproved) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
backgroundColor: AppColors.success,
|
||||
),
|
||||
);
|
||||
context.read<ApprovalBloc>().add(
|
||||
RefreshApprovals(organizationId: organizationId),
|
||||
);
|
||||
} else if (state is TransactionRejected) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
backgroundColor: AppColors.warning,
|
||||
),
|
||||
);
|
||||
context.read<ApprovalBloc>().add(
|
||||
RefreshApprovals(organizationId: organizationId),
|
||||
);
|
||||
} else if (state is ApprovalError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
backgroundColor: AppColors.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is ApprovalsLoading || state is ApprovalActionInProgress) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (state is ApprovalsEmpty) {
|
||||
return _buildEmptyState();
|
||||
}
|
||||
|
||||
if (state is ApprovalsLoaded) {
|
||||
return _buildApprovalsList(context, state.approvals);
|
||||
}
|
||||
|
||||
if (state is ApprovalError) {
|
||||
return _buildErrorState(context, state.message);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildApprovalsList(
|
||||
BuildContext context,
|
||||
List<TransactionApproval> approvals,
|
||||
) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
context.read<ApprovalBloc>().add(
|
||||
RefreshApprovals(organizationId: organizationId),
|
||||
);
|
||||
},
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.all(SpacingTokens.md),
|
||||
itemCount: approvals.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: SpacingTokens.md),
|
||||
itemBuilder: (context, index) {
|
||||
final approval = approvals[index];
|
||||
return _ApprovalCard(
|
||||
approval: approval,
|
||||
onApprove: () => _showApproveDialog(context, approval),
|
||||
onReject: () => _showRejectDialog(context, approval),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showApproveDialog(BuildContext context, TransactionApproval approval) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => BlocProvider.value(
|
||||
value: context.read<ApprovalBloc>(),
|
||||
child: ApproveDialog(approval: approval),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showRejectDialog(BuildContext context, TransactionApproval approval) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => BlocProvider.value(
|
||||
value: context.read<ApprovalBloc>(),
|
||||
child: RejectDialog(approval: approval),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorState(BuildContext context, String message) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 80,
|
||||
color: AppColors.error.withOpacity(0.5),
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.lg),
|
||||
Text(
|
||||
message,
|
||||
style: AppTypography.headerSmall.copyWith(
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.lg),
|
||||
UFPrimaryButton(
|
||||
label: 'Réessayer',
|
||||
onPressed: () {
|
||||
context.read<ApprovalBloc>().add(
|
||||
LoadPendingApprovals(organizationId: organizationId),
|
||||
);
|
||||
},
|
||||
icon: Icons.refresh,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.check_circle_outline,
|
||||
size: 80,
|
||||
color: AppColors.success.withOpacity(0.5),
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.lg),
|
||||
Text(
|
||||
'Aucune approbation en attente',
|
||||
style: AppTypography.headerSmall.copyWith(
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.sm),
|
||||
Text(
|
||||
'Toutes les transactions sont approuvées',
|
||||
style: AppTypography.bodyTextSmall.copyWith(
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class _ApprovalCard extends StatelessWidget {
|
||||
final TransactionApproval approval;
|
||||
final VoidCallback onApprove;
|
||||
final VoidCallback onReject;
|
||||
|
||||
const _ApprovalCard({
|
||||
required this.approval,
|
||||
required this.onApprove,
|
||||
required this.onReject,
|
||||
});
|
||||
|
||||
String _getTransactionTypeLabel(TransactionType type) {
|
||||
switch (type) {
|
||||
case TransactionType.contribution:
|
||||
return 'Cotisation';
|
||||
case TransactionType.deposit:
|
||||
return 'Dépôt';
|
||||
case TransactionType.withdrawal:
|
||||
return 'Retrait';
|
||||
case TransactionType.transfer:
|
||||
return 'Transfert';
|
||||
case TransactionType.solidarity:
|
||||
return 'Solidarité';
|
||||
case TransactionType.event:
|
||||
return 'Événement';
|
||||
case TransactionType.other:
|
||||
return 'Autre';
|
||||
}
|
||||
}
|
||||
|
||||
Color _getLevelColor(ApprovalLevel level) {
|
||||
switch (level) {
|
||||
case ApprovalLevel.none:
|
||||
return AppColors.textSecondaryLight;
|
||||
case ApprovalLevel.level1:
|
||||
return AppColors.brandGreen;
|
||||
case ApprovalLevel.level2:
|
||||
return AppColors.warning;
|
||||
case ApprovalLevel.level3:
|
||||
return AppColors.error;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currencyFormat = NumberFormat.currency(symbol: approval.currency);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(SpacingTokens.md),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
|
||||
border: Border.all(color: AppColors.lightBorder),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0A000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
_getTransactionTypeLabel(approval.transactionType),
|
||||
style: AppTypography.actionText,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _getLevelColor(approval.requiredLevel).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusSm),
|
||||
),
|
||||
child: Text(
|
||||
'Niveau ${approval.requiredApprovals}',
|
||||
style: AppTypography.badgeText.copyWith(
|
||||
color: _getLevelColor(approval.requiredLevel),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.sm),
|
||||
Text(
|
||||
currencyFormat.format(approval.amount),
|
||||
style: AppTypography.headerSmall.copyWith(
|
||||
color: AppColors.primaryGreen,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.sm),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.person_outline,
|
||||
size: 16,
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Demandé par ${approval.requesterName}',
|
||||
style: AppTypography.subtitleSmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.access_time,
|
||||
size: 16,
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
DateFormat('dd/MM/yyyy HH:mm').format(approval.createdAt),
|
||||
style: AppTypography.subtitleSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (approval.approvers.isNotEmpty) ...[
|
||||
const SizedBox(height: SpacingTokens.sm),
|
||||
const Divider(),
|
||||
const SizedBox(height: SpacingTokens.sm),
|
||||
Text(
|
||||
'Approbations : ${approval.approvalCount}/${approval.requiredApprovals}',
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: UFSecondaryButton(
|
||||
label: 'Rejeter',
|
||||
onPressed: onReject,
|
||||
icon: Icons.close,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: SpacingTokens.sm),
|
||||
Expanded(
|
||||
child: UFPrimaryButton(
|
||||
label: 'Approuver',
|
||||
onPressed: onApprove,
|
||||
icon: Icons.check,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user