Première grosse version fonctionnel

This commit is contained in:
2026-04-22 23:30:31 +02:00
commit ccbe5ad801
329 changed files with 29518 additions and 0 deletions
@@ -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;
}
}
}