Merge pull request 'Merge current work tree' (#1) from beta into main
Reviewed-on: #1
This commit is contained in:
commit
24bb8f49ec
16 changed files with 1176 additions and 15 deletions
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"java.configuration.updateBuildConfiguration": "interactive"
|
||||||
|
}
|
34
Jenkinsfile
vendored
34
Jenkinsfile
vendored
|
@ -25,6 +25,10 @@ pipeline {
|
||||||
|
|
||||||
|
|
||||||
mv build/app/outputs/bundle/release/app-release.aab build/app/outputs/bundle/release/timetrack.aab
|
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,36 @@ pipeline {
|
||||||
archiveArtifacts artifacts: "build/app/outputs/flutter-apk/timetrack.apk"
|
archiveArtifacts artifacts: "build/app/outputs/flutter-apk/timetrack.apk"
|
||||||
archiveArtifacts artifacts: "build/app/outputs/bundle/release/timetrack.aab"
|
archiveArtifacts artifacts: "build/app/outputs/bundle/release/timetrack.aab"
|
||||||
|
|
||||||
|
archiveArtifacts artifacts: "php.tgz"
|
||||||
|
|
||||||
|
cleanWs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
cleanWs()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
17
README.md
17
README.md
|
@ -24,16 +24,17 @@ The app does not store data locally, due to the way android permissions function
|
||||||
|
|
||||||
# Implementation
|
# Implementation
|
||||||
|
|
||||||
- [ ] Basic UI
|
- [x] Basic UI
|
||||||
- [x] Permissions
|
- [x] Permissions
|
||||||
- [x] Automatic updates
|
- [x] Automatic updates
|
||||||
- [ ] GPS Tracking
|
- [x] GPS Tracking
|
||||||
- [ ] Formatting GPS on a viewable map
|
- [x] Formatting GPS on a viewable map
|
||||||
- [ ] Track driving hours
|
- [x] Track driving hours
|
||||||
- [ ] Track trips
|
- [x] Track trips
|
||||||
- [ ] Track stops/deliveries
|
- [x] Track stops/deliveries
|
||||||
- [ ] Track trip base pay
|
- [x] Track trip base pay
|
||||||
- [ ] Track each delivery's tips
|
- [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.
|
- [ ] Basic version of the app in readonly mode when deployed on a web server.
|
||||||
- [ ] Backend server
|
- [ ] Backend server
|
||||||
- [ ] PHP?
|
- [ ] PHP?
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
{
|
{
|
||||||
"alpha": "1.0.0-dev.4"
|
"alpha": "1.0.0-dev.10",
|
||||||
|
"beta": "1.0.0-beta.3"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,22 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:geolocator/geolocator.dart';
|
||||||
|
|
||||||
class TTConsts {
|
class TTConsts {
|
||||||
static const 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.4";
|
static get SESSION_SERVER =>
|
||||||
|
"https://api.zontreck.com/timetrack/${UPDATE_CHANNEL}/timetrack.php";
|
||||||
|
|
||||||
|
static const VERSION = "1.0.0-beta.3";
|
||||||
|
|
||||||
static bool UPDATE_AVAILABLE = false;
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
static Future<void> checkUpdate() async {
|
static Future<void> checkUpdate() async {
|
||||||
Dio dio = Dio();
|
Dio dio = Dio();
|
||||||
|
|
385
lib/data.dart
Normal file
385
lib/data.dart
Normal file
|
@ -0,0 +1,385 @@
|
||||||
|
import 'dart:async';
|
||||||
|
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';
|
||||||
|
|
||||||
|
class SessionData {
|
||||||
|
static DateTime StartTime = DateTime(0);
|
||||||
|
|
||||||
|
static bool IsOnTheClock = false;
|
||||||
|
|
||||||
|
static List<Trip> Trips = [];
|
||||||
|
|
||||||
|
static Delivery? currentDelivery;
|
||||||
|
static Trip? currentTrip;
|
||||||
|
static List<Position> positions = [];
|
||||||
|
static late StreamSubscription<Position> _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;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
StartTime = DateTime.now();
|
||||||
|
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.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_listener = Geolocator.getPositionStream(
|
||||||
|
locationSettings: TTConsts.LOCATION_SETTINGS,
|
||||||
|
).listen((pos) {
|
||||||
|
if (!IsOnTheClock) {
|
||||||
|
_listener.cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
positions.add(pos);
|
||||||
|
|
||||||
|
SessionData.Calls.dispatch();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> Logout() async {
|
||||||
|
IsOnTheClock = false;
|
||||||
|
currentDelivery = null;
|
||||||
|
currentTrip = null;
|
||||||
|
_listener.cancel();
|
||||||
|
|
||||||
|
var saveData = SaveData();
|
||||||
|
print(saveData);
|
||||||
|
|
||||||
|
Trips = [];
|
||||||
|
positions = [];
|
||||||
|
|
||||||
|
Dio dio = Dio();
|
||||||
|
Map<String, dynamic> payload = {"cmd": "create", "data": saveData};
|
||||||
|
|
||||||
|
var reply = await dio.post(
|
||||||
|
TTConsts.SESSION_SERVER,
|
||||||
|
data: json.encode(payload),
|
||||||
|
);
|
||||||
|
Map<String, dynamic> replyJs = json.decode(reply.data as String);
|
||||||
|
if (replyJs["status"] == "ok") {
|
||||||
|
print("Successful upload");
|
||||||
|
LastSessionID = replyJs['session'] as String;
|
||||||
|
Calls.dispatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, dynamic> SaveData() {
|
||||||
|
Map<String, dynamic> saveData = {};
|
||||||
|
|
||||||
|
List<Map<String, dynamic>> _trips = [];
|
||||||
|
for (var trip in Trips) {
|
||||||
|
_trips.add(trip.toJsonMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Map<String, dynamic>> _pos = [];
|
||||||
|
for (var pos in positions) {
|
||||||
|
_pos.add(pos.toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
saveData["trips"] = _trips;
|
||||||
|
saveData["positions"] = _pos;
|
||||||
|
|
||||||
|
return saveData;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> DownloadData() async {
|
||||||
|
Dio dio = Dio();
|
||||||
|
Map<String, dynamic> 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<String, dynamic> _js = json.decode(js);
|
||||||
|
if (_js.containsKey("error")) {
|
||||||
|
LastSessionID = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<Map<String, dynamic>> _trips =
|
||||||
|
_js['trips'] as List<Map<String, dynamic>>;
|
||||||
|
List<Map<String, dynamic>> _pos =
|
||||||
|
_js['positions'] as List<Map<String, dynamic>>;
|
||||||
|
|
||||||
|
for (var trip in _trips) {
|
||||||
|
Trips.add(Trip.fromJsonMap(trip));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var position in _pos) {
|
||||||
|
positions.add(Position.fromMap(position));
|
||||||
|
}
|
||||||
|
|
||||||
|
IsReadOnly = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Position> GetNewLocation() async {
|
||||||
|
Position pos = await Geolocator.getCurrentPosition(
|
||||||
|
locationSettings: TTConsts.LOCATION_SETTINGS,
|
||||||
|
);
|
||||||
|
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Trip GetNewTrip({required double basePay}) {
|
||||||
|
currentTrip = Trip(BasePay: basePay);
|
||||||
|
Trips.add(currentTrip!);
|
||||||
|
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;
|
||||||
|
Position? endLocation;
|
||||||
|
DateTime StartTime = DateTime.now();
|
||||||
|
|
||||||
|
Delivery() {
|
||||||
|
StartTime = DateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> MarkEndLocation() async {
|
||||||
|
var pos = await SessionData.GetNewLocation();
|
||||||
|
endLocation = pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJsonMap() {
|
||||||
|
return {
|
||||||
|
"tip": TipAmount,
|
||||||
|
"start": StartTime.toString(),
|
||||||
|
"endPos": endLocation?.toJson() ?? "incomplete",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static Delivery fromMap(Map<String, dynamic> 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<Delivery> deliveries = [];
|
||||||
|
|
||||||
|
DateTime StartTime = DateTime(0);
|
||||||
|
|
||||||
|
double BasePay = 0.0;
|
||||||
|
|
||||||
|
Trip({required this.BasePay}) {
|
||||||
|
StartTime = DateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
Delivery startNewDelivery() {
|
||||||
|
var delivery = Delivery();
|
||||||
|
deliveries.add(delivery);
|
||||||
|
|
||||||
|
return delivery;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJsonMap() {
|
||||||
|
Map<String, Object> _trip = {"start": StartTime.toString(), "pay": BasePay};
|
||||||
|
List<Map<String, dynamic>> _dropOffs = [];
|
||||||
|
for (var delivery in deliveries) {
|
||||||
|
_dropOffs.add(delivery.toJsonMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
_trip["deliveries"] = _dropOffs;
|
||||||
|
|
||||||
|
return _trip;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Trip fromJsonMap(Map<String, dynamic> jsx) {
|
||||||
|
Trip trip = Trip(BasePay: 0);
|
||||||
|
trip.BasePay = jsx['pay'] as double;
|
||||||
|
trip.StartTime = DateTime.parse(jsx['start'] as String);
|
||||||
|
trip.deliveries = [];
|
||||||
|
List<Map<String, dynamic>> _dropOffs =
|
||||||
|
jsx['deliveries'] as List<Map<String, dynamic>>;
|
||||||
|
|
||||||
|
for (var dropOff in _dropOffs) {
|
||||||
|
trip.deliveries.add(Delivery.fromMap(dropOff));
|
||||||
|
}
|
||||||
|
|
||||||
|
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!();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,19 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:timetrack/consts.dart';
|
import 'package:timetrack/consts.dart';
|
||||||
|
import 'package:timetrack/data.dart';
|
||||||
import 'package:timetrack/pages/MainApp.dart';
|
import 'package:timetrack/pages/MainApp.dart';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
await TTConsts.checkUpdate();
|
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());
|
runApp(MainApp());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:libacflutter/Constants.dart';
|
import 'package:libacflutter/Constants.dart';
|
||||||
|
import 'package:libacflutter/Prompt.dart';
|
||||||
import 'package:timetrack/consts.dart';
|
import 'package:timetrack/consts.dart';
|
||||||
|
import 'package:timetrack/data.dart';
|
||||||
|
|
||||||
class HomePage extends StatefulWidget {
|
class HomePage extends StatefulWidget {
|
||||||
const HomePage({super.key});
|
const HomePage({super.key});
|
||||||
|
@ -18,6 +21,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(
|
||||||
|
@ -51,10 +70,219 @@ class _HomePageState extends State<HomePage> {
|
||||||
},
|
},
|
||||||
leading: Icon(Icons.update),
|
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("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 = DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
SessionData.Trips = [];
|
||||||
|
SessionData.currentDelivery = null;
|
||||||
|
SessionData.currentTrip = null;
|
||||||
|
SessionData.positions = [];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
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)
|
||||||
|
ListTile(
|
||||||
|
title: Text("Session ID"),
|
||||||
|
subtitle: Text("${SessionData.LastSessionID} - Tap to copy"),
|
||||||
|
onTap: () {
|
||||||
|
Clipboard.setData(
|
||||||
|
ClipboardData(text: SessionData.LastSessionID),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
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 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: [
|
||||||
|
Text(
|
||||||
|
"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(),
|
||||||
|
if (SessionData.currentDelivery != null) GetDeliveryWidgets(),
|
||||||
|
if (SessionData.currentTrip == null) GetNonTripWidgets(),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
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/UpdateSettings.dart';
|
import 'package:timetrack/pages/UpdateSettings.dart';
|
||||||
|
import 'package:timetrack/pages/WebMainPage.dart';
|
||||||
|
import 'package:timetrack/pages/WorkData.dart';
|
||||||
|
|
||||||
class MainApp extends StatefulWidget {
|
class MainApp extends StatefulWidget {
|
||||||
const MainApp({super.key});
|
const MainApp({super.key});
|
||||||
|
@ -21,7 +26,12 @@ class MainAppState extends State<MainApp> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: "Time Tracker",
|
title: "Time Tracker",
|
||||||
routes: {"/": (ctx) => HomePage(), "/upd": (ctx) => UpdateSettingsPage()},
|
routes: {
|
||||||
|
"/": (ctx) => Platform.isAndroid ? HomePage() : WebMain(),
|
||||||
|
"/upd": (ctx) => UpdateSettingsPage(),
|
||||||
|
"/map": (ctx) => MapPage(),
|
||||||
|
"/work": (ctx) => WorkDataPage(),
|
||||||
|
},
|
||||||
theme: ThemeData.dark(),
|
theme: ThemeData.dark(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
166
lib/pages/MapPage.dart
Normal file
166
lib/pages/MapPage.dart
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
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<StatefulWidget> createState() {
|
||||||
|
return _MapPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MapPage extends State<MapPage> {
|
||||||
|
MapController controller = MapController();
|
||||||
|
LatLng initialPosition = LatLng(0, 0);
|
||||||
|
List<LatLng> PointMap = [];
|
||||||
|
List<Marker> Markers = [];
|
||||||
|
bool autorefresh = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
SessionData.Calls.MapCallback = call;
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void call() {
|
||||||
|
if (autorefresh) {
|
||||||
|
didChangeDependencies();
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
PointMap = [];
|
||||||
|
Markers = [];
|
||||||
|
|
||||||
|
var firstPos = SessionData.positions[0];
|
||||||
|
initialPosition = LatLng(firstPos.latitude, firstPos.longitude);
|
||||||
|
|
||||||
|
for (var position in SessionData.positions) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
SessionData.Calls.MapCallback = null;
|
||||||
|
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
autorefresh = !autorefresh;
|
||||||
|
},
|
||||||
|
icon:
|
||||||
|
autorefresh
|
||||||
|
? Icon(Icons.play_disabled)
|
||||||
|
: Icon(Icons.play_circle),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
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: 8,
|
||||||
|
borderColor: Colors.blue,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
MarkerLayer(markers: Markers),
|
||||||
|
RichAttributionWidget(
|
||||||
|
attributions: [
|
||||||
|
TextSourceAttribution('OpenStreetMap contributors'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
@ -76,7 +93,7 @@ class _UpdSet extends State<UpdateSettingsPage> {
|
||||||
onTap: () {
|
onTap: () {
|
||||||
OtaUpdate()
|
OtaUpdate()
|
||||||
.execute(
|
.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",
|
destinationFilename: "timetrack.apk",
|
||||||
)
|
)
|
||||||
.listen((OtaEvent event) {
|
.listen((OtaEvent event) {
|
||||||
|
|
128
lib/pages/WebMainPage.dart
Normal file
128
lib/pages/WebMainPage.dart
Normal file
|
@ -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<StatefulWidget> createState() {
|
||||||
|
return _WebMain();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WebMain extends State<WebMain> {
|
||||||
|
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)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
81
lib/pages/WorkData.dart
Normal file
81
lib/pages/WorkData.dart
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:libacflutter/Constants.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 Estimated Miles: ${SessionData.GetTotalMilesAsString()}\n(Note: The miles displayed above may not be 100% accurate)",
|
||||||
|
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.4
|
version: 1.0.0-beta.3
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.7.2
|
sdk: ^3.7.2
|
||||||
|
@ -42,6 +42,9 @@ dependencies:
|
||||||
version: 1.0.31525+0222
|
version: 1.0.31525+0222
|
||||||
dio: ^5.8.0+1
|
dio: ^5.8.0+1
|
||||||
ota_update: ^7.0.1
|
ota_update: ^7.0.1
|
||||||
|
geolocator: ^14.0.0
|
||||||
|
flutter_map: ^8.1.1
|
||||||
|
latlong2: ^0.9.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
1
server/add session.sql
Normal file
1
server/add session.sql
Normal file
|
@ -0,0 +1 @@
|
||||||
|
INSERT INTO `sessions` (`ID`, `timestamp`) VALUES (uuid(), current_timestamp());
|
84
server/php/timetrack.php
Normal file
84
server/php/timetrack.php
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
<?php
|
||||||
|
if(!defined("COMMON"))
|
||||||
|
require ("/srv/sites/api.zontreck.com/Common.php");
|
||||||
|
|
||||||
|
|
||||||
|
// Get Database: timetrack
|
||||||
|
$DB = get_DB("timetrack");
|
||||||
|
|
||||||
|
// Get query information
|
||||||
|
$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
|
||||||
|
$result = $DB->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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
?>
|
Loading…
Add table
Add a link
Reference in a new issue