/// Progression d'un lavage en cours. class WashProgress { const WashProgress({ required this.percent, required this.phaseLabel, this.phase, this.estimatedEndAt, this.remainingSeconds, }); final int percent; final String phaseLabel; final String? phase; final DateTime? estimatedEndAt; final int? remainingSeconds; String get remainingLabel { if (remainingSeconds == null) return ''; final s = remainingSeconds!; if (s <= 0) return 'Bientôt terminé'; if (s < 60) return '$s s restantes'; final min = (s / 60).ceil(); return '$min min restantes'; } factory WashProgress.fromJson(Map? json) { if (json == null) { return const WashProgress(percent: 0, phaseLabel: 'En cours'); } return WashProgress( percent: json['percent'] as int? ?? 0, phase: json['phase'] as String?, phaseLabel: json['phase_label'] as String? ?? 'En cours', estimatedEndAt: json['estimated_end_at'] != null ? DateTime.tryParse(json['estimated_end_at'] as String) : null, remainingSeconds: json['remaining_seconds'] as int?, ); } /// Calcul local si pas de données API (rafraîchissement chaque seconde). factory WashProgress.compute({ required DateTime? startedAt, required DateTime? estimatedEndAt, required int? durationMinutes, required String? machineType, required String status, }) { if (status == 'pending_start') { return const WashProgress(percent: 0, phaseLabel: 'En attente de démarrage', phase: 'pending'); } final now = DateTime.now(); final end = estimatedEndAt ?? (startedAt != null && durationMinutes != null ? startedAt.add(Duration(minutes: durationMinutes)) : null); if (startedAt == null || end == null) { return const WashProgress(percent: 5, phaseLabel: 'Démarrage…', phase: 'lock'); } final total = end.difference(startedAt).inSeconds.clamp(1, 999999); final elapsed = now.difference(startedAt).inSeconds.clamp(0, total); final percent = ((elapsed / total) * 100).round().clamp(0, 99); final remaining = end.difference(now).inSeconds.clamp(0, total); return WashProgress( percent: percent, phaseLabel: _phaseLabel(percent, machineType), phase: _phase(percent, machineType), estimatedEndAt: end, remainingSeconds: remaining, ); } static String _phase(int percent, String? type) { final isDryer = type?.startsWith('dryer') ?? false; if (percent < 5) return 'lock'; if (isDryer) { if (percent < 15) return 'heat'; if (percent < 85) return 'dry'; return 'finish'; } if (percent < 15) return 'fill'; if (percent < 55) return 'wash'; if (percent < 75) return 'rinse'; if (percent < 90) return 'spin'; return 'finish'; } static String _phaseLabel(int percent, String? type) { switch (_phase(percent, type)) { case 'lock': return 'Verrouillage'; case 'heat': return 'Préchauffage'; case 'dry': return 'Séchage'; case 'fill': return 'Remplissage'; case 'wash': return 'Lavage'; case 'rinse': return 'Rinçage'; case 'spin': return 'Essorage'; case 'finish': return 'Finition'; default: return 'En cours'; } } }