Files
unionflow-mobile-apps/lib/features/onboarding/presentation/pages/period_selection_page.dart
2026-03-31 09:14:47 +00:00

225 lines
8.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../bloc/onboarding_bloc.dart';
import '../../data/models/formule_model.dart';
import '../../../../features/authentication/presentation/bloc/auth_bloc.dart';
/// Étape 2 — Choix de la période de facturation et du type d'organisation
class PeriodSelectionPage extends StatefulWidget {
final String codeFormule;
final String plage;
final List<FormuleModel> formules;
const PeriodSelectionPage({
super.key,
required this.codeFormule,
required this.plage,
required this.formules,
});
@override
State<PeriodSelectionPage> createState() => _PeriodSelectionPageState();
}
class _PeriodSelectionPageState extends State<PeriodSelectionPage> {
String _selectedPeriode = 'MENSUEL';
String _selectedTypeOrg = 'ASSOCIATION';
static const _periodes = [
('MENSUEL', 'Mensuel', 'Aucune remise', 1.00),
('TRIMESTRIEL', 'Trimestriel', '5% de remise', 0.95),
('SEMESTRIEL', 'Semestriel', '10% de remise', 0.90),
('ANNUEL', 'Annuel', '20% de remise', 0.80),
];
static const _typesOrg = [
('ASSOCIATION', 'Association / ONG locale', '×1.0'),
('MUTUELLE', 'Mutuelle (santé, fonctionnaires…)', '×1.2'),
('COOPERATIVE', 'Coopérative / Microfinance', '×1.3'),
('FEDERATION', 'Fédération / Grande ONG', '×1.0 ou ×1.5 Premium'),
];
FormuleModel? get _formule => widget.formules
.where((f) => f.code == widget.codeFormule && f.plage == widget.plage)
.firstOrNull;
double get _prixEstime {
final f = _formule;
if (f == null) return 0;
final coefPeriode =
_periodes.firstWhere((p) => p.$1 == _selectedPeriode).$4;
final coefOrg =
_selectedTypeOrg == 'COOPERATIVE' ? 1.3 : _selectedTypeOrg == 'MUTUELLE' ? 1.2 : 1.0;
final nbMois = {'MENSUEL': 1, 'TRIMESTRIEL': 3, 'SEMESTRIEL': 6, 'ANNUEL': 12}[_selectedPeriode]!;
return f.prixMensuel * coefOrg * coefPeriode * nbMois;
}
String get _organisationId {
final authState = context.read<AuthBloc>().state;
if (authState is AuthAuthenticated) {
return authState.user.organizationContexts.isNotEmpty
? authState.user.organizationContexts.first.organizationId
: '';
}
if (authState is AuthPendingOnboarding) {
return authState.organisationId ?? '';
}
return '';
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('Période & type d\'organisation'),
leading: BackButton(
onPressed: () => context.read<OnboardingBloc>().add(
OnboardingStarted(initialState: 'NO_SUBSCRIPTION'),
),
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_StepIndicator(current: 2, total: 3),
const SizedBox(height: 24),
// Période
Text('Période de facturation', style: theme.textTheme.titleMedium),
const SizedBox(height: 8),
..._periodes.map((p) {
final (code, label, remise, _) = p;
final selected = _selectedPeriode == code;
return RadioListTile<String>(
value: code,
groupValue: _selectedPeriode,
onChanged: (v) => setState(() => _selectedPeriode = v!),
title: Text(label,
style: TextStyle(
fontWeight:
selected ? FontWeight.bold : FontWeight.normal)),
subtitle: Text(remise,
style: TextStyle(
color: code != 'MENSUEL' ? Colors.green[700] : null)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(
color: selected
? theme.primaryColor
: Colors.grey[300]!,
),
),
tileColor:
selected ? theme.primaryColor.withOpacity(0.05) : null,
contentPadding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
);
}),
const SizedBox(height: 24),
Text('Type de votre organisation', style: theme.textTheme.titleMedium),
const SizedBox(height: 4),
Text(
'Détermine le coefficient tarifaire applicable.',
style:
theme.textTheme.bodySmall?.copyWith(color: Colors.grey[600]),
),
const SizedBox(height: 8),
..._typesOrg.map((t) {
final (code, label, coef) = t;
final selected = _selectedTypeOrg == code;
return RadioListTile<String>(
value: code,
groupValue: _selectedTypeOrg,
onChanged: (v) => setState(() => _selectedTypeOrg = v!),
title: Text(label),
subtitle: Text('Coefficient : $coef',
style: const TextStyle(fontSize: 12)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(
color: selected ? theme.primaryColor : Colors.grey[300]!,
),
),
tileColor:
selected ? theme.primaryColor.withOpacity(0.05) : null,
contentPadding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
);
}),
const SizedBox(height: 24),
// Estimation du prix
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.primaryColor.withOpacity(0.08),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Estimation', style: TextStyle(fontWeight: FontWeight.bold)),
Text(
'${_prixEstime.toStringAsFixed(0)} FCFA',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: theme.primaryColor,
),
),
],
),
),
const SizedBox(height: 32),
],
),
),
bottomNavigationBar: Padding(
padding: const EdgeInsets.all(16),
child: ElevatedButton(
onPressed: () {
context.read<OnboardingBloc>()
..add(OnboardingPeriodeSelected(
typePeriode: _selectedPeriode,
typeOrganisation: _selectedTypeOrg,
organisationId: _organisationId,
))
..add(const OnboardingDemandeConfirmee());
},
style: ElevatedButton.styleFrom(minimumSize: const Size.fromHeight(48)),
child: const Text('Voir le récapitulatif'),
),
),
);
}
}
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),
),
),
);
}),
);
}
}