feat: Add logger service and improve expense dialog with enhanced receipt management and calculation logic.

This commit is contained in:
Van Leemput Dayron
2025-11-28 12:54:54 +01:00
parent cad9d42128
commit fd710b8cb8
35 changed files with 2148 additions and 1296 deletions

View File

@@ -21,19 +21,20 @@ class _MapContentState extends State<MapContent> {
bool _isLoadingLocation = false;
bool _isSearching = false;
Position? _currentPosition;
final Set<Marker> _markers = {};
final Set<Circle> _circles = {};
List<PlaceSuggestion> _suggestions = [];
static final String _apiKey = dotenv.env['GOOGLE_MAPS_API_KEY'] ?? '';
@override
void initState() {
super.initState();
// Si une recherche initiale est fournie, la pré-remplir et lancer la recherche
if (widget.initialSearchQuery != null && widget.initialSearchQuery!.isNotEmpty) {
if (widget.initialSearchQuery != null &&
widget.initialSearchQuery!.isNotEmpty) {
_searchController.text = widget.initialSearchQuery!;
// Lancer la recherche automatiquement après un court délai pour laisser l'interface se charger
Future.delayed(const Duration(milliseconds: 500), () {
@@ -65,17 +66,19 @@ class _MapContentState extends State<MapContent> {
'https://maps.googleapis.com/maps/api/place/autocomplete/json'
'?input=${Uri.encodeComponent(query)}'
'&key=$_apiKey'
'&language=fr'
'&language=fr',
);
final response = await http.get(url);
if (!mounted) return;
if (response.statusCode == 200) {
final data = json.decode(response.body);
if (data['status'] == 'OK') {
final predictions = data['predictions'] as List;
if (predictions.isNotEmpty) {
// Prendre automatiquement la première suggestion
final firstPrediction = predictions.first;
@@ -83,7 +86,7 @@ class _MapContentState extends State<MapContent> {
placeId: firstPrediction['place_id'],
description: firstPrediction['description'],
);
// Effectuer la sélection automatique
await _selectPlaceForInitialSearch(suggestion);
} else {
@@ -117,9 +120,11 @@ class _MapContentState extends State<MapContent> {
final response = await http.get(url);
if (!mounted) return;
if (response.statusCode == 200) {
final data = json.decode(response.body);
if (data['status'] == 'OK') {
final location = data['result']['geometry']['location'];
final lat = location['lat'];
@@ -132,7 +137,7 @@ class _MapContentState extends State<MapContent> {
setState(() {
// Garder le marqueur de position utilisateur s'il existe
_markers.removeWhere((m) => m.markerId.value != 'user_location');
// Ajouter le nouveau marqueur de lieu
_markers.add(
Marker(
@@ -230,9 +235,7 @@ class _MapContentState extends State<MapContent> {
const size = 120.0;
// Dessiner l'icône person_pin_circle en bleu
final iconPainter = TextPainter(
textDirection: TextDirection.ltr,
);
final iconPainter = TextPainter(textDirection: TextDirection.ltr);
iconPainter.text = TextSpan(
text: String.fromCharCode(Icons.person_pin_circle.codePoint),
style: TextStyle(
@@ -242,26 +245,20 @@ class _MapContentState extends State<MapContent> {
),
);
iconPainter.layout();
iconPainter.paint(
canvas,
Offset(
(size - iconPainter.width) / 2,
0,
),
);
iconPainter.paint(canvas, Offset((size - iconPainter.width) / 2, 0));
final picture = pictureRecorder.endRecording();
final image = await picture.toImage(size.toInt(), size.toInt());
final bytes = await image.toByteData(format: ui.ImageByteFormat.png);
return BitmapDescriptor.fromBytes(bytes!.buffer.asUint8List());
return BitmapDescriptor.bytes(bytes!.buffer.asUint8List());
}
// Ajouter le marqueur avec l'icône personnalisée
Future<void> _addUserLocationMarker(LatLng position) async {
_markers.clear();
_circles.clear();
// Ajouter un cercle de précision
_circles.add(
Circle(
@@ -284,10 +281,14 @@ class _MapContentState extends State<MapContent> {
markerId: const MarkerId('user_location'),
position: position,
icon: icon,
anchor: const Offset(0.5, 0.85), // Ancrer au bas de l'icône (le point du pin)
anchor: const Offset(
0.5,
0.85,
), // Ancrer au bas de l'icône (le point du pin)
infoWindow: InfoWindow(
title: 'Ma position',
snippet: 'Lat: ${position.latitude.toStringAsFixed(4)}, Lng: ${position.longitude.toStringAsFixed(4)}',
snippet:
'Lat: ${position.latitude.toStringAsFixed(4)}, Lng: ${position.longitude.toStringAsFixed(4)}',
),
),
);
@@ -311,23 +312,27 @@ class _MapContentState extends State<MapContent> {
'https://maps.googleapis.com/maps/api/place/autocomplete/json'
'?input=${Uri.encodeComponent(query)}'
'&key=$_apiKey'
'&language=fr'
'&language=fr',
);
final response = await http.get(url);
if (!mounted) return;
if (response.statusCode == 200) {
final data = json.decode(response.body);
if (data['status'] == 'OK') {
final predictions = data['predictions'] as List;
setState(() {
_suggestions = predictions
.map((p) => PlaceSuggestion(
placeId: p['place_id'],
description: p['description'],
))
.map(
(p) => PlaceSuggestion(
placeId: p['place_id'],
description: p['description'],
),
)
.toList();
_isSearching = false;
});
@@ -363,9 +368,11 @@ class _MapContentState extends State<MapContent> {
final response = await http.get(url);
if (!mounted) return;
if (response.statusCode == 200) {
final data = json.decode(response.body);
if (data['status'] == 'OK') {
final location = data['result']['geometry']['location'];
final lat = location['lat'];
@@ -378,7 +385,7 @@ class _MapContentState extends State<MapContent> {
setState(() {
// Garder le marqueur de position utilisateur
_markers.removeWhere((m) => m.markerId.value != 'user_location');
// Ajouter le nouveau marqueur de lieu
_markers.add(
Marker(
@@ -394,7 +401,9 @@ class _MapContentState extends State<MapContent> {
CameraUpdate.newLatLngZoom(newPosition, 15),
);
FocusScope.of(context).unfocus();
if (mounted) {
FocusScope.of(context).unfocus();
}
}
}
} catch (e) {
@@ -545,7 +554,10 @@ class _MapContentState extends State<MapContent> {
: Icon(Icons.search, color: Colors.grey[700]),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: Icon(Icons.clear, color: Colors.grey[700]),
icon: Icon(
Icons.clear,
color: Colors.grey[700],
),
onPressed: () {
_searchController.clear();
setState(() {
@@ -567,7 +579,8 @@ class _MapContentState extends State<MapContent> {
),
onChanged: (value) {
// Ne pas rechercher si c'est juste le remplissage initial
if (widget.initialSearchQuery != null && value == widget.initialSearchQuery) {
if (widget.initialSearchQuery != null &&
value == widget.initialSearchQuery) {
return;
}
_searchPlaces(value);
@@ -601,10 +614,8 @@ class _MapContentState extends State<MapContent> {
shrinkWrap: true,
padding: EdgeInsets.zero,
itemCount: _suggestions.length,
separatorBuilder: (context, index) => Divider(
height: 1,
color: Colors.grey[300],
),
separatorBuilder: (context, index) =>
Divider(height: 1, color: Colors.grey[300]),
itemBuilder: (context, index) {
final suggestion = _suggestions[index];
return InkWell(
@@ -664,8 +675,5 @@ class PlaceSuggestion {
final String placeId;
final String description;
PlaceSuggestion({
required this.placeId,
required this.description,
});
}
PlaceSuggestion({required this.placeId, required this.description});
}