import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../providers/installer_mode_provider.dart'; import '../../theme/app_theme.dart'; import '../../theme/etm_theme.dart'; /// Dialog de saisie du PIN installateur. /// Affiche un clavier numérique avec indicateurs de saisie, /// gestion des tentatives échouées et countdown en cas de verrouillage. class InstallerPinDialog extends StatefulWidget { const InstallerPinDialog({super.key}); @override State createState() => _InstallerPinDialogState(); } class _InstallerPinDialogState extends State { String _pin = ''; String? _error; bool _checking = false; Timer? _countdownTimer; int _countdown = 0; @override void dispose() { _countdownTimer?.cancel(); super.dispose(); } void _append(String digit) { if (_checking || _pin.length >= 6) return; setState(() { _pin = _pin + digit; _error = null; }); if (_pin.length >= 4) { _tryUnlock(); } } void _delete() { if (_pin.isEmpty) return; setState(() => _pin = _pin.substring(0, _pin.length - 1)); } Future _tryUnlock() async { final provider = context.read(); if (provider.isLocked) { _startCountdown(provider.lockRemaining.inSeconds); return; } setState(() => _checking = true); final result = await provider.unlock(_pin); if (!mounted) return; switch (result) { case UnlockResult.success: Navigator.of(context).pop(true); break; case UnlockResult.wrongPin: setState(() { _checking = false; _pin = ''; _error = provider.isLocked ? 'PIN incorrect. Veuillez patienter.' : 'PIN incorrect (${provider.failedAttempts} tentative${provider.failedAttempts > 1 ? 's' : ''} restante${(3 - provider.failedAttempts) > 1 ? 's' : ''})'; }); if (provider.isLocked) { _startCountdown(provider.lockRemaining.inSeconds); } break; case UnlockResult.locked: setState(() { _checking = false; _pin = ''; }); _startCountdown(provider.lockRemaining.inSeconds); break; } } void _startCountdown(int seconds) { _countdown = seconds; _countdownTimer?.cancel(); _countdownTimer = Timer.periodic(const Duration(seconds: 1), (t) { if (!mounted) { t.cancel(); return; } setState(() { _countdown--; if (_countdown <= 0) { t.cancel(); _error = null; } else { _error = 'Trop de tentatives. Réessayez dans $_countdown s'; } }); }); } @override Widget build(BuildContext context) { return Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), child: Padding( padding: const EdgeInsets.fromLTRB(24, 28, 24, 24), child: Column( mainAxisSize: MainAxisSize.min, children: [ // ── Icône ──────────────────────────────────────────────────────── Container( width: 52, height: 52, decoration: BoxDecoration( color: ETMTheme.installerBadgeColor.withValues(alpha: 0.12), shape: BoxShape.circle, ), child: const Icon( Icons.build_rounded, color: ETMTheme.installerBadgeColor, size: 26, ), ), const SizedBox(height: 14), const Text( 'Mode Installateur', style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold), ), const SizedBox(height: 4), const Text( 'Entrez votre PIN pour continuer', style: TextStyle(fontSize: 13, color: Color(0xFF6B7280)), ), const SizedBox(height: 20), // ── Indicateurs PIN ────────────────────────────────────────────── Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate(6, (i) { final filled = i < _pin.length; return Container( width: 14, height: 14, margin: const EdgeInsets.symmetric(horizontal: 5), decoration: BoxDecoration( shape: BoxShape.circle, color: filled ? ETMTheme.installerBadgeColor : Colors.grey.shade200, border: Border.all( color: filled ? ETMTheme.installerBadgeColor : Colors.grey.shade400, ), ), ); }), ), if (_checking) ...[ const SizedBox(height: 16), const SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 2.5, color: ETMTheme.installerBadgeColor, ), ), ] else if (_error != null) ...[ const SizedBox(height: 10), Text( _error!, textAlign: TextAlign.center, style: TextStyle( fontSize: 12, color: ETMTheme.errorColor, ), ), ], const SizedBox(height: 20), // ── Clavier numérique ──────────────────────────────────────────── ...[ ['1', '2', '3'], ['4', '5', '6'], ['7', '8', '9'], ['', '0', '⌫'], ].map((row) => Padding( padding: const EdgeInsets.only(bottom: 8), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: row.map((k) { if (k.isEmpty) { return const SizedBox(width: 70, height: 52); } return _KeyButton( label: k, onTap: () { if (k == '⌫') { _delete(); } else { _append(k); } }, isDelete: k == '⌫', disabled: _checking || _countdown > 0, ); }).toList(), ), )), // ── Bouton Annuler ─────────────────────────────────────────────── TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('Annuler'), ), ], ), ), ); } } class _KeyButton extends StatelessWidget { final String label; final VoidCallback onTap; final bool isDelete; final bool disabled; const _KeyButton({ required this.label, required this.onTap, this.isDelete = false, this.disabled = false, }); @override Widget build(BuildContext context) { return GestureDetector( onTap: disabled ? null : onTap, child: Container( width: 70, height: 52, margin: const EdgeInsets.symmetric(horizontal: 6), decoration: BoxDecoration( color: isDelete ? Colors.grey.shade100 : Colors.grey.shade50, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey.shade200), ), child: Center( child: Text( label, style: TextStyle( fontSize: isDelete ? 18 : 22, fontWeight: FontWeight.w500, color: disabled ? Colors.grey.shade400 : AppTheme.textDark, ), ), ), ), ); } }