refactoring

This commit is contained in:
dahoud
2026-03-31 09:14:47 +00:00
parent 9bfffeeebe
commit 5383df6dcb
200 changed files with 11192 additions and 7063 deletions

View File

@@ -0,0 +1,252 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../bloc/onboarding_bloc.dart';
import '../../data/models/formule_model.dart';
/// Étape 1 — Choix de la formule (BASIC / STANDARD / PREMIUM) et de la plage de membres
class PlanSelectionPage extends StatefulWidget {
final List<FormuleModel> formules;
const PlanSelectionPage({super.key, required this.formules});
@override
State<PlanSelectionPage> createState() => _PlanSelectionPageState();
}
class _PlanSelectionPageState extends State<PlanSelectionPage> {
String? _selectedPlage;
String? _selectedFormule;
static const _plages = [
('PETITE', 'Petite', '1100 membres'),
('MOYENNE', 'Moyenne', '101500 membres'),
('GRANDE', 'Grande', '5012 000 membres'),
('TRES_GRANDE', 'Très grande', '2 000+ membres'),
];
static const _formules = [
('BASIC', 'Basic', Icons.star_outline, Color(0xFF1976D2)),
('STANDARD', 'Standard', Icons.star_half, Color(0xFF388E3C)),
('PREMIUM', 'Premium', Icons.star, Color(0xFFF57C00)),
];
List<FormuleModel> get _filteredFormules => widget.formules
.where((f) => _selectedPlage == null || f.plage == _selectedPlage)
.toList()
..sort((a, b) => a.ordreAffichage.compareTo(b.ordreAffichage));
FormuleModel? get _selectedFormuleModel => _filteredFormules
.where((f) => f.code == _selectedFormule && f.plage == _selectedPlage)
.firstOrNull;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('Choisir votre formule'),
automaticallyImplyLeading: false,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Indicateur d'étapes
_StepIndicator(current: 1, total: 3),
const SizedBox(height: 24),
Text('Taille de votre organisation',
style: theme.textTheme.titleMedium),
const SizedBox(height: 8),
// Sélecteur de plage
Wrap(
spacing: 8,
runSpacing: 8,
children: _plages.map((p) {
final (code, label, sublabel) = p;
final selected = _selectedPlage == code;
return ChoiceChip(
label: Column(
children: [
Text(label,
style: TextStyle(
fontWeight: FontWeight.bold,
color: selected ? Colors.white : null)),
Text(sublabel,
style: TextStyle(
fontSize: 11,
color: selected
? Colors.white70
: Colors.grey[600])),
],
),
selected: selected,
onSelected: (_) => setState(() {
_selectedPlage = code;
_selectedFormule = null;
}),
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
);
}).toList(),
),
if (_selectedPlage != null) ...[
const SizedBox(height: 24),
Text('Niveau de formule', style: theme.textTheme.titleMedium),
const SizedBox(height: 8),
// Cartes de formules
..._formules.map((f) {
final (code, label, icon, color) = f;
final formule = _filteredFormules
.where((fm) => fm.code == code)
.firstOrNull;
if (formule == null) return const SizedBox.shrink();
final selected = _selectedFormule == code;
return _FormuleCard(
formule: formule,
label: label,
icon: icon,
color: color,
selected: selected,
onTap: () => setState(() => _selectedFormule = code),
);
}),
],
const SizedBox(height: 32),
],
),
),
bottomNavigationBar: Padding(
padding: const EdgeInsets.all(16),
child: ElevatedButton(
onPressed: _selectedPlage != null && _selectedFormule != null
? () => context.read<OnboardingBloc>().add(
OnboardingFormuleSelected(
codeFormule: _selectedFormule!,
plage: _selectedPlage!,
),
)
: null,
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(48),
),
child: const Text('Continuer'),
),
),
);
}
}
class _FormuleCard extends StatelessWidget {
final FormuleModel formule;
final String label;
final IconData icon;
final Color color;
final bool selected;
final VoidCallback onTap;
const _FormuleCard({
required this.formule,
required this.label,
required this.icon,
required this.color,
required this.selected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
color: selected ? color.withOpacity(0.1) : Colors.white,
border: Border.all(
color: selected ? color : Colors.grey[300]!,
width: selected ? 2 : 1,
),
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(icon, color: color, size: 28),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: selected ? color : null)),
if (formule.description != null)
Text(formule.description!,
style: const TextStyle(fontSize: 12, color: Colors.grey)),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${_formatPrix(formule.prixMensuel)} FCFA',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
color: color),
),
const Text('/mois', style: TextStyle(fontSize: 11, color: Colors.grey)),
],
),
const SizedBox(width: 8),
Icon(
selected ? Icons.check_circle : Icons.radio_button_unchecked,
color: selected ? color : Colors.grey,
),
],
),
),
),
);
}
String _formatPrix(double prix) {
if (prix >= 1000) {
return '${(prix / 1000).toStringAsFixed(0)} 000';
}
return prix.toStringAsFixed(0);
}
}
class _StepIndicator extends StatelessWidget {
final int current;
final int total;
const _StepIndicator({required this.current, required this.total});
@override
Widget build(BuildContext context) {
return Row(
children: List.generate(total, (i) {
final active = i + 1 <= current;
return Expanded(
child: Container(
height: 4,
margin: EdgeInsets.only(right: i < total - 1 ? 4 : 0),
decoration: BoxDecoration(
color: active
? Theme.of(context).primaryColor
: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
);
}),
);
}
}