Compare commits
7
Commits
8a888e2ecd
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a10b00f84 | ||
|
|
615fe2b937 | ||
|
|
bb5ac94e8b | ||
|
|
cef740de26 | ||
|
|
eb4c322001 | ||
|
|
e0028e10e0 | ||
|
|
d42163f12f |
@@ -0,0 +1,160 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
import '../router/app_router.dart';
|
||||||
|
import '../theme/app_colors.dart';
|
||||||
|
|
||||||
|
/// Fenêtre de confirmation avant le lancement d'un lavage.
|
||||||
|
class WashStartConfirmationDialog {
|
||||||
|
static Future<bool> show(
|
||||||
|
BuildContext context, {
|
||||||
|
required String machineName,
|
||||||
|
required double currentBalance,
|
||||||
|
required double price,
|
||||||
|
required NumberFormat currency,
|
||||||
|
}) async {
|
||||||
|
final remainingBalance = currentBalance - price;
|
||||||
|
final hasEnoughBalance = remainingBalance >= 0;
|
||||||
|
|
||||||
|
final result = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) => AlertDialog(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
|
title: const Text('Confirmer le lavage'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
machineName,
|
||||||
|
style: Theme.of(dialogContext).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: AppColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.background,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: AppColors.divider),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_AmountRow(
|
||||||
|
label: 'Solde actuel',
|
||||||
|
amount: currency.format(currentBalance),
|
||||||
|
emphasized: true,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_AmountRow(
|
||||||
|
label: 'Prix du cycle',
|
||||||
|
amount: '− ${currency.format(price)}',
|
||||||
|
color: AppColors.textSecondary,
|
||||||
|
),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 12),
|
||||||
|
child: Divider(height: 1),
|
||||||
|
),
|
||||||
|
_AmountRow(
|
||||||
|
label: 'Solde restant',
|
||||||
|
amount: currency.format(remainingBalance),
|
||||||
|
emphasized: true,
|
||||||
|
color: hasEnoughBalance ? AppColors.success : AppColors.error,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!hasEnoughBalance) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.error.withValues(alpha: 0.08),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.warning_amber_rounded, color: AppColors.error, size: 20),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Solde insuffisant. Rechargez votre portefeuille pour continuer.',
|
||||||
|
style: Theme.of(dialogContext).textTheme.bodySmall?.copyWith(
|
||||||
|
color: AppColors.error,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(dialogContext, false),
|
||||||
|
child: const Text('Annuler'),
|
||||||
|
),
|
||||||
|
if (!hasEnoughBalance)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(dialogContext, false);
|
||||||
|
dialogContext.push(AppRoutes.walletTopUp);
|
||||||
|
},
|
||||||
|
child: const Text('Recharger'),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Navigator.pop(dialogContext, true),
|
||||||
|
child: const Text('Démarrer le lavage'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return result ?? false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AmountRow extends StatelessWidget {
|
||||||
|
const _AmountRow({
|
||||||
|
required this.label,
|
||||||
|
required this.amount,
|
||||||
|
this.emphasized = false,
|
||||||
|
this.color,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final String amount;
|
||||||
|
final bool emphasized;
|
||||||
|
final Color? color;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final amountColor = color ?? AppColors.textPrimary;
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
fontWeight: emphasized ? FontWeight.w600 : FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
amount,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
color: amountColor,
|
||||||
|
fontWeight: emphasized ? FontWeight.w700 : FontWeight.w600,
|
||||||
|
fontSize: emphasized ? 18 : 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -60,14 +60,9 @@ class HomeScreen extends ConsumerWidget {
|
|||||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
user?.firstName != null ? 'Bonjour ${user!.firstName} 👋' : 'Bienvenue',
|
user?.firstName != null ? 'Bonjour ${user!.firstName}' : 'Bienvenue',
|
||||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(fontSize: 20),
|
style: Theme.of(context).textTheme.headlineMedium?.copyWith(fontSize: 20),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
'Votre laverie, dans votre poche.',
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const QuickActionsRow(),
|
const QuickActionsRow(),
|
||||||
if (activeWash != null) ...[
|
if (activeWash != null) ...[
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import '../../../core/router/app_router.dart';
|
|||||||
import '../../../core/theme/app_colors.dart';
|
import '../../../core/theme/app_colors.dart';
|
||||||
import '../../../core/theme/machine_status_theme.dart';
|
import '../../../core/theme/machine_status_theme.dart';
|
||||||
import '../../../core/widgets/empty_state.dart';
|
import '../../../core/widgets/empty_state.dart';
|
||||||
|
import '../../../core/widgets/wash_start_confirmation_dialog.dart';
|
||||||
|
import '../../wallet/data/wallet_repository.dart';
|
||||||
import '../../wash/data/wash_repository.dart';
|
import '../../wash/data/wash_repository.dart';
|
||||||
import '../data/machine_repository.dart';
|
import '../data/machine_repository.dart';
|
||||||
import '../domain/machine_detail.dart';
|
import '../domain/machine_detail.dart';
|
||||||
@@ -25,11 +27,46 @@ class MachineActionScreen extends ConsumerStatefulWidget {
|
|||||||
class _MachineActionScreenState extends ConsumerState<MachineActionScreen> {
|
class _MachineActionScreenState extends ConsumerState<MachineActionScreen> {
|
||||||
bool _isStarting = false;
|
bool _isStarting = false;
|
||||||
|
|
||||||
|
Future<void> _confirmAndStartWash(MachineDetail detail) async {
|
||||||
|
final currency = NumberFormat.currency(locale: 'fr_FR', symbol: '€');
|
||||||
|
|
||||||
|
double currentBalance;
|
||||||
|
try {
|
||||||
|
final wallet = await ref.read(walletProvider.future);
|
||||||
|
currentBalance = wallet.currentBalance;
|
||||||
|
} catch (_) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Impossible de charger votre solde'),
|
||||||
|
backgroundColor: AppColors.error,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
final confirmed = await WashStartConfirmationDialog.show(
|
||||||
|
context,
|
||||||
|
machineName: detail.machine.name,
|
||||||
|
currentBalance: currentBalance,
|
||||||
|
price: detail.price,
|
||||||
|
currency: currency,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmed && mounted) {
|
||||||
|
await _startWash(detail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _startWash(MachineDetail detail) async {
|
Future<void> _startWash(MachineDetail detail) async {
|
||||||
setState(() => _isStarting = true);
|
setState(() => _isStarting = true);
|
||||||
try {
|
try {
|
||||||
await ref.read(washRepositoryProvider).startWashFromMachine(detail.machine.uuid);
|
await ref.read(washRepositoryProvider).startWashFromMachine(detail.machine.uuid);
|
||||||
ref.invalidate(washesProvider);
|
ref.invalidate(washesProvider);
|
||||||
|
ref.invalidate(walletProvider);
|
||||||
ref.invalidate(machineDetailProvider(widget.machineUuid));
|
ref.invalidate(machineDetailProvider(widget.machineUuid));
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -74,7 +111,7 @@ class _MachineActionScreenState extends ConsumerState<MachineActionScreen> {
|
|||||||
detail: detail,
|
detail: detail,
|
||||||
currency: currency,
|
currency: currency,
|
||||||
isStarting: _isStarting,
|
isStarting: _isStarting,
|
||||||
onStart: () => _startWash(detail),
|
onStart: () => _confirmAndStartWash(detail),
|
||||||
onReserve: () => context.push(AppRoutes.machineBooking(widget.machineUuid)),
|
onReserve: () => context.push(AppRoutes.machineBooking(widget.machineUuid)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ description: >
|
|||||||
Application mobile native Laverie Connectée (Android + iOS).
|
Application mobile native Laverie Connectée (Android + iOS).
|
||||||
Pas de cible web Flutter — Android en priorité, iOS préparé.
|
Pas de cible web Flutter — Android en priorité, iOS préparé.
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 0.0.2
|
version: 0.0.4
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.2.0 <4.0.0'
|
sdk: '>=3.2.0 <4.0.0'
|
||||||
|
|||||||
Reference in New Issue
Block a user