Initial commit du projet Flutter

This commit is contained in:
2025-12-01 10:56:37 +01:00
commit 8a728a612e
162 changed files with 33799 additions and 0 deletions

946
lib/pages/login.dart Normal file
View File

@@ -0,0 +1,946 @@
// ===== lib/pages/login.dart AVEC SUPPORT ENTREPRISE =====
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:wtpe/widgets/wortis_logo.dart';
import '../main.dart';
import '../services/wortis_api_service.dart';
import 'role_navigator.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage>
with SingleTickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
final _agentIdController = TextEditingController();
final _pinController = TextEditingController();
bool _obscurePin = true;
bool _apiConnected = false;
bool _testingConnection = false;
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
// Couleurs Wortis
static const Color primaryColor = Color(0xFF006699);
static const Color secondaryColor = Color(0xFF0088CC);
static const Color accentColor = Color(0xFFFF6B35);
static const Color backgroundColor = Color(0xFFF8FAFC);
static const Color surfaceColor = Colors.white;
static const Color errorColor = Color(0xFFE53E3E);
static const Color textPrimaryColor = Color(0xFF1A202C);
static const Color textSecondaryColor = Color(0xFF718096);
static const Color borderColor = Color(0xFFE2E8F0);
static const Color enterpriseColor = Color(0xFF8B5CF6);
@override
void initState() {
super.initState();
_setupAnimations();
_testApiConnection();
}
void _setupAnimations() {
_animationController = AnimationController(
duration: Duration(milliseconds: 1500),
vsync: this,
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _animationController,
curve: Interval(0.0, 0.8, curve: Curves.easeOut),
),
);
_slideAnimation = Tween<Offset>(
begin: Offset(0, 0.3),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _animationController,
curve: Interval(0.3, 1.0, curve: Curves.easeOut),
),
);
_animationController.forward();
}
Future<void> _testApiConnection() async {
setState(() {
_testingConnection = true;
});
final isConnected = await AuthApiService.testConnection();
if (mounted) {
setState(() {
_apiConnected = isConnected;
_testingConnection = false;
});
}
}
Future<void> _createTestUser() async {
String? nom = await _showCreateUserDialog();
if (nom == null || nom.isEmpty) return;
final result = await AuthApiService.createUser(nom, '1234');
if (result['success'] == true) {
final agentId = result['generated_agent_id'];
setState(() {
_agentIdController.text = agentId;
_pinController.text = '1234';
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(Icons.check_circle, color: Colors.white),
SizedBox(width: 8),
Expanded(child: Text('Utilisateur créé: $agentId / PIN: 1234')),
],
),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur: ${result['message']}'),
backgroundColor: Colors.orange,
),
);
}
}
Future<String?> _showCreateUserDialog() async {
final nameController = TextEditingController();
return showDialog<String>(
context: context,
builder:
(context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: Row(
children: [
Icon(Icons.person_add, color: primaryColor),
SizedBox(width: 12),
Text('Créer un utilisateur'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: primaryColor.withOpacity(0.3)),
),
child: Row(
children: [
Icon(Icons.info_outline, color: primaryColor, size: 20),
SizedBox(width: 8),
Expanded(
child: Text(
'L\'ID agent sera généré automatiquement (WRT####)',
style: TextStyle(fontSize: 12, color: primaryColor),
),
),
],
),
),
SizedBox(height: 16),
TextField(
controller: nameController,
decoration: InputDecoration(
labelText: 'Nom complet',
hintText: 'Ex: Jean Dupont',
prefixIcon: Icon(Icons.person_outline),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
autofocus: true,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Annuler'),
),
ElevatedButton(
onPressed:
() => Navigator.pop(context, nameController.text.trim()),
style: ElevatedButton.styleFrom(
backgroundColor: primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text('Créer', style: TextStyle(color: Colors.white)),
),
],
),
);
}
@override
void dispose() {
_agentIdController.dispose();
_pinController.dispose();
_animationController.dispose();
super.dispose();
}
void _login() async {
if (_formKey.currentState!.validate()) {
final authController = Provider.of<AuthController>(
context,
listen: false,
);
authController.clearError();
bool success = await authController.login(
_agentIdController.text.trim(),
_pinController.text.trim(),
false,
);
if (success && mounted) {
// Afficher un message de bienvenue personnalisé
final isEnterprise = authController.isEnterpriseMember;
final accountType = isEnterprise ? 'Entreprise' : 'Agent';
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(
isEnterprise ? Icons.business : Icons.person,
color: Colors.white,
size: 20,
),
),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Bienvenue ${authController.agentName}',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
if (isEnterprise &&
authController.enterprise?.nomEntreprise != null) ...[
SizedBox(height: 2),
Text(
authController.enterprise!.nomEntreprise,
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 12,
),
),
],
],
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text(
accountType,
style: TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.w600,
),
),
),
],
),
backgroundColor: isEnterprise ? enterpriseColor : Colors.green,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
duration: Duration(seconds: 2),
margin: EdgeInsets.all(16),
),
);
// Navigation basée sur le rôle
final userRole = authController.role ?? 'agent';
RoleNavigator.navigateByRole(context, userRole);
} else if (mounted) {
final errorMessage =
authController.errorMessage ?? 'Erreur de connexion';
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(Icons.error_outline, color: Colors.white, size: 24),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Connexion échouée',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 2),
Text(errorMessage, style: TextStyle(fontSize: 12)),
],
),
),
],
),
backgroundColor: errorColor,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
duration: Duration(seconds: 4),
margin: EdgeInsets.all(16),
),
);
}
}
}
void _openWifiSettings() async {
try {
const platform = MethodChannel('com.wortis.agent/settings');
await platform.invokeMethod('exitKioskMode');
await platform.invokeMethod('openWifiSettings');
} catch (e) {
print('Erreur ouverture WiFi: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Impossible d\'ouvrir les paramètres WiFi'),
backgroundColor: Colors.orange,
),
);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: backgroundColor,
body: SafeArea(
child: SingleChildScrollView(
physics: BouncingScrollPhysics(),
padding: EdgeInsets.all(24),
child: FadeTransition(
opacity: _fadeAnimation,
child: SlideTransition(
position: _slideAnimation,
child: Column(
children: [
SizedBox(height: 20),
_buildApiStatusIndicator(),
SizedBox(height: 20),
_buildHeader(),
SizedBox(height: 50),
_buildLoginForm(),
SizedBox(height: 30),
_buildHelpSection(),
SizedBox(height: 20),
],
),
),
),
),
),
);
}
Widget _buildApiStatusIndicator() {
return Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color:
_testingConnection
? Colors.orange.withOpacity(0.1)
: _apiConnected
? Colors.green.withOpacity(0.1)
: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(25),
border: Border.all(
color:
_testingConnection
? Colors.orange.withOpacity(0.3)
: _apiConnected
? Colors.green.withOpacity(0.3)
: Colors.red.withOpacity(0.3),
width: 1.5,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (_testingConnection) ...[
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.orange),
),
),
SizedBox(width: 8),
Text(
'Test connexion...',
style: TextStyle(
color: Colors.orange.shade700,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
] else ...[
Container(
padding: EdgeInsets.all(4),
decoration: BoxDecoration(
color:
_apiConnected
? Colors.green.withOpacity(0.2)
: Colors.red.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(
_apiConnected
? Icons.cloud_done_rounded
: Icons.cloud_off_rounded,
size: 16,
color:
_apiConnected ? Colors.green.shade700 : Colors.red.shade700,
),
),
SizedBox(width: 8),
Text(
_apiConnected ? 'API connectée' : 'API déconnectée',
style: TextStyle(
color:
_apiConnected ? Colors.green.shade700 : Colors.red.shade700,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
SizedBox(width: 8),
GestureDetector(
onTap: _testApiConnection,
child: Container(
padding: EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.5),
shape: BoxShape.circle,
),
child: Icon(
Icons.refresh_rounded,
size: 16,
color:
_apiConnected
? Colors.green.shade700
: Colors.red.shade700,
),
),
),
],
],
),
);
}
Widget _buildHeader() {
return Column(
children: [
WortisLogoWidget(size: 100, isWhite: false, withShadow: true),
SizedBox(height: 24),
ShaderMask(
shaderCallback:
(bounds) => LinearGradient(
colors: [primaryColor, secondaryColor],
).createShader(bounds),
child: Text(
'WORTIS AGENT',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 2,
),
),
),
SizedBox(height: 8),
Text(
'Connexion à votre espace',
style: TextStyle(
fontSize: 16,
color: textSecondaryColor,
fontWeight: FontWeight.w400,
),
),
SizedBox(height: 12),
// Badges des types de comptes supportés
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildAccountTypeBadge(
icon: Icons.person,
label: 'Agent',
color: primaryColor,
),
SizedBox(width: 12),
_buildAccountTypeBadge(
icon: Icons.business,
label: 'Entreprise',
color: enterpriseColor,
),
],
),
],
);
}
Widget _buildAccountTypeBadge({
required IconData icon,
required String label,
required Color color,
}) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: color.withOpacity(0.3), width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: color, size: 14),
SizedBox(width: 6),
Text(
label,
style: TextStyle(
color: color,
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
Widget _buildLoginForm() {
return Container(
padding: EdgeInsets.all(28),
decoration: BoxDecoration(
color: surfaceColor,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 20,
offset: Offset(0, 4),
),
],
),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
Icons.login_rounded,
color: primaryColor,
size: 24,
),
),
SizedBox(width: 12),
Text(
'Identifiants',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: textPrimaryColor,
),
),
],
),
SizedBox(height: 24),
_buildTextField(
controller: _agentIdController,
label: 'ID Agent',
hint: 'Entrez votre identifiant',
prefixIcon: Icons.badge_outlined,
validator: (value) {
if (value == null || value.isEmpty) {
return 'L\'ID agent est requis';
}
if (value.length < 3) {
return 'ID agent trop court (min. 3 caractères)';
}
return null;
},
),
SizedBox(height: 20),
_buildTextField(
controller: _pinController,
label: 'Code PIN',
hint: 'Entrez votre code PIN',
prefixIcon: Icons.lock_outline,
obscureText: _obscurePin,
suffixIcon: IconButton(
icon: Icon(
_obscurePin
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
color: textSecondaryColor,
),
onPressed: () {
setState(() {
_obscurePin = !_obscurePin;
});
},
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Le code PIN est requis';
}
if (value.length < 4) {
return 'Le PIN doit contenir au moins 4 chiffres';
}
return null;
},
),
SizedBox(height: 32),
Consumer<AuthController>(
builder: (context, authController, child) {
return _buildLoginButton(authController);
},
),
SizedBox(height: 16),
Container(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: _openWifiSettings,
icon: Icon(Icons.wifi_find_rounded, size: 20),
label: Text('Paramètres WiFi'),
style: OutlinedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 14),
side: BorderSide(color: primaryColor, width: 1.5),
foregroundColor: primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
// Bouton de test (commenté par défaut)
// if (_apiConnected) ...[
// SizedBox(height: 16),
// TextButton.icon(
// onPressed: _createTestUser,
// icon: Icon(Icons.add_circle_outline, size: 18),
// label: Text('Créer utilisateur test'),
// style: TextButton.styleFrom(foregroundColor: primaryColor),
// ),
// ],
],
),
),
);
}
Widget _buildTextField({
required TextEditingController controller,
required String label,
required String hint,
required IconData prefixIcon,
Widget? suffixIcon,
bool obscureText = false,
String? Function(String?)? validator,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: textPrimaryColor,
),
),
SizedBox(height: 8),
TextFormField(
controller: controller,
validator: validator,
obscureText: obscureText,
decoration: InputDecoration(
hintText: hint,
hintStyle: TextStyle(color: textSecondaryColor),
prefixIcon: Icon(prefixIcon, color: primaryColor),
suffixIcon: suffixIcon,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: borderColor),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: borderColor),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: primaryColor, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: errorColor),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: errorColor, width: 2),
),
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 16),
filled: true,
fillColor: backgroundColor.withOpacity(0.3),
),
),
],
);
}
Widget _buildLoginButton(AuthController authController) {
return Container(
height: 54,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [primaryColor, secondaryColor],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: primaryColor.withOpacity(0.4),
blurRadius: 10,
offset: Offset(0, 4),
),
],
),
child: ElevatedButton(
onPressed: authController.isLoading ? null : _login,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child:
authController.isLoading
? SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2.5,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.login_rounded, size: 22),
SizedBox(width: 10),
Text(
'SE CONNECTER',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
),
),
],
),
),
);
}
Widget _buildHelpSection() {
return Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
secondaryColor.withOpacity(0.1),
primaryColor.withOpacity(0.05),
],
),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: secondaryColor.withOpacity(0.3), width: 1.5),
),
child: Column(
children: [
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: primaryColor.withOpacity(0.2),
blurRadius: 10,
offset: Offset(0, 4),
),
],
),
child: Icon(
Icons.help_outline_rounded,
color: primaryColor,
size: 32,
),
),
SizedBox(height: 16),
Text(
'Besoin d\'aide ?',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: textPrimaryColor,
),
),
SizedBox(height: 12),
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.7),
borderRadius: BorderRadius.circular(12),
),
// child: Column(
// children: [
// Row(
// children: [
// Icon(Icons.person, color: primaryColor, size: 16),
// SizedBox(width: 8),
// Expanded(
// child: Text(
// 'Compte Agent - Accès individuel',
// style: TextStyle(fontSize: 13, color: textPrimaryColor),
// ),
// ),
// ],
// ),
// SizedBox(height: 8),
// Row(
// children: [
// Icon(Icons.business, color: enterpriseColor, size: 16),
// SizedBox(width: 8),
// Expanded(
// child: Text(
// 'Compte Entreprise - Solde partagé',
// style: TextStyle(fontSize: 13, color: textPrimaryColor),
// ),
// ),
// ],
// ),
// ],
// ),
),
SizedBox(height: 16),
Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: primaryColor,
borderRadius: BorderRadius.circular(25),
boxShadow: [
BoxShadow(
color: primaryColor.withOpacity(0.3),
blurRadius: 8,
offset: Offset(0, 4),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.phone, color: Colors.white, size: 18),
SizedBox(width: 8),
Text(
'Support: 50 05',
style: TextStyle(
fontSize: 14,
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
);
}
}