Begin adding the work data visualizer
This commit is contained in:
parent
7e78ed323d
commit
5bc49b4cff
9 changed files with 294 additions and 13 deletions
|
@ -1,3 +1,3 @@
|
||||||
{
|
{
|
||||||
"alpha": "1.0.0-dev.9"
|
"alpha": "1.0.0-dev.10"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
|
@ -6,7 +7,8 @@ import 'package:geolocator/geolocator.dart';
|
||||||
class TTConsts {
|
class TTConsts {
|
||||||
static get UPDATE_URL =>
|
static get UPDATE_URL =>
|
||||||
"https://git.zontreck.com/AriasCreations/TimeTracker/raw/branch/main/latest-releases.json";
|
"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 bool UPDATE_AVAILABLE = false;
|
||||||
static UpdateChannel UPDATE_CHANNEL = UpdateChannel.alpha;
|
static UpdateChannel UPDATE_CHANNEL = UpdateChannel.alpha;
|
||||||
static final LocationSettings LOCATION_SETTINGS = LocationSettings(
|
static final LocationSettings LOCATION_SETTINGS = LocationSettings(
|
||||||
|
|
136
lib/data.dart
136
lib/data.dart
|
@ -1,15 +1,14 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:libac_dart/utils/TimeUtils.dart';
|
import 'package:libac_dart/nbt/Stream.dart';
|
||||||
import 'package:timetrack/consts.dart';
|
import 'package:timetrack/consts.dart';
|
||||||
|
|
||||||
class SessionData {
|
class SessionData {
|
||||||
static int StartTime = 0;
|
static DateTime StartTime = DateTime(0);
|
||||||
|
|
||||||
static get StartTimeAsDateTime =>
|
|
||||||
DateTime.fromMillisecondsSinceEpoch(StartTime * 1000);
|
|
||||||
|
|
||||||
static bool IsOnTheClock = false;
|
static bool IsOnTheClock = false;
|
||||||
|
|
||||||
|
@ -19,12 +18,121 @@ class SessionData {
|
||||||
static Trip? currentTrip;
|
static Trip? currentTrip;
|
||||||
static List<Position> positions = [];
|
static List<Position> positions = [];
|
||||||
static late StreamSubscription<Position> _listener;
|
static late StreamSubscription<Position> _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.
|
/// 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 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<String> 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<Position> 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<void> Login() async {
|
static Future<void> Login() async {
|
||||||
StartTime = TimeUtils.getUnixTimestamp();
|
StartTime = DateTime.now();
|
||||||
IsOnTheClock = true;
|
IsOnTheClock = true;
|
||||||
|
|
||||||
bool hasGPS;
|
bool hasGPS;
|
||||||
|
@ -60,6 +168,8 @@ class SessionData {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
positions.add(pos);
|
positions.add(pos);
|
||||||
|
|
||||||
|
SessionData.Calls.dispatch();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -226,3 +336,17 @@ class Trip {
|
||||||
return 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!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -20,6 +20,22 @@ class _HomePageState extends State<HomePage> {
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
SessionData.Calls.HomeCallback = call;
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void call() {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
SessionData.Calls.HomeCallback = null;
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
@ -63,15 +79,29 @@ class _HomePageState extends State<HomePage> {
|
||||||
await Navigator.pushNamed(context, "/map");
|
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(
|
ListTile(
|
||||||
title: Text("RESET APP SESSION"),
|
title: Text("RESET APP SESSION"),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
setState(() {
|
setState(() {
|
||||||
SessionData.IsOnTheClock = false;
|
SessionData.IsOnTheClock = false;
|
||||||
SessionData.StartTime = 0;
|
SessionData.StartTime = DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
0,
|
||||||
|
);
|
||||||
SessionData.Trips = [];
|
SessionData.Trips = [];
|
||||||
SessionData.currentDelivery = null;
|
SessionData.currentDelivery = null;
|
||||||
SessionData.currentTrip = null;
|
SessionData.currentTrip = null;
|
||||||
|
SessionData.positions = [];
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -233,7 +263,7 @@ class _HomePageState extends State<HomePage> {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
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),
|
style: TextStyle(fontSize: 18),
|
||||||
),
|
),
|
||||||
if (SessionData.currentTrip != null) GetTripWidgets(),
|
if (SessionData.currentTrip != null) GetTripWidgets(),
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:timetrack/pages/HomePage.dart';
|
import 'package:timetrack/pages/HomePage.dart';
|
||||||
import 'package:timetrack/pages/MapPage.dart';
|
import 'package:timetrack/pages/MapPage.dart';
|
||||||
import 'package:timetrack/pages/UpdateSettings.dart';
|
import 'package:timetrack/pages/UpdateSettings.dart';
|
||||||
|
import 'package:timetrack/pages/WorkData.dart';
|
||||||
|
|
||||||
class MainApp extends StatefulWidget {
|
class MainApp extends StatefulWidget {
|
||||||
const MainApp({super.key});
|
const MainApp({super.key});
|
||||||
|
@ -26,6 +27,7 @@ class MainAppState extends State<MainApp> {
|
||||||
"/": (ctx) => HomePage(),
|
"/": (ctx) => HomePage(),
|
||||||
"/upd": (ctx) => UpdateSettingsPage(),
|
"/upd": (ctx) => UpdateSettingsPage(),
|
||||||
"/map": (ctx) => MapPage(),
|
"/map": (ctx) => MapPage(),
|
||||||
|
"/work": (ctx) => WorkDataPage(),
|
||||||
},
|
},
|
||||||
theme: ThemeData.dark(),
|
theme: ThemeData.dark(),
|
||||||
);
|
);
|
||||||
|
|
|
@ -19,6 +19,20 @@ class _MapPage extends State<MapPage> {
|
||||||
LatLng initialPosition = LatLng(0, 0);
|
LatLng initialPosition = LatLng(0, 0);
|
||||||
List<LatLng> PointMap = [];
|
List<LatLng> PointMap = [];
|
||||||
List<Marker> Markers = [];
|
List<Marker> Markers = [];
|
||||||
|
bool autorefresh = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
SessionData.Calls.MapCallback = call;
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void call() {
|
||||||
|
if (autorefresh) {
|
||||||
|
didChangeDependencies();
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
|
@ -80,7 +94,8 @@ class _MapPage extends State<MapPage> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
print("Map page disposed");
|
SessionData.Calls.MapCallback = null;
|
||||||
|
|
||||||
controller.dispose();
|
controller.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
@ -98,6 +113,15 @@ class _MapPage extends State<MapPage> {
|
||||||
},
|
},
|
||||||
icon: Icon(Icons.refresh),
|
icon: Icon(Icons.refresh),
|
||||||
),
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
autorefresh = !autorefresh;
|
||||||
|
},
|
||||||
|
icon:
|
||||||
|
autorefresh
|
||||||
|
? Icon(Icons.play_disabled)
|
||||||
|
: Icon(Icons.play_circle),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: GestureDetector(
|
body: GestureDetector(
|
||||||
|
@ -122,7 +146,7 @@ class _MapPage extends State<MapPage> {
|
||||||
Polyline(
|
Polyline(
|
||||||
points: PointMap,
|
points: PointMap,
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
borderStrokeWidth: 3,
|
borderStrokeWidth: 8,
|
||||||
borderColor: Colors.blue,
|
borderColor: Colors.blue,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'package:flutter/widgets.dart';
|
||||||
import 'package:libacflutter/Constants.dart';
|
import 'package:libacflutter/Constants.dart';
|
||||||
import 'package:ota_update/ota_update.dart';
|
import 'package:ota_update/ota_update.dart';
|
||||||
import 'package:timetrack/consts.dart';
|
import 'package:timetrack/consts.dart';
|
||||||
|
import 'package:timetrack/data.dart';
|
||||||
|
|
||||||
class UpdateSettingsPage extends StatefulWidget {
|
class UpdateSettingsPage extends StatefulWidget {
|
||||||
const UpdateSettingsPage({super.key});
|
const UpdateSettingsPage({super.key});
|
||||||
|
@ -14,6 +15,22 @@ class UpdateSettingsPage extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _UpdSet extends State<UpdateSettingsPage> {
|
class _UpdSet extends State<UpdateSettingsPage> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
SessionData.Calls.UpdateSettingsCallback = call;
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
SessionData.Calls.UpdateSettingsCallback = null;
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void call() {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
|
|
82
lib/pages/WorkData.dart
Normal file
82
lib/pages/WorkData.dart
Normal file
|
@ -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<StatefulWidget> createState() {
|
||||||
|
return _WorkData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WorkData extends State<WorkDataPage> {
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
# 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
|
# 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.
|
# 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:
|
environment:
|
||||||
sdk: ^3.7.2
|
sdk: ^3.7.2
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue