Première grosse version fonctionnel
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
class User {
|
||||
final int id;
|
||||
final String name;
|
||||
final String email;
|
||||
|
||||
User({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.email,
|
||||
});
|
||||
|
||||
factory User.fromJson(Map<String, dynamic> json) {
|
||||
return User(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
email: json['email'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'email': email,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../models/user.dart';
|
||||
import '../services/auth_service.dart';
|
||||
|
||||
class AuthProvider with ChangeNotifier {
|
||||
final AuthService _authService = AuthService();
|
||||
static const _platform = MethodChannel('com.cesizen/wearable');
|
||||
|
||||
User? _user;
|
||||
bool _isLoading = false;
|
||||
String? _errorMessage;
|
||||
|
||||
AuthProvider() {
|
||||
// Écouter les demandes de mise à jour venant de la couche native (montre -> téléphone)
|
||||
_platform.setMethodCallHandler((call) async {
|
||||
if (call.method == "requestStatusUpdate") {
|
||||
_syncWithWear();
|
||||
}
|
||||
});
|
||||
|
||||
// Tenter de restaurer la session au lancement
|
||||
_checkInitialAuth();
|
||||
}
|
||||
|
||||
Future<void> _checkInitialAuth() async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
final result = await _authService.getProfile();
|
||||
if (result['status'] == 'success') {
|
||||
_user = result['user'];
|
||||
_syncWithWear();
|
||||
}
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
User? get user => _user;
|
||||
bool get isLoading => _isLoading;
|
||||
String? get errorMessage => _errorMessage;
|
||||
bool get isAuthenticated => _user != null;
|
||||
|
||||
void _syncWithWear() {
|
||||
try {
|
||||
_platform.invokeMethod('sendAuthStatus', {
|
||||
'status': isAuthenticated ? 'authenticated' : 'unauthenticated',
|
||||
'userName': _user?.name ?? '',
|
||||
});
|
||||
} catch (e) {
|
||||
// Échec silencieux si pas d'appareil Wear OS à proximité
|
||||
debugPrint("Wear OS sync failed: $e");
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> login(String email, String password) async {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
final result = await _authService.login(email, password);
|
||||
|
||||
_isLoading = false;
|
||||
if (result['status'] == 'success') {
|
||||
_user = result['user'];
|
||||
_syncWithWear();
|
||||
notifyListeners();
|
||||
return true;
|
||||
} else {
|
||||
_errorMessage = result['message'];
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> register(String name, String email, String password, String passwordConfirmation) async {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
final result = await _authService.register(name, email, password, passwordConfirmation);
|
||||
|
||||
_isLoading = false;
|
||||
if (result['status'] == 'success') {
|
||||
_user = result['user'];
|
||||
_syncWithWear();
|
||||
notifyListeners();
|
||||
return true;
|
||||
} else {
|
||||
if (result['errors'] is Map) {
|
||||
_errorMessage = (result['errors'] as Map).values.first[0];
|
||||
} else {
|
||||
_errorMessage = result['errors'].toString();
|
||||
}
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
await _authService.logout();
|
||||
_user = null;
|
||||
_syncWithWear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<bool> updateProfile(String name, String email, {String? password}) async {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
final result = await _authService.updateProfile(name, email, password: password);
|
||||
|
||||
_isLoading = false;
|
||||
if (result['status'] == 'success') {
|
||||
_user = result['user'];
|
||||
_syncWithWear();
|
||||
notifyListeners();
|
||||
return true;
|
||||
} else {
|
||||
_errorMessage = result['message'];
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
import 'register_screen.dart';
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
const LoginScreen({super.key});
|
||||
|
||||
@override
|
||||
State<LoginScreen> createState() => _LoginScreenState();
|
||||
}
|
||||
|
||||
class _LoginScreenState extends State<LoginScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _login() async {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
||||
final success = await authProvider.login(
|
||||
_emailController.text,
|
||||
_passwordController.text,
|
||||
);
|
||||
|
||||
if (success) {
|
||||
// Rediriger vers l'accueil ou le dashboard
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Connexion réussie')),
|
||||
);
|
||||
Navigator.of(context).pop(); // Retourne à l'écran précédent (HomeScreen)
|
||||
}
|
||||
} else {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(authProvider.errorMessage ?? 'Échec de la connexion')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Connexion CESIZen')),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
'Bienvenue sur CESIZen',
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Email',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.email),
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) return 'Veuillez entrer votre email';
|
||||
if (!value.contains('@')) return 'Email invalide';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Mot de passe',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.lock),
|
||||
),
|
||||
obscureText: true,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) return 'Veuillez entrer votre mot de passe';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Consumer<AuthProvider>(
|
||||
builder: (context, auth, child) {
|
||||
return auth.isLoading
|
||||
? const CircularProgressIndicator()
|
||||
: ElevatedButton(
|
||||
onPressed: _login,
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(50),
|
||||
),
|
||||
child: const Text('Se connecter'),
|
||||
);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const RegisterScreen()),
|
||||
);
|
||||
},
|
||||
child: const Text("Pas encore de compte ? S'inscrire"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
import '../../tracker/providers/emotion_provider.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class ProfileScreen extends StatefulWidget {
|
||||
const ProfileScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ProfileScreen> createState() => _ProfileScreenState();
|
||||
}
|
||||
|
||||
class _ProfileScreenState extends State<ProfileScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late TextEditingController _nameController;
|
||||
late TextEditingController _emailController;
|
||||
late TextEditingController _passwordController;
|
||||
late String _currentEmail;
|
||||
bool _isEditing = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final user = Provider.of<AuthProvider>(context, listen: false).user;
|
||||
_currentEmail = user?.email ?? '';
|
||||
_nameController = TextEditingController(text: user?.name);
|
||||
_emailController = TextEditingController(text: _currentEmail);
|
||||
_passwordController = TextEditingController();
|
||||
|
||||
_emailController.addListener(() {
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
|
||||
// Charger les émotions pour le journal
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
Provider.of<EmotionProvider>(context, listen: false).loadEmotions();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _submitUpdate() async {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
||||
|
||||
final String newName = _nameController.text.trim();
|
||||
final String newEmail = _emailController.text.trim();
|
||||
final String? newPassword = _passwordController.text.isNotEmpty ? _passwordController.text : null;
|
||||
|
||||
final success = await authProvider.updateProfile(
|
||||
newName,
|
||||
newEmail,
|
||||
password: newPassword,
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
if (success) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Profil mis à jour avec succès')),
|
||||
);
|
||||
setState(() {
|
||||
_isEditing = false;
|
||||
_currentEmail = newEmail;
|
||||
_passwordController.clear();
|
||||
});
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(authProvider.errorMessage ?? 'Erreur lors de la mise à jour')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final authProvider = Provider.of<AuthProvider>(context);
|
||||
final emotionProvider = Provider.of<EmotionProvider>(context);
|
||||
final user = authProvider.user;
|
||||
final bool emailChanged = _emailController.text.trim() != _currentEmail.trim();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Mon Espace Santé'),
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(_isEditing ? Icons.close : Icons.edit),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isEditing = !_isEditing;
|
||||
if (!_isEditing) {
|
||||
_nameController.text = user?.name ?? '';
|
||||
_emailController.text = user?.email ?? '';
|
||||
_passwordController.clear();
|
||||
}
|
||||
});
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
// Section Profil
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
const CircleAvatar(
|
||||
radius: 50,
|
||||
backgroundColor: Color(0xFF000080),
|
||||
child: Icon(Icons.person, size: 50, color: Colors.white),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
enabled: _isEditing,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nom',
|
||||
prefixIcon: Icon(Icons.person_outline),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (value) => (value == null || value.trim().isEmpty) ? 'Veuillez entrer un nom' : null,
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
enabled: _isEditing,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Email',
|
||||
prefixIcon: Icon(Icons.email_outlined),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (value) {
|
||||
final email = value?.trim() ?? '';
|
||||
if (email.isEmpty) return 'Veuillez entrer un email';
|
||||
if (!email.contains('@')) return 'Email invalide';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
if (_isEditing) ...[
|
||||
const SizedBox(height: 15),
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: emailChanged ? 'Mot de passe requis' : 'Nouveau mot de passe',
|
||||
prefixIcon: const Icon(Icons.lock_outline),
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
onPressed: authProvider.isLoading ? null : _submitUpdate,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF000080),
|
||||
foregroundColor: Colors.white,
|
||||
minimumSize: const Size.fromHeight(50),
|
||||
),
|
||||
child: const Text('ENREGISTRER'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Section Journal Émotionnel (Le "Journal Santé")
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
color: Colors.grey.shade50,
|
||||
child: const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Journal Émotionnel',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Color(0xFF000080)),
|
||||
),
|
||||
Text('Historique de vos saisies (Mobile & Montre)', style: TextStyle(color: Colors.grey, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
if (emotionProvider.isLoading)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(20.0),
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
else if (emotionProvider.entries.isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(40.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(Icons.history, size: 48, color: Colors.grey),
|
||||
SizedBox(height: 10),
|
||||
Text('Aucun historique pour le moment.', style: TextStyle(color: Colors.grey)),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: emotionProvider.entries.length,
|
||||
separatorBuilder: (context, index) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final entry = emotionProvider.entries[index];
|
||||
final dateStr = DateFormat('dd/MM/yyyy HH:mm').format(entry.createdAt);
|
||||
return ListTile(
|
||||
leading: _getMoodEmoji(entry.emotion),
|
||||
title: Text(entry.emotion, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: Text(entry.note ?? 'Saisie rapide'),
|
||||
trailing: Text(dateStr, style: const TextStyle(fontSize: 11, color: Colors.grey)),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 100), // Espace pour le scroll
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getMoodEmoji(String mood) {
|
||||
String emoji = "😐";
|
||||
Color color = Colors.grey;
|
||||
switch (mood) {
|
||||
case 'Très bien':
|
||||
emoji = "😊";
|
||||
color = const Color(0xFF4CAF50);
|
||||
break;
|
||||
case 'Bien':
|
||||
emoji = "🙂";
|
||||
color = const Color(0xFF8BC34A);
|
||||
break;
|
||||
case 'Neutre':
|
||||
emoji = "😐";
|
||||
color = const Color(0xFFFFC107);
|
||||
break;
|
||||
case 'Pas top':
|
||||
emoji = "🙁";
|
||||
color = const Color(0xFFFF9800);
|
||||
break;
|
||||
case 'Stressé':
|
||||
emoji = "😫";
|
||||
color = const Color(0xFFF44336);
|
||||
break;
|
||||
}
|
||||
return CircleAvatar(
|
||||
backgroundColor: color.withOpacity(0.1),
|
||||
child: Text(emoji, style: const TextStyle(fontSize: 20)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
|
||||
class RegisterScreen extends StatefulWidget {
|
||||
const RegisterScreen({super.key});
|
||||
|
||||
@override
|
||||
State<RegisterScreen> createState() => _RegisterScreenState();
|
||||
}
|
||||
|
||||
class _RegisterScreenState extends State<RegisterScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _nameController = TextEditingController();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
final _confirmPasswordController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
_confirmPasswordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _register() async {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
||||
final success = await authProvider.register(
|
||||
_nameController.text,
|
||||
_emailController.text,
|
||||
_passwordController.text,
|
||||
_confirmPasswordController.text,
|
||||
);
|
||||
|
||||
if (success) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Compte créé avec succès !')),
|
||||
);
|
||||
// On vide la pile et on retourne au menu principal (HomeScreen)
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
}
|
||||
} else {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(authProvider.errorMessage ?? 'Échec de l\'inscription')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Inscription CESIZen')),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 20),
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nom complet',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.person),
|
||||
),
|
||||
validator: (value) => value == null || value.isEmpty ? 'Entrez votre nom' : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Email',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.email),
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (value) => value == null || !value.contains('@') ? 'Email invalide' : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Mot de passe',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.lock),
|
||||
),
|
||||
obscureText: true,
|
||||
validator: (value) => value == null || value.length < 8 ? '8 caractères minimum' : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _confirmPasswordController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Confirmer le mot de passe',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.lock_outline),
|
||||
),
|
||||
obscureText: true,
|
||||
validator: (value) {
|
||||
if (value != _passwordController.text) return 'Les mots de passe ne correspondent pas';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Consumer<AuthProvider>(
|
||||
builder: (context, auth, child) {
|
||||
return auth.isLoading
|
||||
? const CircularProgressIndicator()
|
||||
: ElevatedButton(
|
||||
onPressed: _register,
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(50),
|
||||
),
|
||||
child: const Text('S\'inscrire'),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import '../../../core/network/dio_client.dart';
|
||||
import '../models/user.dart';
|
||||
|
||||
class AuthService {
|
||||
final Dio _dio = DioClient().dio;
|
||||
final FlutterSecureStorage _storage = const FlutterSecureStorage();
|
||||
|
||||
Future<Map<String, dynamic>> login(String email, String password) async {
|
||||
try {
|
||||
final response = await _dio.post('/login', data: {
|
||||
'email': email,
|
||||
'password': password,
|
||||
});
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
String token = response.data['token'];
|
||||
await _storage.write(key: 'token', value: token);
|
||||
return {
|
||||
'status': 'success',
|
||||
'user': User.fromJson(response.data['user']),
|
||||
};
|
||||
}
|
||||
return {'status': 'error', 'message': 'Erreur inconnue'};
|
||||
} on DioException catch (e) {
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': e.response?.data['message'] ?? 'Erreur de connexion'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> register(String name, String email, String password, String passwordConfirmation) async {
|
||||
try {
|
||||
final response = await _dio.post('/register', data: {
|
||||
'name': name,
|
||||
'email': email,
|
||||
'password': password,
|
||||
'password_confirmation': passwordConfirmation,
|
||||
});
|
||||
|
||||
if (response.statusCode == 201) {
|
||||
String token = response.data['token'];
|
||||
await _storage.write(key: 'token', value: token);
|
||||
return {
|
||||
'status': 'success',
|
||||
'user': User.fromJson(response.data['user']),
|
||||
};
|
||||
}
|
||||
return {'status': 'error', 'message': 'Erreur inconnue'};
|
||||
} on DioException catch (e) {
|
||||
return {
|
||||
'status': 'error',
|
||||
'errors': e.response?.data['errors'] ?? 'Erreur lors de l\'inscription'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> updateProfile(String name, String email, {String? password}) async {
|
||||
try {
|
||||
String? token = await getToken();
|
||||
|
||||
final Map<String, dynamic> data = {
|
||||
'name': name,
|
||||
'email': email,
|
||||
};
|
||||
|
||||
if (password != null && password.isNotEmpty) {
|
||||
data['current_password'] = password;
|
||||
data['password'] = password;
|
||||
data['password_confirmation'] = password;
|
||||
}
|
||||
|
||||
final response = await _dio.post(
|
||||
'/update-profile',
|
||||
data: data,
|
||||
options: Options(
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return {
|
||||
'status': 'success',
|
||||
'user': User.fromJson(response.data['user']),
|
||||
};
|
||||
}
|
||||
return {'status': 'error', 'message': 'Erreur lors de la mise à jour'};
|
||||
} on DioException catch (e) {
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': e.response?.data['message'] ?? 'Erreur lors de la modification du profil'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getProfile() async {
|
||||
try {
|
||||
String? token = await getToken();
|
||||
if (token == null) return {'status': 'error', 'message': 'No token'};
|
||||
|
||||
final response = await _dio.get('/profile', options: Options(
|
||||
headers: {'Authorization': 'Bearer $token'}
|
||||
));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return {
|
||||
'status': 'success',
|
||||
'user': User.fromJson(response.data),
|
||||
};
|
||||
}
|
||||
return {'status': 'error', 'message': 'Session expirée'};
|
||||
} catch (e) {
|
||||
return {'status': 'error', 'message': e.toString()};
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
await _storage.delete(key: 'token');
|
||||
}
|
||||
|
||||
Future<String?> getToken() async {
|
||||
return await _storage.read(key: 'token');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
class Question {
|
||||
final String text;
|
||||
final List<Option> options;
|
||||
|
||||
Question({required this.text, required this.options});
|
||||
}
|
||||
|
||||
class Option {
|
||||
final String text;
|
||||
final int points;
|
||||
|
||||
Option({required this.text, required this.points});
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/question.dart';
|
||||
import '../services/diagnostic_service.dart';
|
||||
|
||||
class DiagnosticProvider with ChangeNotifier {
|
||||
final DiagnosticService _service = DiagnosticService();
|
||||
List<Question> _questions = [];
|
||||
int _currentQuestionIndex = 0;
|
||||
int _totalScore = 0;
|
||||
bool _isFinished = false;
|
||||
bool _isLoading = true;
|
||||
|
||||
List<Question> get questions => _questions;
|
||||
int get currentQuestionIndex => _currentQuestionIndex;
|
||||
int get totalScore => _totalScore;
|
||||
bool get isFinished => _isFinished;
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
DiagnosticProvider() {
|
||||
loadQuestions();
|
||||
}
|
||||
|
||||
Future<void> loadQuestions() async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
_questions = await _service.fetchStressEvents();
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void answerQuestion(int points) {
|
||||
_totalScore += points;
|
||||
if (_currentQuestionIndex < _questions.length - 1) {
|
||||
_currentQuestionIndex++;
|
||||
} else {
|
||||
_isFinished = true;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void reset() {
|
||||
_currentQuestionIndex = 0;
|
||||
_totalScore = 0;
|
||||
_isFinished = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
String get stressLevel {
|
||||
if (_totalScore < 150) return "Risque Faible";
|
||||
if (_totalScore < 300) return "Risque Modéré (50%)";
|
||||
return "Risque Élevé (80%)";
|
||||
}
|
||||
|
||||
String get advice {
|
||||
if (_totalScore < 150) {
|
||||
return "Votre niveau de changement de vie est gérable. Continuez à maintenir votre équilibre actuel.";
|
||||
}
|
||||
if (_totalScore < 300) {
|
||||
return "Vous avez vécu beaucoup de changements. Il y a un risque modéré que cela affecte votre santé. Prenez du temps pour vous reposer.";
|
||||
}
|
||||
return "Attention : Votre score est très élevé. L'accumulation de stress lié à ces changements majeurs pourrait sérieusement impacter votre santé. Il est recommandé de consulter un professionnel.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/diagnostic_provider.dart';
|
||||
|
||||
class DiagnosticScreen extends StatelessWidget {
|
||||
const DiagnosticScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider(
|
||||
create: (_) => DiagnosticProvider(),
|
||||
child: const _DiagnosticView(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DiagnosticView extends StatelessWidget {
|
||||
const _DiagnosticView();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = Provider.of<DiagnosticProvider>(context);
|
||||
|
||||
if (provider.isLoading) {
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
if (provider.questions.isEmpty) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Diagnostic')),
|
||||
body: const Center(child: Text('Aucun événement trouvé.')),
|
||||
);
|
||||
}
|
||||
|
||||
final question = provider.questions[provider.currentQuestionIndex];
|
||||
|
||||
if (provider.isFinished) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Résultat du Diagnostic')),
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('Votre niveau de stress est :', style: TextStyle(fontSize: 18)),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
provider.stressLevel,
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _getColorForLevel(provider.stressLevel),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
provider.advice,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 16, fontStyle: FontStyle.italic),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Retour à l\'accueil'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Question ${provider.currentQuestionIndex + 1}/${provider.questions.length}'),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
LinearProgressIndicator(
|
||||
value: (provider.currentQuestionIndex + 1) / provider.questions.length,
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
Text(
|
||||
question.text,
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
...question.options.map((option) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12.0),
|
||||
child: ElevatedButton(
|
||||
onPressed: () => provider.answerQuestion(option.points),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
child: Text(option.text),
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getColorForLevel(String level) {
|
||||
switch (level) {
|
||||
case "Faible": return Colors.green;
|
||||
case "Modéré": return Colors.orange;
|
||||
case "Élevé": return Colors.red;
|
||||
case "Très élevé": return Colors.red.shade900;
|
||||
default: return Colors.black;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../../core/network/dio_client.dart';
|
||||
import '../models/question.dart';
|
||||
|
||||
class DiagnosticService {
|
||||
final Dio _dio = DioClient().dio;
|
||||
|
||||
Future<List<Question>> fetchStressEvents() async {
|
||||
try {
|
||||
final response = await _dio.get('/stress-events');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
List<dynamic> data = response.data;
|
||||
return data.map((item) {
|
||||
return Question(
|
||||
text: "Avez-vous vécu : ${item['event_name']} ?",
|
||||
options: [
|
||||
Option(text: "Oui", points: int.parse(item['points'].toString())),
|
||||
Option(text: "Non", points: 0),
|
||||
],
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
return [];
|
||||
} catch (e) {
|
||||
print("Erreur lors de la récupération des événements : $e");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BreathingMode {
|
||||
final String name;
|
||||
final String description;
|
||||
final int inhale;
|
||||
final int hold;
|
||||
final int exhale;
|
||||
final Color color;
|
||||
|
||||
BreathingMode({
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.inhale,
|
||||
required this.hold,
|
||||
required this.exhale,
|
||||
required this.color,
|
||||
});
|
||||
}
|
||||
|
||||
class BreathingScreen extends StatefulWidget {
|
||||
const BreathingScreen({super.key});
|
||||
|
||||
@override
|
||||
State<BreathingScreen> createState() => _BreathingScreenState();
|
||||
}
|
||||
|
||||
class _BreathingScreenState extends State<BreathingScreen> with TickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
|
||||
final List<BreathingMode> _modes = [
|
||||
BreathingMode(
|
||||
name: "7-4-8",
|
||||
description: "Sommeil & Détente profonde",
|
||||
inhale: 7,
|
||||
hold: 4,
|
||||
exhale: 8,
|
||||
color: Colors.indigo,
|
||||
),
|
||||
BreathingMode(
|
||||
name: "5-5",
|
||||
description: "Cohérence Cardiaque classique",
|
||||
inhale: 5,
|
||||
hold: 0,
|
||||
exhale: 5,
|
||||
color: Colors.teal,
|
||||
),
|
||||
BreathingMode(
|
||||
name: "4-6",
|
||||
description: "Réduction rapide du stress",
|
||||
inhale: 4,
|
||||
hold: 0,
|
||||
exhale: 6,
|
||||
color: Colors.orange,
|
||||
),
|
||||
];
|
||||
|
||||
late BreathingMode _selectedMode;
|
||||
bool _isStarted = false;
|
||||
String _actionText = "Prêt ?";
|
||||
int _secondsRemaining = 0;
|
||||
Timer? _timer;
|
||||
int _phaseSecondsRemaining = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedMode = _modes[1]; // 5-5 par défaut
|
||||
_setupController();
|
||||
}
|
||||
|
||||
void _setupController() {
|
||||
_controller = AnimationController(vsync: this);
|
||||
_animation = Tween<double>(begin: 0.6, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _runExercise() async {
|
||||
while (_isStarted && _secondsRemaining > 0) {
|
||||
// Phase 1 : Inspiration
|
||||
if (!_isStarted) break;
|
||||
setState(() => _actionText = "Inspirez");
|
||||
_controller.duration = Duration(seconds: _selectedMode.inhale);
|
||||
_controller.forward();
|
||||
await _waitForPhase(_selectedMode.inhale);
|
||||
|
||||
// Phase 2 : Apnée (Hold)
|
||||
if (!_isStarted) break;
|
||||
if (_selectedMode.hold > 0) {
|
||||
setState(() => _actionText = "Bloquez");
|
||||
await _waitForPhase(_selectedMode.hold);
|
||||
}
|
||||
|
||||
// Phase 3 : Expiration
|
||||
if (!_isStarted) break;
|
||||
setState(() => _actionText = "Expirez");
|
||||
_controller.duration = Duration(seconds: _selectedMode.exhale);
|
||||
_controller.reverse();
|
||||
await _waitForPhase(_selectedMode.exhale);
|
||||
}
|
||||
if (_isStarted) _stopExercise();
|
||||
}
|
||||
|
||||
Future<void> _waitForPhase(int seconds) async {
|
||||
_phaseSecondsRemaining = seconds;
|
||||
for (int i = 0; i < seconds; i++) {
|
||||
if (!_isStarted) return;
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
if (mounted) setState(() => _phaseSecondsRemaining--);
|
||||
}
|
||||
}
|
||||
|
||||
void _startExercise() {
|
||||
setState(() {
|
||||
_isStarted = true;
|
||||
_secondsRemaining = 300; // 5 minutes
|
||||
});
|
||||
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (_secondsRemaining > 0) {
|
||||
setState(() => _secondsRemaining--);
|
||||
} else {
|
||||
_stopExercise();
|
||||
}
|
||||
});
|
||||
|
||||
_runExercise();
|
||||
}
|
||||
|
||||
void _stopExercise() {
|
||||
_timer?.cancel();
|
||||
_controller.stop();
|
||||
_controller.reset();
|
||||
setState(() {
|
||||
_isStarted = false;
|
||||
_actionText = "Prêt ?";
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String _formatTime(int seconds) {
|
||||
int mins = seconds ~/ 60;
|
||||
int secs = seconds % 60;
|
||||
return "$mins:${secs.toString().padLeft(2, '0')}";
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: const Text('Respiration Guidée', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.white,
|
||||
),
|
||||
body: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
children: [
|
||||
if (!_isStarted) ...[
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
"Choisissez votre exercice",
|
||||
style: TextStyle(fontSize: 18, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
..._modes.map((mode) => _buildModeCard(mode)).toList(),
|
||||
] else ...[
|
||||
const SizedBox(height: 40),
|
||||
Text(
|
||||
_formatTime(_secondsRemaining),
|
||||
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold, letterSpacing: 2),
|
||||
),
|
||||
const Spacer(),
|
||||
_buildAnimatedCircle(),
|
||||
const SizedBox(height: 20),
|
||||
if (_phaseSecondsRemaining > 0)
|
||||
Text(
|
||||
"$_phaseSecondsRemaining",
|
||||
style: TextStyle(fontSize: 40, fontWeight: FontWeight.w300, color: _selectedMode.color),
|
||||
),
|
||||
const Spacer(),
|
||||
ElevatedButton(
|
||||
onPressed: _stopExercise,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red.shade50,
|
||||
foregroundColor: Colors.red,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 15),
|
||||
),
|
||||
child: const Text("ARRÊTER LA SESSION", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildModeCard(BreathingMode mode) {
|
||||
final isSelected = _selectedMode == mode;
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _selectedMode = mode),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? mode.color.withOpacity(0.05) : Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: isSelected ? mode.color : Colors.grey.shade200,
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: mode.color.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.air, color: mode.color),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(mode.name, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
Text(mode.description, style: TextStyle(color: Colors.grey.shade600, fontSize: 13)),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isSelected)
|
||||
ElevatedButton(
|
||||
onPressed: _startExercise,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: mode.color,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
child: const Text("GO"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAnimatedCircle() {
|
||||
return ScaleTransition(
|
||||
scale: _animation,
|
||||
child: Container(
|
||||
width: 250,
|
||||
height: 250,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
colors: [
|
||||
_selectedMode.color.withOpacity(0.2),
|
||||
_selectedMode.color.withOpacity(0.05),
|
||||
],
|
||||
),
|
||||
border: Border.all(color: _selectedMode.color.withOpacity(0.3), width: 8),
|
||||
),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Logo de méditation en fond
|
||||
Opacity(
|
||||
opacity: 0.1,
|
||||
child: Image.asset(
|
||||
'assets/images/CesiZen logo.png',
|
||||
width: 150,
|
||||
),
|
||||
),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
_actionText,
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _selectedMode.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
class Activity {
|
||||
final int id;
|
||||
final String title;
|
||||
final String description;
|
||||
final String category; // 'Méditation', 'Musique', 'Respiration', etc.
|
||||
final String? imageUrl;
|
||||
final String? videoUrl;
|
||||
|
||||
Activity({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.category,
|
||||
this.imageUrl,
|
||||
this.videoUrl,
|
||||
});
|
||||
|
||||
factory Activity.fromJson(Map<String, dynamic> json) {
|
||||
return Activity(
|
||||
id: json['id'],
|
||||
title: json['title'] ?? '',
|
||||
description: json['description'] ?? '',
|
||||
category: json['category'] ?? 'Détente',
|
||||
imageUrl: json['image_url'],
|
||||
videoUrl: json['video_url'],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/activity.dart';
|
||||
import '../services/relaxation_service.dart';
|
||||
|
||||
class RelaxationProvider with ChangeNotifier {
|
||||
final RelaxationService _service = RelaxationService();
|
||||
List<Activity> _activities = [];
|
||||
bool _isLoading = false;
|
||||
String _selectedCategory = 'Toutes';
|
||||
|
||||
List<Activity> get activities {
|
||||
if (_selectedCategory == 'Toutes') return _activities;
|
||||
return _activities.where((a) => a.category == _selectedCategory).toList();
|
||||
}
|
||||
|
||||
bool get isLoading => _isLoading;
|
||||
String get selectedCategory => _selectedCategory;
|
||||
|
||||
Future<void> loadActivities() async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
_activities = await _service.fetchActivities();
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setCategory(String category) {
|
||||
_selectedCategory = category;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/relaxation_provider.dart';
|
||||
|
||||
class RelaxationScreen extends StatefulWidget {
|
||||
const RelaxationScreen({super.key});
|
||||
|
||||
@override
|
||||
State<RelaxationScreen> createState() => _RelaxationScreenState();
|
||||
}
|
||||
|
||||
class _RelaxationScreenState extends State<RelaxationScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Future.microtask(() =>
|
||||
Provider.of<RelaxationProvider>(context, listen: false).loadActivities());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = Provider.of<RelaxationProvider>(context);
|
||||
final categories = ['Toutes', 'Méditation', 'Musique', 'Lecture', 'Sport'];
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Espace Détente'),
|
||||
elevation: 0,
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Barre de catégories
|
||||
Container(
|
||||
height: 60,
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: categories.length,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemBuilder: (context, index) {
|
||||
final cat = categories[index];
|
||||
final isSelected = provider.selectedCategory == cat;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
child: FilterChip(
|
||||
label: Text(cat),
|
||||
selected: isSelected,
|
||||
onSelected: (_) => provider.setCategory(cat),
|
||||
selectedColor: const Color(0xFF000080).withOpacity(0.2),
|
||||
checkmarkColor: const Color(0xFF000080),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
// Liste des activités
|
||||
Expanded(
|
||||
child: provider.isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: provider.activities.isEmpty
|
||||
? const Center(child: Text('Aucune activité trouvée.'))
|
||||
: ListView.builder(
|
||||
itemCount: provider.activities.length,
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemBuilder: (context, index) {
|
||||
final activity = provider.activities[index];
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (activity.imageUrl != null)
|
||||
Image.network(
|
||||
activity.imageUrl!,
|
||||
height: 150,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => Container(
|
||||
height: 150,
|
||||
color: Colors.grey.shade200,
|
||||
child: const Icon(Icons.image_not_supported),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
activity.category,
|
||||
style: TextStyle(
|
||||
color: Colors.orange.shade700,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
if (activity.videoUrl != null)
|
||||
const Icon(Icons.play_circle_fill, color: Color(0xFF000080)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
activity.title,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
activity.description,
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../../core/network/dio_client.dart';
|
||||
import '../models/activity.dart';
|
||||
|
||||
class RelaxationService {
|
||||
final Dio _dio = DioClient().dio;
|
||||
|
||||
Future<List<Activity>> fetchActivities() async {
|
||||
try {
|
||||
final response = await _dio.get('/activities');
|
||||
if (response.statusCode == 200) {
|
||||
List<dynamic> data = response.data;
|
||||
return data.map((item) => Activity.fromJson(item)).toList();
|
||||
}
|
||||
return [];
|
||||
} catch (e) {
|
||||
print("Erreur fetchActivities: $e");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
class EmotionEntry {
|
||||
final int? id;
|
||||
final String emotion; // ex: 'Heureux', 'Triste', 'Stressé'
|
||||
final String? note;
|
||||
final DateTime createdAt;
|
||||
|
||||
EmotionEntry({
|
||||
this.id,
|
||||
required this.emotion,
|
||||
this.note,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory EmotionEntry.fromJson(Map<String, dynamic> json) {
|
||||
return EmotionEntry(
|
||||
id: json['id'],
|
||||
emotion: json['emotion_name'] ?? json['emotion'] ?? '',
|
||||
note: json['note'],
|
||||
createdAt: DateTime.parse(json['created_at']),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'emotion_name': emotion,
|
||||
'note': note,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/emotion_entry.dart';
|
||||
import '../services/emotion_service.dart';
|
||||
|
||||
class EmotionProvider with ChangeNotifier {
|
||||
final EmotionService _service = EmotionService();
|
||||
List<EmotionEntry> _entries = [];
|
||||
bool _isLoading = false;
|
||||
|
||||
List<EmotionEntry> get entries => _entries;
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
Future<void> loadEmotions() async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
_entries = await _service.fetchEmotions();
|
||||
// Trier par date décroissante (plus récent en haut)
|
||||
_entries.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<bool> addEmotion(String emotion, String note) async {
|
||||
final success = await _service.addEmotion(emotion, note);
|
||||
if (success) {
|
||||
await loadEmotions();
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
Future<bool> deleteEmotion(int id) async {
|
||||
final success = await _service.deleteEmotion(id);
|
||||
if (success) {
|
||||
_entries.removeWhere((entry) => entry.id == id);
|
||||
notifyListeners();
|
||||
}
|
||||
return success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/emotion_provider.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class TrackerScreen extends StatefulWidget {
|
||||
const TrackerScreen({super.key});
|
||||
|
||||
@override
|
||||
State<TrackerScreen> createState() => _TrackerScreenState();
|
||||
}
|
||||
|
||||
class _TrackerScreenState extends State<TrackerScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Future.microtask(() =>
|
||||
Provider.of<EmotionProvider>(context, listen: false).loadEmotions());
|
||||
}
|
||||
|
||||
void _showAddEmotionDialog() {
|
||||
String selectedEmotion = 'Bien';
|
||||
final noteController = TextEditingController();
|
||||
final emotions = ['Très bien', 'Bien', 'Neutre', 'Pas top', 'Stressé'];
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setDialogState) => AlertDialog(
|
||||
title: const Text('Comment vous sentez-vous ?'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
DropdownButton<String>(
|
||||
value: selectedEmotion,
|
||||
isExpanded: true,
|
||||
items: emotions.map((String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (newValue) {
|
||||
setDialogState(() => selectedEmotion = newValue!);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextField(
|
||||
controller: noteController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Note (optionnel)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('ANNULER'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final success = await Provider.of<EmotionProvider>(context, listen: false)
|
||||
.addEmotion(selectedEmotion, noteController.text);
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
if (!success) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Erreur lors de l\'ajout')),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Text('ENREGISTRER'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = Provider.of<EmotionProvider>(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Journal des Émotions')),
|
||||
body: provider.isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: provider.entries.isEmpty
|
||||
? const Center(child: Text('Aucune entrée pour le moment.'))
|
||||
: ListView.builder(
|
||||
itemCount: provider.entries.length,
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemBuilder: (context, index) {
|
||||
final entry = provider.entries[index];
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
||||
child: ListTile(
|
||||
leading: _getEmotionIcon(entry.emotion),
|
||||
title: Text(
|
||||
entry.emotion,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (entry.note != null && entry.note!.isNotEmpty)
|
||||
Text(entry.note!),
|
||||
Text(
|
||||
DateFormat('dd/MM/yyyy HH:mm').format(entry.createdAt),
|
||||
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete_outline, color: Colors.red),
|
||||
onPressed: () => provider.deleteEmotion(entry.id!),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _showAddEmotionDialog,
|
||||
backgroundColor: const Color(0xFF000080),
|
||||
child: const Icon(Icons.add, color: Colors.white),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getEmotionIcon(String emotion) {
|
||||
IconData icon;
|
||||
Color color;
|
||||
switch (emotion) {
|
||||
case 'Très bien':
|
||||
icon = Icons.sentiment_very_satisfied;
|
||||
color = const Color(0xFF4CAF50);
|
||||
break;
|
||||
case 'Bien':
|
||||
icon = Icons.sentiment_satisfied;
|
||||
color = const Color(0xFF8BC34A);
|
||||
break;
|
||||
case 'Neutre':
|
||||
icon = Icons.sentiment_neutral;
|
||||
color = const Color(0xFFFFC107);
|
||||
break;
|
||||
case 'Pas top':
|
||||
icon = Icons.sentiment_dissatisfied;
|
||||
color = const Color(0xFFFF9800);
|
||||
break;
|
||||
case 'Stressé':
|
||||
icon = Icons.sentiment_very_dissatisfied;
|
||||
color = const Color(0xFFF44336);
|
||||
break;
|
||||
default:
|
||||
icon = Icons.sentiment_neutral;
|
||||
color = Colors.grey;
|
||||
}
|
||||
return CircleAvatar(
|
||||
backgroundColor: color.withOpacity(0.1),
|
||||
child: Icon(icon, color: color),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../../core/network/dio_client.dart';
|
||||
import '../../auth/services/auth_service.dart';
|
||||
import '../models/emotion_entry.dart';
|
||||
|
||||
class EmotionService {
|
||||
final Dio _dio = DioClient().dio;
|
||||
final AuthService _authService = AuthService();
|
||||
|
||||
Future<List<EmotionEntry>> fetchEmotions() async {
|
||||
try {
|
||||
String? token = await _authService.getToken();
|
||||
final response = await _dio.get(
|
||||
'/emotions',
|
||||
options: Options(headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Accept': 'application/json',
|
||||
}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
List<dynamic> data = response.data;
|
||||
return data.map((item) => EmotionEntry.fromJson(item)).toList();
|
||||
}
|
||||
return [];
|
||||
} catch (e) {
|
||||
print("Erreur fetchEmotions: $e");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> addEmotion(String emotion, String note) async {
|
||||
try {
|
||||
String? token = await _authService.getToken();
|
||||
final response = await _dio.post(
|
||||
'/emotions',
|
||||
data: {
|
||||
'emotion_name': emotion,
|
||||
'note': note,
|
||||
},
|
||||
options: Options(headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Accept': 'application/json',
|
||||
}),
|
||||
);
|
||||
return response.statusCode == 201 || response.statusCode == 200;
|
||||
} catch (e) {
|
||||
print("Erreur addEmotion: $e");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> deleteEmotion(int id) async {
|
||||
try {
|
||||
String? token = await _authService.getToken();
|
||||
final response = await _dio.delete(
|
||||
'/emotions/$id',
|
||||
options: Options(headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Accept': 'application/json',
|
||||
}),
|
||||
);
|
||||
return response.statusCode == 200 || response.statusCode == 204;
|
||||
} catch (e) {
|
||||
print("Erreur deleteEmotion: $e");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../tracker/providers/emotion_provider.dart';
|
||||
|
||||
class WearSyncService {
|
||||
static const _platform = MethodChannel('com.cesizen/wearable');
|
||||
|
||||
static void initialize(BuildContext context) {
|
||||
_platform.setMethodCallHandler((call) async {
|
||||
switch (call.method) {
|
||||
case "requestStatusUpdate":
|
||||
// On peut forcer un rafraîchissement des données ici si besoin
|
||||
break;
|
||||
case "saveMoodFromWear":
|
||||
final String mood = call.arguments['mood'];
|
||||
print("DEBUG FLUTTER: Message saveMoodFromWear reçu pour '$mood'");
|
||||
if (mood.isNotEmpty) {
|
||||
// UTILISATION DU PROVIDER : C'est la clé pour que le token soit inclus
|
||||
try {
|
||||
final emotionProvider = Provider.of<EmotionProvider>(context, listen: false);
|
||||
await emotionProvider.addEmotion(mood, "Enregistré depuis la montre");
|
||||
print("DEBUG: Humeur '$mood' envoyée à l'API via Provider");
|
||||
} catch (e) {
|
||||
print("DEBUG ERROR: Impossible d'accéder au Provider: $e");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static Future<void> syncMoodToWatch(String mood) async {
|
||||
await _platform.invokeMethod('syncMood', {'mood': mood});
|
||||
}
|
||||
|
||||
static Future<void> checkAndPromptWearSync(BuildContext context, String? userName) async {
|
||||
if (await Permission.bluetoothConnect.request().isGranted) {
|
||||
final bool? isWatchNearby = await _platform.invokeMethod('isWatchNearby');
|
||||
|
||||
if (isWatchNearby == true && context.mounted) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text("Montre connectée détectée"),
|
||||
content: const Text(
|
||||
"Souhaitez-vous lier CESIZen à votre montre pour suivre votre état émotionnel au poignet ?"
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text("PLUS TARD", style: TextStyle(color: Colors.grey)),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
await _platform.invokeMethod('sendAuthStatus', {
|
||||
'status': userName != null ? 'authenticated' : 'unauthenticated',
|
||||
'userName': userName ?? '',
|
||||
});
|
||||
},
|
||||
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF000091)),
|
||||
child: const Text("ACTIVER LA LIAISON", style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user