From 061fd46f89116daccfb1941ed5db88c05b810d67 Mon Sep 17 00:00:00 2001 From: zontreck Date: Wed, 14 May 2025 20:59:06 -0700 Subject: [PATCH 01/13] Adjust the download URL when on a different channel --- latest-releases.json | 2 +- lib/consts.dart | 4 ++-- lib/pages/UpdateSettings.dart | 2 +- pubspec.yaml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/latest-releases.json b/latest-releases.json index e7c12a6..e8f4557 100644 --- a/latest-releases.json +++ b/latest-releases.json @@ -1,3 +1,3 @@ { - "alpha": "1.0.0-dev.4" + "alpha": "1.0.0-dev.5" } diff --git a/lib/consts.dart b/lib/consts.dart index 8bdd496..a0d3c5a 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -3,9 +3,9 @@ import 'dart:convert'; import 'package:dio/dio.dart'; class TTConsts { - static const UPDATE_URL = + static get UPDATE_URL => "https://git.zontreck.com/AriasCreations/TimeTracker/raw/branch/main/latest-releases.json"; - static const VERSION = "1.0.0-dev.4"; + static const VERSION = "1.0.0-dev.5"; static bool UPDATE_AVAILABLE = false; static UpdateChannel UPDATE_CHANNEL = UpdateChannel.alpha; diff --git a/lib/pages/UpdateSettings.dart b/lib/pages/UpdateSettings.dart index 5a0acaa..7bb0b25 100644 --- a/lib/pages/UpdateSettings.dart +++ b/lib/pages/UpdateSettings.dart @@ -76,7 +76,7 @@ class _UpdSet extends State { onTap: () { OtaUpdate() .execute( - "https://ci.zontreck.com/job/Projects/job/Dart/job/Time%20Tracker/job/main/lastSuccessfulBuild/artifact/build/app/outputs/flutter-apk/timetrack.apk", + "https://ci.zontreck.com/job/Projects/job/Dart/job/Time%20Tracker/job/${TTConsts.UPDATE_CHANNEL == UpdateChannel.stable ? "main" : TTConsts.UPDATE_CHANNEL}/lastSuccessfulBuild/artifact/build/app/outputs/flutter-apk/timetrack.apk", destinationFilename: "timetrack.apk", ) .listen((OtaEvent event) { diff --git a/pubspec.yaml b/pubspec.yaml index 02c44b6..ddf5916 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0-dev.4 +version: 1.0.0-dev.5 environment: sdk: ^3.7.2 From 770c1e7c74e17afeb5c112b64a20b900e4243d45 Mon Sep 17 00:00:00 2001 From: zontreck Date: Thu, 15 May 2025 00:33:42 -0700 Subject: [PATCH 02/13] Begin implementing GPS related functions, and initial UI for trip and delivery --- .vscode/settings.json | 3 + latest-releases.json | 2 +- lib/consts.dart | 6 +- lib/data.dart | 121 ++++++++++++++++++++++++++++++++++ lib/pages/HomePage.dart | 143 ++++++++++++++++++++++++++++++++++++++++ pubspec.yaml | 4 +- 6 files changed, 276 insertions(+), 3 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 lib/data.dart diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c5f3f6b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "interactive" +} \ No newline at end of file diff --git a/latest-releases.json b/latest-releases.json index e8f4557..34609f7 100644 --- a/latest-releases.json +++ b/latest-releases.json @@ -1,3 +1,3 @@ { - "alpha": "1.0.0-dev.5" + "alpha": "1.0.0-dev.6" } diff --git a/lib/consts.dart b/lib/consts.dart index a0d3c5a..636cd5a 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -1,13 +1,17 @@ import 'dart:convert'; import 'package:dio/dio.dart'; +import 'package:geolocator/geolocator.dart'; class TTConsts { static get UPDATE_URL => "https://git.zontreck.com/AriasCreations/TimeTracker/raw/branch/main/latest-releases.json"; - static const VERSION = "1.0.0-dev.5"; + static const VERSION = "1.0.0-dev.6"; static bool UPDATE_AVAILABLE = false; static UpdateChannel UPDATE_CHANNEL = UpdateChannel.alpha; + static final LocationSettings LOCATION_SETTINGS = LocationSettings( + accuracy: LocationAccuracy.bestForNavigation, + ); static Future checkUpdate() async { Dio dio = Dio(); diff --git a/lib/data.dart b/lib/data.dart new file mode 100644 index 0000000..3f1f317 --- /dev/null +++ b/lib/data.dart @@ -0,0 +1,121 @@ +import 'package:geolocator/geolocator.dart'; +import 'package:libac_dart/utils/TimeUtils.dart'; +import 'package:timetrack/consts.dart'; + +class SessionData { + static int StartTime = 0; + + static get StartTimeAsDateTime => + DateTime.fromMillisecondsSinceEpoch(StartTime * 1000); + + static bool IsOnTheClock = false; + + static List Trips = []; + + static Delivery? currentDelivery; + static Trip? currentTrip; + List positions = []; + + static Future Login() async { + StartTime = TimeUtils.getUnixTimestamp(); + IsOnTheClock = true; + + bool hasGPS; + + LocationPermission perm; + hasGPS = await Geolocator.isLocationServiceEnabled(); + if (!hasGPS) { + IsOnTheClock = false; + return Future.error("Location services are disabled"); + } + + perm = await Geolocator.checkPermission(); + if (perm == LocationPermission.denied) { + perm = await Geolocator.requestPermission(); + if (perm == LocationPermission.denied) { + IsOnTheClock = false; + return Future.error("Location permissions are denied"); + } + } + + if (perm == LocationPermission.deniedForever) { + IsOnTheClock = false; + return Future.error( + "Location permissions are denied permanently. Login cannot proceed.", + ); + } + } + + static Future Logout() async { + IsOnTheClock = false; + + // TODO: Do other tasks to finalize the saved work data. + } + + static Future GetNewLocation() async { + Position pos = await Geolocator.getCurrentPosition( + locationSettings: TTConsts.LOCATION_SETTINGS, + ); + + return pos; + } + + static Trip GetNewTrip({required double basePay}) { + currentTrip = Trip(BasePay: basePay); + return currentTrip!; + } + + static Delivery GetNewDelivery() { + if (currentTrip != null) { + var dropOff = currentTrip!.startNewDelivery(); + ; + currentDelivery = dropOff; + return dropOff; + } else { + throw Exception("A delivery cannot exist without a trip"); + } + } + + static void EndTrip() { + currentDelivery = null; + currentTrip = null; + } +} + +class Delivery { + double TipAmount = 0; + late Position endLocation; + int StartTime = 0; + DateTime get StartTimeAsDateTime => + DateTime.fromMillisecondsSinceEpoch(StartTime * 1000); + + Delivery() { + StartTime = TimeUtils.getUnixTimestamp(); + } + + Future MarkEndLocation() async { + var pos = await SessionData.GetNewLocation(); + endLocation = pos; + } +} + +class Trip { + List deliveries = []; + + int StartTime = 0; + DateTime get StartTimeAsDateTime => + DateTime.fromMillisecondsSinceEpoch(StartTime * 1000); + + double BasePay = 0.0; + + Trip({required this.BasePay}) { + StartTime = TimeUtils.getUnixTimestamp(); + } + + Delivery startNewDelivery() { + var delivery = Delivery(); + deliveries.add(delivery); + + return delivery; + } +} diff --git a/lib/pages/HomePage.dart b/lib/pages/HomePage.dart index 70455ad..9c7706f 100644 --- a/lib/pages/HomePage.dart +++ b/lib/pages/HomePage.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:libac_dart/utils/TimeUtils.dart'; import 'package:libacflutter/Constants.dart'; +import 'package:libacflutter/Prompt.dart'; import 'package:timetrack/consts.dart'; +import 'package:timetrack/data.dart'; class HomePage extends StatefulWidget { const HomePage({super.key}); @@ -51,10 +54,150 @@ class _HomePageState extends State { }, leading: Icon(Icons.update), ), + ListTile( + title: Text("RESET APP SESSION"), + onTap: () async { + setState(() { + SessionData.IsOnTheClock = false; + SessionData.StartTime = 0; + SessionData.Trips = []; + SessionData.currentDelivery = null; + SessionData.currentTrip = null; + }); + }, + ), ], ), ), ), + body: SingleChildScrollView( + child: Column( + children: [ + if (!SessionData.IsOnTheClock) + Center( + child: ElevatedButton( + onPressed: () async { + setState(() { + SessionData.Login(); + }); + }, + child: Text("ENGAGE"), + ), + ), + + if (SessionData.IsOnTheClock) GetLoggedInWidgets(), + ], + ), + ), + ); + } + + Widget GetTripWidgets() { + return Column( + children: [ + Text( + "Trip started; Base Pay: \$${SessionData.currentTrip!.BasePay}", + style: TextStyle(fontSize: 18), + ), + Text( + "To end both your current delivery, and the trip, tap on END TRIP", + style: TextStyle(fontSize: 18), + ), + Text( + "You are currently on Delivery #${SessionData.currentTrip!.deliveries.length}", + ), + ElevatedButton( + onPressed: () async { + var reply = await showDialog( + context: context, + builder: (bld) { + return InputPrompt( + title: "What was the tip?", + prompt: "If there was no tip, enter a 0, or just hit submit.", + type: InputPromptType.Number, + ); + }, + ); + + if (reply == null || reply == "") reply = "0"; + double tip = double.parse(reply as String); + SessionData.currentDelivery!.TipAmount = tip; + SessionData.currentDelivery!.MarkEndLocation(); + + SessionData.EndTrip(); + + setState(() {}); + }, + child: Text("END TRIP"), + ), + ], + ); + } + + Widget GetDeliveryWidgets() { + return Column( + children: [ + ElevatedButton( + onPressed: () async { + var reply = await showDialog( + context: context, + builder: (bld) { + return InputPrompt( + title: "What was the tip?", + prompt: + "If there was no tip, enter a 0, or hit submit and leave blank", + type: InputPromptType.Number, + ); + }, + ); + + if (reply == null || reply == "") reply = "0"; + double tip = double.parse(reply as String); + SessionData.currentDelivery!.TipAmount = tip; + SessionData.currentDelivery!.MarkEndLocation(); + SessionData.GetNewDelivery(); + + setState(() {}); + }, + child: Text("Start new delivery"), + ), + ], + ); + } + + Widget GetLoggedInWidgets() { + return Column( + children: [ + Text( + "You are now on the clock\nYour location is being tracked for record keeping purposes.\n\nYou started at ${SessionData.StartTimeAsDateTime}\n\n", + style: TextStyle(fontSize: 18), + ), + if (SessionData.currentTrip != null) GetTripWidgets(), + if (SessionData.currentDelivery != null) GetDeliveryWidgets(), + if (SessionData.currentTrip == null) + ElevatedButton( + onPressed: () async { + var reply = await showDialog( + context: context, + builder: (builder) { + return InputPrompt( + title: "What is the base pay?", + prompt: "Enter the base pay amount below.", + type: InputPromptType.Number, + ); + }, + ); + if (reply == null || reply == "") reply = "0"; + + double basePay = double.parse(reply as String); + SessionData.GetNewTrip(basePay: basePay); + SessionData.GetNewDelivery(); + + setState(() {}); + }, + child: Text("Start New Trip"), + ), + ], ); } } diff --git a/pubspec.yaml b/pubspec.yaml index ddf5916..ce24803 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0-dev.5 +version: 1.0.0-dev.6 environment: sdk: ^3.7.2 @@ -42,6 +42,8 @@ dependencies: version: 1.0.31525+0222 dio: ^5.8.0+1 ota_update: ^7.0.1 + geolocator: ^14.0.0 + free_map: ^2.0.3 dev_dependencies: flutter_test: From 69e82fcf4c656ac6faef3a69879f82adb8c6aa91 Mon Sep 17 00:00:00 2001 From: zontreck Date: Thu, 15 May 2025 17:17:10 -0700 Subject: [PATCH 03/13] Add position logging, and ability to end the work day --- latest-releases.json | 2 +- lib/consts.dart | 2 +- lib/data.dart | 128 ++++++++++++++++++++++++++++++++++++---- lib/pages/HomePage.dart | 22 ++++++- pubspec.yaml | 2 +- 5 files changed, 141 insertions(+), 15 deletions(-) diff --git a/latest-releases.json b/latest-releases.json index 34609f7..3f7c553 100644 --- a/latest-releases.json +++ b/latest-releases.json @@ -1,3 +1,3 @@ { - "alpha": "1.0.0-dev.6" + "alpha": "1.0.0-dev.7" } diff --git a/lib/consts.dart b/lib/consts.dart index 636cd5a..5c90c74 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -6,7 +6,7 @@ import 'package:geolocator/geolocator.dart'; class TTConsts { static get UPDATE_URL => "https://git.zontreck.com/AriasCreations/TimeTracker/raw/branch/main/latest-releases.json"; - static const VERSION = "1.0.0-dev.6"; + static const VERSION = "1.0.0-dev.7"; static bool UPDATE_AVAILABLE = false; static UpdateChannel UPDATE_CHANNEL = UpdateChannel.alpha; static final LocationSettings LOCATION_SETTINGS = LocationSettings( diff --git a/lib/data.dart b/lib/data.dart index 3f1f317..5d44865 100644 --- a/lib/data.dart +++ b/lib/data.dart @@ -1,3 +1,6 @@ +import 'dart:async'; +import 'dart:convert'; + import 'package:geolocator/geolocator.dart'; import 'package:libac_dart/utils/TimeUtils.dart'; import 'package:timetrack/consts.dart'; @@ -14,7 +17,11 @@ class SessionData { static Delivery? currentDelivery; static Trip? currentTrip; - List positions = []; + static List positions = []; + static late StreamSubscription _listener; + + /// This flag is usually set when data is loaded from a saved state. Or when accessed using the Web version of the app. + static bool IsReadOnly = false; static Future Login() async { StartTime = TimeUtils.getUnixTimestamp(); @@ -44,12 +51,68 @@ class SessionData { "Location permissions are denied permanently. Login cannot proceed.", ); } + + _listener = Geolocator.getPositionStream( + locationSettings: TTConsts.LOCATION_SETTINGS, + ).listen((pos) { + if (!IsOnTheClock) { + _listener.cancel(); + return; + } + positions.add(pos); + }); } static Future Logout() async { IsOnTheClock = false; + currentDelivery = null; + currentTrip = null; + _listener.cancel(); - // TODO: Do other tasks to finalize the saved work data. + var saveData = SaveData(); + print(saveData); + + Trips = []; + positions = []; + + // TODO: Upload to the server. + } + + static String SaveData() { + Map saveData = {}; + + List> _trips = []; + for (var trip in Trips) { + _trips.add(trip.toJsonMap()); + } + + List> _pos = []; + for (var pos in positions) { + _pos.add(pos.toJson()); + } + + saveData["trips"] = _trips; + saveData["positions"] = _pos; + + return json.encode(saveData); + } + + void LoadData(String js) { + Map _js = json.decode(js); + List> _trips = + _js['trips'] as List>; + List> _pos = + _js['positions'] as List>; + + for (var trip in _trips) { + Trips.add(Trip.fromJsonMap(trip)); + } + + for (var position in _pos) { + positions.add(Position.fromMap(position)); + } + + IsReadOnly = true; } static Future GetNewLocation() async { @@ -84,32 +147,48 @@ class SessionData { class Delivery { double TipAmount = 0; - late Position endLocation; - int StartTime = 0; - DateTime get StartTimeAsDateTime => - DateTime.fromMillisecondsSinceEpoch(StartTime * 1000); + Position? endLocation; + DateTime StartTime = DateTime.now(); Delivery() { - StartTime = TimeUtils.getUnixTimestamp(); + StartTime = DateTime.now(); } Future MarkEndLocation() async { var pos = await SessionData.GetNewLocation(); endLocation = pos; } + + Map toJsonMap() { + return { + "tip": TipAmount, + "start": StartTime.toString(), + "endPos": endLocation?.toJson() ?? "incomplete", + }; + } + + static Delivery fromMap(Map jsx) { + Delivery delivery = Delivery(); + delivery.StartTime = DateTime.parse(jsx['start'] as String); + delivery.TipAmount = jsx['tip'] as double; + if (jsx['endPos'] as String == "incomplete") + delivery.endLocation = null; + else + delivery.endLocation = Position.fromMap(jsx['endPos']); + + return delivery; + } } class Trip { List deliveries = []; - int StartTime = 0; - DateTime get StartTimeAsDateTime => - DateTime.fromMillisecondsSinceEpoch(StartTime * 1000); + DateTime StartTime = DateTime(0); double BasePay = 0.0; Trip({required this.BasePay}) { - StartTime = TimeUtils.getUnixTimestamp(); + StartTime = DateTime.now(); } Delivery startNewDelivery() { @@ -118,4 +197,31 @@ class Trip { return delivery; } + + Map toJsonMap() { + Map _trip = {"start": StartTime.toString(), "pay": BasePay}; + List> _dropOffs = []; + for (var delivery in deliveries) { + _dropOffs.add(delivery.toJsonMap()); + } + + _trip["deliveries"] = _dropOffs; + + return _trip; + } + + static Trip fromJsonMap(Map jsx) { + Trip trip = Trip(BasePay: 0); + trip.BasePay = jsx['pay'] as double; + trip.StartTime = DateTime.parse(jsx['start'] as String); + trip.deliveries = []; + List> _dropOffs = + jsx['deliveries'] as List>; + + for (var dropOff in _dropOffs) { + trip.deliveries.add(Delivery.fromMap(dropOff)); + } + + return trip; + } } diff --git a/lib/pages/HomePage.dart b/lib/pages/HomePage.dart index 9c7706f..9188bc3 100644 --- a/lib/pages/HomePage.dart +++ b/lib/pages/HomePage.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:libac_dart/utils/TimeUtils.dart'; import 'package:libacflutter/Constants.dart'; import 'package:libacflutter/Prompt.dart'; import 'package:timetrack/consts.dart'; @@ -130,6 +129,27 @@ class _HomePageState extends State { }, child: Text("END TRIP"), ), + ElevatedButton( + onPressed: () async { + if (SessionData.currentTrip != null || + SessionData.currentDelivery != null) { + showDialog( + context: context, + builder: (build) { + return AlertDialog( + title: Text("Cannot end work day"), + content: Text( + "You must end the trip and any delivery before you can fully end the work day.", + ), + ); + }, + ); + } else { + SessionData.Logout(); + } + }, + child: Text("End work day"), + ), ], ); } diff --git a/pubspec.yaml b/pubspec.yaml index ce24803..c33c09a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0-dev.6 +version: 1.0.0-dev.7 environment: sdk: ^3.7.2 From a5fa4e0309ae512e3591a60cc665a9ff3b162c5c Mon Sep 17 00:00:00 2001 From: zontreck Date: Thu, 15 May 2025 19:49:19 -0700 Subject: [PATCH 04/13] Implement map --- README.md | 17 ++++--- latest-releases.json | 2 +- lib/consts.dart | 3 +- lib/pages/HomePage.dart | 110 ++++++++++++++++++++++++---------------- lib/pages/MainApp.dart | 7 ++- lib/pages/MapPage.dart | 97 +++++++++++++++++++++++++++++++++++ pubspec.yaml | 5 +- 7 files changed, 184 insertions(+), 57 deletions(-) create mode 100644 lib/pages/MapPage.dart diff --git a/README.md b/README.md index 42b994c..86e9546 100644 --- a/README.md +++ b/README.md @@ -24,16 +24,17 @@ The app does not store data locally, due to the way android permissions function # Implementation -- [ ] Basic UI +- [x] Basic UI - [x] Permissions - [x] Automatic updates -- [ ] GPS Tracking -- [ ] Formatting GPS on a viewable map -- [ ] Track driving hours - - [ ] Track trips - - [ ] Track stops/deliveries -- [ ] Track trip base pay - - [ ] Track each delivery's tips +- [x] GPS Tracking +- [x] Formatting GPS on a viewable map +- [x] Track driving hours + - [x] Track trips + - [x] Track stops/deliveries +- [x] Track trip base pay + - [x] Track each delivery's tips +- [ ] Map marker for each stop/delivery with text saying "Trip #X/DropOff #X\nBase Pay: $$$; Tip: $$$" - [ ] Basic version of the app in readonly mode when deployed on a web server. - [ ] Backend server - [ ] PHP? diff --git a/latest-releases.json b/latest-releases.json index 3f7c553..489da18 100644 --- a/latest-releases.json +++ b/latest-releases.json @@ -1,3 +1,3 @@ { - "alpha": "1.0.0-dev.7" + "alpha": "1.0.0-dev.8" } diff --git a/lib/consts.dart b/lib/consts.dart index 5c90c74..f65b15a 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -6,11 +6,12 @@ import 'package:geolocator/geolocator.dart'; class TTConsts { static get UPDATE_URL => "https://git.zontreck.com/AriasCreations/TimeTracker/raw/branch/main/latest-releases.json"; - static const VERSION = "1.0.0-dev.7"; + static const VERSION = "1.0.0-dev.8"; static bool UPDATE_AVAILABLE = false; static UpdateChannel UPDATE_CHANNEL = UpdateChannel.alpha; static final LocationSettings LOCATION_SETTINGS = LocationSettings( accuracy: LocationAccuracy.bestForNavigation, + distanceFilter: 50, ); static Future checkUpdate() async { diff --git a/lib/pages/HomePage.dart b/lib/pages/HomePage.dart index 9188bc3..217fa73 100644 --- a/lib/pages/HomePage.dart +++ b/lib/pages/HomePage.dart @@ -53,6 +53,16 @@ class _HomePageState extends State { }, leading: Icon(Icons.update), ), + ListTile( + title: Text("Trip Map"), + leading: Icon(Icons.map), + subtitle: Text( + "View a map of the route\n(NOTE: This is not live, and reflects the current state as of the time the map is opened.)", + ), + onTap: () async { + await Navigator.pushNamed(context, "/map"); + }, + ), ListTile( title: Text("RESET APP SESSION"), onTap: () async { @@ -72,6 +82,10 @@ class _HomePageState extends State { body: SingleChildScrollView( child: Column( children: [ + Text( + "Hit engage when you are ready to go online and start tracking location data, and trips.", + style: TextStyle(fontSize: 18), + ), if (!SessionData.IsOnTheClock) Center( child: ElevatedButton( @@ -129,27 +143,6 @@ class _HomePageState extends State { }, child: Text("END TRIP"), ), - ElevatedButton( - onPressed: () async { - if (SessionData.currentTrip != null || - SessionData.currentDelivery != null) { - showDialog( - context: context, - builder: (build) { - return AlertDialog( - title: Text("Cannot end work day"), - content: Text( - "You must end the trip and any delivery before you can fully end the work day.", - ), - ); - }, - ); - } else { - SessionData.Logout(); - } - }, - child: Text("End work day"), - ), ], ); } @@ -185,6 +178,57 @@ class _HomePageState extends State { ); } + Widget GetNonTripWidgets() { + return Column( + children: [ + ElevatedButton( + onPressed: () async { + var reply = await showDialog( + context: context, + builder: (builder) { + return InputPrompt( + title: "What is the base pay?", + prompt: "Enter the base pay amount below.", + type: InputPromptType.Number, + ); + }, + ); + if (reply == null || reply == "") reply = "0"; + + double basePay = double.parse(reply as String); + SessionData.GetNewTrip(basePay: basePay); + SessionData.GetNewDelivery(); + + setState(() {}); + }, + child: Text("Start New Trip"), + ), + ElevatedButton( + onPressed: () async { + if (SessionData.currentTrip != null || + SessionData.currentDelivery != null) { + showDialog( + context: context, + builder: (build) { + return AlertDialog( + title: Text("Cannot end work day"), + content: Text( + "You must end the trip and any delivery before you can fully end the work day.", + ), + ); + }, + ); + } else { + SessionData.Logout(); + setState(() {}); + } + }, + child: Text("End work day"), + ), + ], + ); + } + Widget GetLoggedInWidgets() { return Column( children: [ @@ -194,29 +238,7 @@ class _HomePageState extends State { ), if (SessionData.currentTrip != null) GetTripWidgets(), if (SessionData.currentDelivery != null) GetDeliveryWidgets(), - if (SessionData.currentTrip == null) - ElevatedButton( - onPressed: () async { - var reply = await showDialog( - context: context, - builder: (builder) { - return InputPrompt( - title: "What is the base pay?", - prompt: "Enter the base pay amount below.", - type: InputPromptType.Number, - ); - }, - ); - if (reply == null || reply == "") reply = "0"; - - double basePay = double.parse(reply as String); - SessionData.GetNewTrip(basePay: basePay); - SessionData.GetNewDelivery(); - - setState(() {}); - }, - child: Text("Start New Trip"), - ), + if (SessionData.currentTrip == null) GetNonTripWidgets(), ], ); } diff --git a/lib/pages/MainApp.dart b/lib/pages/MainApp.dart index 6aa2ec4..1da68e6 100644 --- a/lib/pages/MainApp.dart +++ b/lib/pages/MainApp.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:timetrack/pages/HomePage.dart'; +import 'package:timetrack/pages/MapPage.dart'; import 'package:timetrack/pages/UpdateSettings.dart'; class MainApp extends StatefulWidget { @@ -21,7 +22,11 @@ class MainAppState extends State { Widget build(BuildContext context) { return MaterialApp( title: "Time Tracker", - routes: {"/": (ctx) => HomePage(), "/upd": (ctx) => UpdateSettingsPage()}, + routes: { + "/": (ctx) => HomePage(), + "/upd": (ctx) => UpdateSettingsPage(), + "/map": (ctx) => MapPage(), + }, theme: ThemeData.dark(), ); } diff --git a/lib/pages/MapPage.dart b/lib/pages/MapPage.dart new file mode 100644 index 0000000..0d9b075 --- /dev/null +++ b/lib/pages/MapPage.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:libacflutter/Constants.dart'; +import 'package:timetrack/data.dart'; + +class MapPage extends StatefulWidget { + MapPage({super.key}); + + @override + State createState() { + return _MapPage(); + } +} + +class _MapPage extends State { + MapController controller = MapController(); + LatLng initialPosition = LatLng(0, 0); + List PointMap = []; + + @override + void didChangeDependencies() { + PointMap = []; + + var firstPos = SessionData.positions[0]; + initialPosition = LatLng(firstPos.latitude, firstPos.longitude); + + for (var position in SessionData.positions) { + PointMap.add(LatLng(position.latitude, position.longitude)); + } + + setState(() {}); + super.didChangeDependencies(); + } + + @override + void dispose() { + print("Map page disposed"); + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Time Tracker - Map View"), + backgroundColor: LibACFlutterConstants.TITLEBAR_COLOR, + actions: [ + IconButton( + onPressed: () async { + didChangeDependencies(); + }, + icon: Icon(Icons.refresh), + ), + ], + ), + body: GestureDetector( + onTap: FocusScope.of(context).unfocus, + child: SafeArea( + child: FlutterMap( + mapController: controller, + options: MapOptions( + minZoom: 1, + maxZoom: 30, + initialZoom: 15, + initialCenter: initialPosition, + keepAlive: false, + ), + children: [ + TileLayer( + urlTemplate: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", + userAgentPackageName: "dev.zontreck.timetrack", + ), + PolylineLayer( + polylines: [ + Polyline( + points: PointMap, + color: Colors.blue, + borderStrokeWidth: 3, + borderColor: Colors.blue, + ), + ], + ), + RichAttributionWidget( + attributions: [ + TextSourceAttribution('OpenStreetMap contributors'), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index c33c09a..267eb92 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0-dev.7 +version: 1.0.0-dev.8 environment: sdk: ^3.7.2 @@ -43,7 +43,8 @@ dependencies: dio: ^5.8.0+1 ota_update: ^7.0.1 geolocator: ^14.0.0 - free_map: ^2.0.3 + flutter_map: ^8.1.1 + latlong2: ^0.9.1 dev_dependencies: flutter_test: From e94bbaaf915f7eed9cced7077187e63d16241307 Mon Sep 17 00:00:00 2001 From: zontreck Date: Thu, 15 May 2025 20:19:35 -0700 Subject: [PATCH 05/13] Finish adding the markers to the map view to display info about that specific drop off's tips and base pay. --- latest-releases.json | 2 +- lib/consts.dart | 4 ++-- lib/data.dart | 1 + lib/pages/MapPage.dart | 45 ++++++++++++++++++++++++++++++++++++++++++ pubspec.yaml | 2 +- 5 files changed, 50 insertions(+), 4 deletions(-) diff --git a/latest-releases.json b/latest-releases.json index 489da18..f7cc517 100644 --- a/latest-releases.json +++ b/latest-releases.json @@ -1,3 +1,3 @@ { - "alpha": "1.0.0-dev.8" + "alpha": "1.0.0-dev.9" } diff --git a/lib/consts.dart b/lib/consts.dart index f65b15a..0a57556 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -6,12 +6,12 @@ import 'package:geolocator/geolocator.dart'; class TTConsts { static get UPDATE_URL => "https://git.zontreck.com/AriasCreations/TimeTracker/raw/branch/main/latest-releases.json"; - static const VERSION = "1.0.0-dev.8"; + static const VERSION = "1.0.0-dev.9"; static bool UPDATE_AVAILABLE = false; static UpdateChannel UPDATE_CHANNEL = UpdateChannel.alpha; static final LocationSettings LOCATION_SETTINGS = LocationSettings( accuracy: LocationAccuracy.bestForNavigation, - distanceFilter: 50, + distanceFilter: 15, ); static Future checkUpdate() async { diff --git a/lib/data.dart b/lib/data.dart index 5d44865..af2d0b2 100644 --- a/lib/data.dart +++ b/lib/data.dart @@ -125,6 +125,7 @@ class SessionData { static Trip GetNewTrip({required double basePay}) { currentTrip = Trip(BasePay: basePay); + Trips.add(currentTrip!); return currentTrip!; } diff --git a/lib/pages/MapPage.dart b/lib/pages/MapPage.dart index 0d9b075..16d55fb 100644 --- a/lib/pages/MapPage.dart +++ b/lib/pages/MapPage.dart @@ -18,10 +18,12 @@ class _MapPage extends State { MapController controller = MapController(); LatLng initialPosition = LatLng(0, 0); List PointMap = []; + List Markers = []; @override void didChangeDependencies() { PointMap = []; + Markers = []; var firstPos = SessionData.positions[0]; initialPosition = LatLng(firstPos.latitude, firstPos.longitude); @@ -30,6 +32,48 @@ class _MapPage extends State { PointMap.add(LatLng(position.latitude, position.longitude)); } + print("Total trips: ${SessionData.Trips.length}"); + + for (var trip in SessionData.Trips) { + for (var dropOff in trip.deliveries) { + Markers.add( + Marker( + point: LatLng( + dropOff.endLocation!.latitude, + dropOff.endLocation!.longitude, + ), + child: Stack( + children: [ + IconButton( + onPressed: () { + showDialog( + context: context, + builder: (builder) { + return AlertDialog( + title: Text( + "Trip #${SessionData.Trips.indexOf(trip) + 1}; DropOff #${trip.deliveries.indexOf(dropOff) + 1}", + ), + content: Text( + "Pay: \$${trip.BasePay}\nTip: \$${dropOff.TipAmount}", + ), + ); + }, + ); + }, + icon: Icon( + Icons.location_on_rounded, + size: 24, + color: Colors.red, + ), + ), + ], + ), + ), + ); + print("Marker added"); + } + } + setState(() {}); super.didChangeDependencies(); } @@ -83,6 +127,7 @@ class _MapPage extends State { ), ], ), + MarkerLayer(markers: Markers), RichAttributionWidget( attributions: [ TextSourceAttribution('OpenStreetMap contributors'), diff --git a/pubspec.yaml b/pubspec.yaml index 267eb92..8dbead9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0-dev.8 +version: 1.0.0-dev.9 environment: sdk: ^3.7.2 From 220a317d31e5fa29bde343def51dabf0ff64f3c8 Mon Sep 17 00:00:00 2001 From: zontreck Date: Thu, 15 May 2025 22:48:14 -0700 Subject: [PATCH 06/13] ci: Archive the php scripts --- Jenkinsfile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Jenkinsfile b/Jenkinsfile index 4fae3ca..38ee14a 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -25,6 +25,10 @@ pipeline { mv build/app/outputs/bundle/release/app-release.aab build/app/outputs/bundle/release/timetrack.aab + + cd server/php + tar -cvf ../../php.tgz . + cd ../.. ''' } } @@ -34,6 +38,8 @@ pipeline { archiveArtifacts artifacts: "build/app/outputs/flutter-apk/timetrack.apk" archiveArtifacts artifacts: "build/app/outputs/bundle/release/timetrack.aab" + archiveArtifacts artifacts: "php.tgz" + cleanWs() } } From 7e78ed323d6d78b40c06e4157719ab49d4a7a3e9 Mon Sep 17 00:00:00 2001 From: zontreck Date: Thu, 15 May 2025 22:48:36 -0700 Subject: [PATCH 07/13] Add initial php data --- server/add session.sql | 1 + server/php/timetrack.php | 83 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 server/add session.sql create mode 100644 server/php/timetrack.php diff --git a/server/add session.sql b/server/add session.sql new file mode 100644 index 0000000..5c66790 --- /dev/null +++ b/server/add session.sql @@ -0,0 +1 @@ +INSERT INTO `sessions` (`ID`, `timestamp`) VALUES (uuid(), current_timestamp()); \ No newline at end of file diff --git a/server/php/timetrack.php b/server/php/timetrack.php new file mode 100644 index 0000000..56cde70 --- /dev/null +++ b/server/php/timetrack.php @@ -0,0 +1,83 @@ +query("SELECT UUID() AS id"); + if (!$result) { + http_response_code(500); + echo json_encode(["error" => "Failed to generate UUID"]); + break; + } + + $row = $result->fetch_assoc(); + $sessionId = $row['id']; + + // Insert into `sessions` table + $stmt = $DB->prepare("INSERT INTO `sessions` (`ID`, `timestamp`) VALUES (?, CURRENT_TIMESTAMP())"); + $stmt->bind_param("s", $sessionId); + if (!$stmt->execute()) { + http_response_code(500); + echo json_encode(["error" => "Failed to insert into sessions"]); + break; + } + $stmt->close(); + + // Prepare data as JSON and insert into `data` table + $data = json_encode($jsx['data']); + + $stmt = $DB->prepare("INSERT INTO `data` (`ID`, `SessionData`) VALUES (?, ?)"); + $null = NULL; // Required for bind_param with blob + $stmt->bind_param("sb", $sessionId, $null); // Temporarily bind $null for blob + + // Send the actual blob content using send_long_data + $stmt->send_long_data(1, $data); // Index 1 refers to the second "?" in bind_param + if (!$stmt->execute()) { + http_response_code(500); + echo json_encode(["error" => "Failed to insert into data"]); + break; + } + + $stmt->close(); + + echo json_encode(["status" => "ok", "session" => $sessionId]); + break; + } + case "get": { + $sessionId = $jsx['id']; + + $stmt = $DB->prepare("SELECT `SessionData` FROM `data` WHERE `ID` = ?"); + $stmt->bind_param("s", $sessionId); + $stmt->execute(); + $stmt->store_result(); + + if ($stmt->num_rows === 0) { + http_response_code(404); + echo json_encode(["error" => "Session not found"]); + $stmt->close(); + break; + } + + $stmt->bind_result($sessionData); + $stmt->fetch(); + $stmt->close(); + + // Decode the JSON blob (optional — if you want raw JSON output) + header("Content-Type: application/json"); + echo $sessionData; + break; + } + + +} +?> \ No newline at end of file From 5bc49b4cffd0c4aa626df3752f32b28d78bb8114 Mon Sep 17 00:00:00 2001 From: zontreck Date: Thu, 15 May 2025 22:49:18 -0700 Subject: [PATCH 08/13] Begin adding the work data visualizer --- latest-releases.json | 2 +- lib/consts.dart | 4 +- lib/data.dart | 136 ++++++++++++++++++++++++++++++++-- lib/pages/HomePage.dart | 34 ++++++++- lib/pages/MainApp.dart | 2 + lib/pages/MapPage.dart | 28 ++++++- lib/pages/UpdateSettings.dart | 17 +++++ lib/pages/WorkData.dart | 82 ++++++++++++++++++++ pubspec.yaml | 2 +- 9 files changed, 294 insertions(+), 13 deletions(-) create mode 100644 lib/pages/WorkData.dart diff --git a/latest-releases.json b/latest-releases.json index f7cc517..d84fd53 100644 --- a/latest-releases.json +++ b/latest-releases.json @@ -1,3 +1,3 @@ { - "alpha": "1.0.0-dev.9" + "alpha": "1.0.0-dev.10" } diff --git a/lib/consts.dart b/lib/consts.dart index 0a57556..3ee858c 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:ui'; import 'package:dio/dio.dart'; import 'package:geolocator/geolocator.dart'; @@ -6,7 +7,8 @@ import 'package:geolocator/geolocator.dart'; class TTConsts { static get UPDATE_URL => "https://git.zontreck.com/AriasCreations/TimeTracker/raw/branch/main/latest-releases.json"; - static const VERSION = "1.0.0-dev.9"; + static const VERSION = "1.0.0-dev.10"; + static bool UPDATE_AVAILABLE = false; static UpdateChannel UPDATE_CHANNEL = UpdateChannel.alpha; static final LocationSettings LOCATION_SETTINGS = LocationSettings( diff --git a/lib/data.dart b/lib/data.dart index af2d0b2..fdce05f 100644 --- a/lib/data.dart +++ b/lib/data.dart @@ -1,15 +1,14 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:math' as math; +import 'dart:ui'; import 'package:geolocator/geolocator.dart'; -import 'package:libac_dart/utils/TimeUtils.dart'; +import 'package:libac_dart/nbt/Stream.dart'; import 'package:timetrack/consts.dart'; class SessionData { - static int StartTime = 0; - - static get StartTimeAsDateTime => - DateTime.fromMillisecondsSinceEpoch(StartTime * 1000); + static DateTime StartTime = DateTime(0); static bool IsOnTheClock = false; @@ -19,12 +18,121 @@ class SessionData { static Trip? currentTrip; static List positions = []; static late StreamSubscription _listener; + static Callbacks Calls = Callbacks(); /// This flag is usually set when data is loaded from a saved state. Or when accessed using the Web version of the app. static bool IsReadOnly = false; + static double GetTotalBasePay() { + double total = 0; + for (var trip in Trips) { + total += trip.BasePay; + } + + return total; + } + + static double GetTotalTips() { + double total = 0; + for (var trip in Trips) { + for (var drop in trip.deliveries) { + total += drop.TipAmount; + } + } + + return total; + } + + static double GetTotalPay() { + return GetTotalBasePay() + GetTotalTips(); + } + + static double GetTotalMiles() { + double total = 0; + total = _totalMilesTraveled( + positions, + minDistanceMeters: 5, + maxDistanceMeters: 512, + ); + return total; + } + + static String GetTotalMilesAsString() { + double miles = GetTotalMiles(); + if (miles == 0) return "0.0"; + List split = miles.toString().split("."); + StringBuilder out = StringBuilder(); + + out.append(split[0]); + out.append("."); + out.append(split[1].substring(0, 4)); + + return out.toString(); + } + + //** Begin AI Generated code */ + /// Converts meters → miles. + static const _metersPerMile = 1609.344; + + /// Radius of the earth in meters (WGS‑84 mean radius). + static const _earthRadiusM = 6371000.0; + static double _haversineMeters( + double lat1, + double lon1, + double lat2, + double lon2, + ) { + final dLat = _deg2rad(lat2 - lat1); + final dLon = _deg2rad(lon2 - lon1); + + final a = + math.sin(dLat / 2) * math.sin(dLat / 2) + + math.cos(_deg2rad(lat1)) * + math.cos(_deg2rad(lat2)) * + math.sin(dLon / 2) * + math.sin(dLon / 2); + + return _earthRadiusM * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)); + } + + static double _deg2rad(double deg) => deg * math.pi / 180.0; + + /// Returns total miles traveled after basic GPS‑noise cleanup. + /// + /// * [minDistanceMeters] – drop segments shorter than this (jitter). + /// * [maxDistanceMeters] – drop segments longer than this (impossible jump). + static double _totalMilesTraveled( + List positions, { + double minDistanceMeters = 5, + double? maxDistanceMeters, + }) { + if (positions.length < 2) return 0; + + var meters = 0.0; + + for (var i = 1; i < positions.length; i++) { + final p1 = positions[i - 1]; + final p2 = positions[i]; + + final d = _haversineMeters( + p1.latitude, + p1.longitude, + p2.latitude, + p2.longitude, + ); + + if (d < minDistanceMeters) continue; // too small → jitter + if (maxDistanceMeters != null && d > maxDistanceMeters) + continue; // glitch + + meters += d; + } + return meters / _metersPerMile; + } + //** End AI Generated code */ + static Future Login() async { - StartTime = TimeUtils.getUnixTimestamp(); + StartTime = DateTime.now(); IsOnTheClock = true; bool hasGPS; @@ -60,6 +168,8 @@ class SessionData { return; } positions.add(pos); + + SessionData.Calls.dispatch(); }); } @@ -226,3 +336,17 @@ class Trip { return trip; } } + +class Callbacks { + VoidCallback? HomeCallback; + VoidCallback? MapCallback; + VoidCallback? UpdateSettingsCallback; + VoidCallback? WorkDataCallback; + + void dispatch() { + if (HomeCallback != null) HomeCallback!(); + if (MapCallback != null) MapCallback!(); + if (UpdateSettingsCallback != null) UpdateSettingsCallback!(); + if (WorkDataCallback != null) WorkDataCallback!(); + } +} diff --git a/lib/pages/HomePage.dart b/lib/pages/HomePage.dart index 217fa73..c63d778 100644 --- a/lib/pages/HomePage.dart +++ b/lib/pages/HomePage.dart @@ -20,6 +20,22 @@ class _HomePageState extends State { super.didChangeDependencies(); } + @override + void initState() { + SessionData.Calls.HomeCallback = call; + super.initState(); + } + + void call() { + setState(() {}); + } + + @override + void dispose() { + SessionData.Calls.HomeCallback = null; + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -63,15 +79,29 @@ class _HomePageState extends State { await Navigator.pushNamed(context, "/map"); }, ), + ListTile( + title: Text("Work Data"), + subtitle: Text("View and edit work data"), + leading: Icon(Icons.work_history), + onTap: () async { + // Open up the work data viewer and editor. + // Edit will be disabled for web or read only mode. + await Navigator.pushNamed(context, "/work"); + setState(() {}); + }, + ), ListTile( title: Text("RESET APP SESSION"), onTap: () async { setState(() { SessionData.IsOnTheClock = false; - SessionData.StartTime = 0; + SessionData.StartTime = DateTime.fromMillisecondsSinceEpoch( + 0, + ); SessionData.Trips = []; SessionData.currentDelivery = null; SessionData.currentTrip = null; + SessionData.positions = []; }); }, ), @@ -233,7 +263,7 @@ class _HomePageState extends State { return Column( children: [ Text( - "You are now on the clock\nYour location is being tracked for record keeping purposes.\n\nYou started at ${SessionData.StartTimeAsDateTime}\n\n", + "You are now on the clock\nYour location is being tracked for record keeping purposes.\n\nYou started at ${SessionData.StartTime.toLocal()}\n\n", style: TextStyle(fontSize: 18), ), if (SessionData.currentTrip != null) GetTripWidgets(), diff --git a/lib/pages/MainApp.dart b/lib/pages/MainApp.dart index 1da68e6..4a01ae0 100644 --- a/lib/pages/MainApp.dart +++ b/lib/pages/MainApp.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:timetrack/pages/HomePage.dart'; import 'package:timetrack/pages/MapPage.dart'; import 'package:timetrack/pages/UpdateSettings.dart'; +import 'package:timetrack/pages/WorkData.dart'; class MainApp extends StatefulWidget { const MainApp({super.key}); @@ -26,6 +27,7 @@ class MainAppState extends State { "/": (ctx) => HomePage(), "/upd": (ctx) => UpdateSettingsPage(), "/map": (ctx) => MapPage(), + "/work": (ctx) => WorkDataPage(), }, theme: ThemeData.dark(), ); diff --git a/lib/pages/MapPage.dart b/lib/pages/MapPage.dart index 16d55fb..fc05285 100644 --- a/lib/pages/MapPage.dart +++ b/lib/pages/MapPage.dart @@ -19,6 +19,20 @@ class _MapPage extends State { LatLng initialPosition = LatLng(0, 0); List PointMap = []; List Markers = []; + bool autorefresh = true; + + @override + void initState() { + SessionData.Calls.MapCallback = call; + super.initState(); + } + + void call() { + if (autorefresh) { + didChangeDependencies(); + } + setState(() {}); + } @override void didChangeDependencies() { @@ -80,7 +94,8 @@ class _MapPage extends State { @override void dispose() { - print("Map page disposed"); + SessionData.Calls.MapCallback = null; + controller.dispose(); super.dispose(); } @@ -98,6 +113,15 @@ class _MapPage extends State { }, icon: Icon(Icons.refresh), ), + IconButton( + onPressed: () { + autorefresh = !autorefresh; + }, + icon: + autorefresh + ? Icon(Icons.play_disabled) + : Icon(Icons.play_circle), + ), ], ), body: GestureDetector( @@ -122,7 +146,7 @@ class _MapPage extends State { Polyline( points: PointMap, color: Colors.blue, - borderStrokeWidth: 3, + borderStrokeWidth: 8, borderColor: Colors.blue, ), ], diff --git a/lib/pages/UpdateSettings.dart b/lib/pages/UpdateSettings.dart index 7bb0b25..2922068 100644 --- a/lib/pages/UpdateSettings.dart +++ b/lib/pages/UpdateSettings.dart @@ -3,6 +3,7 @@ import 'package:flutter/widgets.dart'; import 'package:libacflutter/Constants.dart'; import 'package:ota_update/ota_update.dart'; import 'package:timetrack/consts.dart'; +import 'package:timetrack/data.dart'; class UpdateSettingsPage extends StatefulWidget { const UpdateSettingsPage({super.key}); @@ -14,6 +15,22 @@ class UpdateSettingsPage extends StatefulWidget { } class _UpdSet extends State { + @override + void initState() { + SessionData.Calls.UpdateSettingsCallback = call; + super.initState(); + } + + @override + void dispose() { + SessionData.Calls.UpdateSettingsCallback = null; + super.dispose(); + } + + void call() { + setState(() {}); + } + @override void didChangeDependencies() { super.didChangeDependencies(); diff --git a/lib/pages/WorkData.dart b/lib/pages/WorkData.dart new file mode 100644 index 0000000..eeac3fe --- /dev/null +++ b/lib/pages/WorkData.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:libacflutter/Constants.dart'; +import 'package:timetrack/consts.dart'; +import 'package:timetrack/data.dart'; + +class WorkDataPage extends StatefulWidget { + WorkDataPage({super.key}); + + @override + State createState() { + return _WorkData(); + } +} + +class _WorkData extends State { + void call() { + setState(() {}); + } + + @override + void initState() { + SessionData.Calls.WorkDataCallback = call; + super.initState(); + } + + @override + void dispose() { + SessionData.Calls.WorkDataCallback = null; + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Time Tracker - Work Data"), + backgroundColor: LibACFlutterConstants.TITLEBAR_COLOR, + ), + body: Padding( + padding: EdgeInsets.all(8), + child: SingleChildScrollView( + child: Column( + children: [ + // This is where we'll display all the work data, like total earnings, and present a editor + Text( + "Total saved GPS Positions: ${SessionData.positions.length}", + style: TextStyle(fontSize: 18), + ), + Text( + "Start Date & Time: ${SessionData.StartTime.toString()}", + style: TextStyle(fontSize: 18), + ), + SizedBox(height: 20), + Text( + "Total Trips: ${SessionData.Trips.length}", + style: TextStyle(fontSize: 18), + ), + Text( + "Total Base Pay: \$${SessionData.GetTotalBasePay()}", + style: TextStyle(fontSize: 18), + ), + Text( + "Total Tips: \$${SessionData.GetTotalTips()}", + style: TextStyle(fontSize: 18), + ), + Text( + "Total Earnings: \$${SessionData.GetTotalPay()}", + style: TextStyle(fontSize: 18), + ), + SizedBox(height: 20), + Text( + "Total Miles: ${SessionData.GetTotalMilesAsString()}", + style: TextStyle(fontSize: 24), + ), + ], + ), + ), + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 8dbead9..1eb0ca2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0-dev.9 +version: 1.0.0-dev.10 environment: sdk: ^3.7.2 From 762be79df6ad64c4004cf8b5bfde14cca83e4389 Mon Sep 17 00:00:00 2001 From: zontreck Date: Thu, 15 May 2025 23:14:41 -0700 Subject: [PATCH 09/13] php: Add a disclaimer about ai content --- server/php/timetrack.php | 1 + 1 file changed, 1 insertion(+) diff --git a/server/php/timetrack.php b/server/php/timetrack.php index 56cde70..f29d28c 100644 --- a/server/php/timetrack.php +++ b/server/php/timetrack.php @@ -10,6 +10,7 @@ $DB = get_DB("timetrack"); $jsx = json_decode(file_get_contents("php://input"), true); // Get operation information +// DISCLAIMER: All php code below this point is AI Generated switch($jsx['cmd']) { case "create": { // Get UUID from MySQL and insert into sessions table From 7c5e3360a523a4e7da06dc6053bff0737e30df2e Mon Sep 17 00:00:00 2001 From: zontreck Date: Fri, 16 May 2025 01:38:40 -0700 Subject: [PATCH 10/13] Make some changes to the PHP URL to make it channel specific --- latest-releases.json | 3 ++- lib/consts.dart | 6 ++++-- lib/data.dart | 21 ++++++++++++++++++--- lib/pages/HomePage.dart | 15 +++++++++++---- lib/pages/WorkData.dart | 3 +-- pubspec.yaml | 2 +- 6 files changed, 37 insertions(+), 13 deletions(-) diff --git a/latest-releases.json b/latest-releases.json index d84fd53..db193d0 100644 --- a/latest-releases.json +++ b/latest-releases.json @@ -1,3 +1,4 @@ { - "alpha": "1.0.0-dev.10" + "alpha": "1.0.0-dev.10", + "beta": "1.0.0-beta.1" } diff --git a/lib/consts.dart b/lib/consts.dart index 3ee858c..f3b09c9 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:ui'; import 'package:dio/dio.dart'; import 'package:geolocator/geolocator.dart'; @@ -7,7 +6,10 @@ import 'package:geolocator/geolocator.dart'; class TTConsts { static get UPDATE_URL => "https://git.zontreck.com/AriasCreations/TimeTracker/raw/branch/main/latest-releases.json"; - static const VERSION = "1.0.0-dev.10"; + static get SESSION_SERVER => + "https://api.zontreck.com/timetrack/${UPDATE_CHANNEL}/timetrack.php"; + + static const VERSION = "1.0.0-beta.1"; static bool UPDATE_AVAILABLE = false; static UpdateChannel UPDATE_CHANNEL = UpdateChannel.alpha; diff --git a/lib/data.dart b/lib/data.dart index fdce05f..4bf36ff 100644 --- a/lib/data.dart +++ b/lib/data.dart @@ -3,6 +3,8 @@ import 'dart:convert'; import 'dart:math' as math; import 'dart:ui'; +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:libac_dart/nbt/Stream.dart'; import 'package:timetrack/consts.dart'; @@ -19,6 +21,7 @@ class SessionData { static List positions = []; static late StreamSubscription _listener; static Callbacks Calls = Callbacks(); + static String LastSessionID = ""; /// This flag is usually set when data is loaded from a saved state. Or when accessed using the Web version of the app. static bool IsReadOnly = false; @@ -185,10 +188,22 @@ class SessionData { Trips = []; positions = []; - // TODO: Upload to the server. + Dio dio = Dio(); + Map payload = {"cmd": "create", "data": saveData}; + + var reply = await dio.post( + TTConsts.SESSION_SERVER, + data: json.encode(payload), + ); + Map replyJs = json.decode(reply.data as String); + if (replyJs["status"] == "ok") { + print("Successful upload"); + LastSessionID = replyJs['session'] as String; + Calls.dispatch(); + } } - static String SaveData() { + static Map SaveData() { Map saveData = {}; List> _trips = []; @@ -204,7 +219,7 @@ class SessionData { saveData["trips"] = _trips; saveData["positions"] = _pos; - return json.encode(saveData); + return saveData; } void LoadData(String js) { diff --git a/lib/pages/HomePage.dart b/lib/pages/HomePage.dart index c63d778..c1b8e18 100644 --- a/lib/pages/HomePage.dart +++ b/lib/pages/HomePage.dart @@ -112,10 +112,17 @@ class _HomePageState extends State { body: SingleChildScrollView( child: Column( children: [ - Text( - "Hit engage when you are ready to go online and start tracking location data, and trips.", - style: TextStyle(fontSize: 18), - ), + if (!SessionData.IsOnTheClock) + Text( + "Hit engage when you are ready to go online and start tracking location data, and trips.", + style: TextStyle(fontSize: 18), + ), + + if (SessionData.LastSessionID.isNotEmpty) + Text( + "Session ID Code: ${SessionData.LastSessionID}", + style: TextStyle(fontSize: 18), + ), if (!SessionData.IsOnTheClock) Center( child: ElevatedButton( diff --git a/lib/pages/WorkData.dart b/lib/pages/WorkData.dart index eeac3fe..f99e551 100644 --- a/lib/pages/WorkData.dart +++ b/lib/pages/WorkData.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:libacflutter/Constants.dart'; -import 'package:timetrack/consts.dart'; import 'package:timetrack/data.dart'; class WorkDataPage extends StatefulWidget { @@ -70,7 +69,7 @@ class _WorkData extends State { ), SizedBox(height: 20), Text( - "Total Miles: ${SessionData.GetTotalMilesAsString()}", + "Total Estimated Miles: ${SessionData.GetTotalMilesAsString()}\n(Note: The miles displayed above may not be 100% accurate)", style: TextStyle(fontSize: 24), ), ], diff --git a/pubspec.yaml b/pubspec.yaml index 1eb0ca2..b42f1a3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0-dev.10 +version: 1.0.0-beta.1 environment: sdk: ^3.7.2 From 37e56888425db6b4743b79805aabc24670de26aa Mon Sep 17 00:00:00 2001 From: zontreck Date: Fri, 16 May 2025 02:21:25 -0700 Subject: [PATCH 11/13] Add ability to tap to copy the session ID --- latest-releases.json | 2 +- lib/consts.dart | 2 +- lib/pages/HomePage.dart | 12 +++++++++--- pubspec.yaml | 2 +- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/latest-releases.json b/latest-releases.json index db193d0..59e46f8 100644 --- a/latest-releases.json +++ b/latest-releases.json @@ -1,4 +1,4 @@ { "alpha": "1.0.0-dev.10", - "beta": "1.0.0-beta.1" + "beta": "1.0.0-beta.2" } diff --git a/lib/consts.dart b/lib/consts.dart index f3b09c9..51df1d6 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -9,7 +9,7 @@ class TTConsts { static get SESSION_SERVER => "https://api.zontreck.com/timetrack/${UPDATE_CHANNEL}/timetrack.php"; - static const VERSION = "1.0.0-beta.1"; + static const VERSION = "1.0.0-beta.2"; static bool UPDATE_AVAILABLE = false; static UpdateChannel UPDATE_CHANNEL = UpdateChannel.alpha; diff --git a/lib/pages/HomePage.dart b/lib/pages/HomePage.dart index c1b8e18..9d9b7b9 100644 --- a/lib/pages/HomePage.dart +++ b/lib/pages/HomePage.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:libacflutter/Constants.dart'; import 'package:libacflutter/Prompt.dart'; import 'package:timetrack/consts.dart'; @@ -119,9 +120,14 @@ class _HomePageState extends State { ), if (SessionData.LastSessionID.isNotEmpty) - Text( - "Session ID Code: ${SessionData.LastSessionID}", - style: TextStyle(fontSize: 18), + ListTile( + title: Text("Session ID"), + subtitle: Text("${SessionData.LastSessionID} - Tap to copy"), + onTap: () { + Clipboard.setData( + ClipboardData(text: SessionData.LastSessionID), + ); + }, ), if (!SessionData.IsOnTheClock) Center( diff --git a/pubspec.yaml b/pubspec.yaml index b42f1a3..0781771 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0-beta.1 +version: 1.0.0-beta.2 environment: sdk: ^3.7.2 From 0b83eaaf3c94e44257a698d4b73efc9478824b94 Mon Sep 17 00:00:00 2001 From: zontreck Date: Fri, 16 May 2025 10:09:41 -0700 Subject: [PATCH 12/13] Begin to add the web interface page --- latest-releases.json | 2 +- lib/consts.dart | 4 +- lib/data.dart | 20 +++++- lib/main.dart | 10 +++ lib/pages/MainApp.dart | 5 +- lib/pages/WebMainPage.dart | 128 +++++++++++++++++++++++++++++++++++++ pubspec.yaml | 2 +- 7 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 lib/pages/WebMainPage.dart diff --git a/latest-releases.json b/latest-releases.json index 59e46f8..5922860 100644 --- a/latest-releases.json +++ b/latest-releases.json @@ -1,4 +1,4 @@ { "alpha": "1.0.0-dev.10", - "beta": "1.0.0-beta.2" + "beta": "1.0.0-beta.3" } diff --git a/lib/consts.dart b/lib/consts.dart index 51df1d6..85c2227 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -9,10 +9,10 @@ class TTConsts { static get SESSION_SERVER => "https://api.zontreck.com/timetrack/${UPDATE_CHANNEL}/timetrack.php"; - static const VERSION = "1.0.0-beta.2"; + static const VERSION = "1.0.0-beta.3"; static bool UPDATE_AVAILABLE = false; - static UpdateChannel UPDATE_CHANNEL = UpdateChannel.alpha; + static UpdateChannel UPDATE_CHANNEL = UpdateChannel.beta; static final LocationSettings LOCATION_SETTINGS = LocationSettings( accuracy: LocationAccuracy.bestForNavigation, distanceFilter: 15, diff --git a/lib/data.dart b/lib/data.dart index 4bf36ff..460585f 100644 --- a/lib/data.dart +++ b/lib/data.dart @@ -22,6 +22,7 @@ class SessionData { static late StreamSubscription _listener; static Callbacks Calls = Callbacks(); static String LastSessionID = ""; + static String DisplayError = ""; /// This flag is usually set when data is loaded from a saved state. Or when accessed using the Web version of the app. static bool IsReadOnly = false; @@ -222,8 +223,25 @@ class SessionData { return saveData; } - void LoadData(String js) { + static Future DownloadData() async { + Dio dio = Dio(); + Map payload = {"cmd": "get", "id": LastSessionID}; + + // Send the data, and get the response + var reply = await dio.post( + TTConsts.SESSION_SERVER, + data: json.encode(payload), + ); + + LoadData(reply.data as String); + } + + static void LoadData(String js) { Map _js = json.decode(js); + if (_js.containsKey("error")) { + LastSessionID = ""; + return; + } List> _trips = _js['trips'] as List>; List> _pos = diff --git a/lib/main.dart b/lib/main.dart index 3c1c510..fef79bc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,9 +1,19 @@ import 'package:flutter/material.dart'; import 'package:timetrack/consts.dart'; +import 'package:timetrack/data.dart'; import 'package:timetrack/pages/MainApp.dart'; Future main() async { await TTConsts.checkUpdate(); + var sess = Uri.base.queryParameters["code"] ?? ""; + SessionData.LastSessionID = sess; + if (SessionData.LastSessionID.isNotEmpty) { + await SessionData.DownloadData(); + if (SessionData.LastSessionID.isEmpty) { + // Invalid session token + SessionData.DisplayError = "The URL and or session token is invalid"; + } + } runApp(MainApp()); } diff --git a/lib/pages/MainApp.dart b/lib/pages/MainApp.dart index 4a01ae0..087fd6f 100644 --- a/lib/pages/MainApp.dart +++ b/lib/pages/MainApp.dart @@ -1,7 +1,10 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:timetrack/pages/HomePage.dart'; import 'package:timetrack/pages/MapPage.dart'; import 'package:timetrack/pages/UpdateSettings.dart'; +import 'package:timetrack/pages/WebMainPage.dart'; import 'package:timetrack/pages/WorkData.dart'; class MainApp extends StatefulWidget { @@ -24,7 +27,7 @@ class MainAppState extends State { return MaterialApp( title: "Time Tracker", routes: { - "/": (ctx) => HomePage(), + "/": (ctx) => Platform.isAndroid ? HomePage() : WebMain(), "/upd": (ctx) => UpdateSettingsPage(), "/map": (ctx) => MapPage(), "/work": (ctx) => WorkDataPage(), diff --git a/lib/pages/WebMainPage.dart b/lib/pages/WebMainPage.dart new file mode 100644 index 0000000..41db49f --- /dev/null +++ b/lib/pages/WebMainPage.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:libacflutter/Constants.dart'; +import 'package:timetrack/consts.dart'; +import 'package:timetrack/data.dart'; + +class WebMain extends StatefulWidget { + @override + State createState() { + return _WebMain(); + } +} + +class _WebMain extends State { + TextEditingController sessionIDController = TextEditingController(); + + @override + void didChangeDependencies() { + sessionIDController.text = SessionData.LastSessionID; + super.didChangeDependencies(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Time Tracker"), + backgroundColor: LibACFlutterConstants.TITLEBAR_COLOR, + ), + drawer: Drawer( + elevation: 8, + child: SingleChildScrollView( + child: Column( + children: [ + DrawerHeader( + child: Column( + children: [ + Text("Time Tracker"), + Text("Created by Tara Piccari"), + Text("Copyright 2025 - Present"), + Text("Version: ${TTConsts.VERSION}"), + ], + ), + ), + if (SessionData.IsReadOnly) + ListTile( + title: Text("Trip Map"), + leading: Icon(Icons.map), + subtitle: Text( + "View a map of the route\n(NOTE: This is not live, and reflects the current state as of the time the map is opened.)", + ), + onTap: () async { + await Navigator.pushNamed(context, "/map"); + }, + ), + if (SessionData.IsReadOnly) + ListTile( + title: Text("Work Data"), + subtitle: Text("View work data"), + leading: Icon(Icons.work_history), + onTap: () async { + // Open up the work data viewer and editor. + // Edit will be disabled for web or read only mode. + await Navigator.pushNamed(context, "/work"); + setState(() {}); + }, + ), + ], + ), + ), + ), + body: Padding( + padding: EdgeInsets.all(8), + child: SingleChildScrollView( + child: Column( + children: [ + // Start doing magic! + if (SessionData.DisplayError.isNotEmpty) + Text(SessionData.DisplayError, style: TextStyle(fontSize: 18)), + // Check what widgets need to be displayed. + if (SessionData.IsReadOnly) GetReadOnlyWidgets(), + if (!SessionData.IsReadOnly) GetLoadWidgets(), + ], + ), + ), + ), + ); + } + + Widget GetReadOnlyWidgets() { + return Column( + children: [ + Text( + "Use the top left menu to show the various pages for the data viewer.", + ), + ElevatedButton( + onPressed: () async { + SessionData.IsReadOnly = false; + SessionData.Trips = []; + SessionData.positions = []; + SessionData.DisplayError = ""; + }, + child: Text("Close Session"), + ), + ], + ); + } + + Widget GetLoadWidgets() { + return Column( + children: [ + // Present a text box for the session ID, and a button for loading. + ListTile(title: Text("Session ID")), + TextField( + controller: sessionIDController, + decoration: InputDecoration(border: OutlineInputBorder()), + ), + ElevatedButton( + onPressed: () async { + await SessionData.DownloadData(); + setState(() {}); + }, + child: Text("Load Session", style: TextStyle(fontSize: 18)), + ), + ], + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 0781771..dd244a5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0-beta.2 +version: 1.0.0-beta.3 environment: sdk: ^3.7.2 From 9b50945e3b48978f707e64a2ac7b0d8771f4dec4 Mon Sep 17 00:00:00 2001 From: zontreck Date: Fri, 16 May 2025 11:46:33 -0700 Subject: [PATCH 13/13] ci: Add web build task for web.tgz --- Jenkinsfile | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Jenkinsfile b/Jenkinsfile index 38ee14a..799743d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -44,5 +44,33 @@ pipeline { } } } + + stage("Build Web App") { + agent { + label 'linux' + } + + steps { + script { + sh ''' + #!/bin/bash + + flutter build web + + cd build/web + tar -cvf ../../web.tgz . + cd ../.. + ''' + } + } + + post { + always { + archiveArtifacts artifacts: "web.tgz" + + cleanWs() + } + } + } } } \ No newline at end of file