From 3ca55ac09cabe55403137e6acd029bc1f7019ed3 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 21 Jun 2026 09:21:57 -0500 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20GpsTrackingHelper=20=E2=80=94=20fle?= =?UTF-8?q?et=20positions,=20drive=20history,=20speed=20alerts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Helper/GpsTrackingHelper.php | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 source/packages/plg_system_mokosuitefield/src/Helper/GpsTrackingHelper.php diff --git a/source/packages/plg_system_mokosuitefield/src/Helper/GpsTrackingHelper.php b/source/packages/plg_system_mokosuitefield/src/Helper/GpsTrackingHelper.php new file mode 100644 index 0000000..99c8cf8 --- /dev/null +++ b/source/packages/plg_system_mokosuitefield/src/Helper/GpsTrackingHelper.php @@ -0,0 +1,112 @@ + 90 || $longitude < -180 || $longitude > 180) { + throw new \InvalidArgumentException('Invalid GPS coordinates.'); + } + + $db = Factory::getContainer()->get(DatabaseInterface::class); + + $ping = (object) [ + 'vehicle_id' => $vehicleId, + 'latitude' => round($latitude, 6), + 'longitude' => round($longitude, 6), + 'speed_mph' => max(0, round($speed, 1)), + 'recorded_at'=> Factory::getDate()->toSql(), + ]; + + $db->insertObject('#__mokosuitefield_gps_pings', $ping); + + return true; + } + + /** + * Get latest position for all active vehicles. + */ + public static function getFleetPositions(): array + { + $db = Factory::getContainer()->get(DatabaseInterface::class); + + $db->setQuery($db->getQuery(true) + ->select('v.id AS vehicle_id, v.name AS vehicle_name, v.license_plate') + ->select('gp.latitude, gp.longitude, gp.speed_mph, gp.recorded_at') + ->select('cd.name AS assigned_tech') + ->from($db->quoteName('#__mokosuitefield_vehicles', 'v')) + ->join('LEFT', '(SELECT g1.* FROM #__mokosuitefield_gps_pings g1' + . ' INNER JOIN (SELECT vehicle_id, MAX(recorded_at) AS max_at' + . ' FROM #__mokosuitefield_gps_pings GROUP BY vehicle_id) g2' + . ' ON g1.vehicle_id = g2.vehicle_id AND g1.recorded_at = g2.max_at) AS gp' + . ' ON gp.vehicle_id = v.id') + ->join('LEFT', $db->quoteName('#__mokosuitefield_technicians', 't') . ' ON t.vehicle_id = v.id') + ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = t.contact_id') + ->where($db->quoteName('v.status') . ' = ' . $db->quote('active')) + ->order('v.name ASC')); + + return $db->loadObjectList() ?: []; + } + + /** + * Get drive history for a vehicle on a specific date. + */ + public static function getDriveHistory(int $vehicleId, string $date = ''): array + { + $date = $date ?: date('Y-m-d'); + + if (!\DateTime::createFromFormat('Y-m-d', $date)) { + throw new \InvalidArgumentException('Date must be Y-m-d format.'); + } + + $db = Factory::getContainer()->get(DatabaseInterface::class); + + $db->setQuery($db->getQuery(true) + ->select('gp.latitude, gp.longitude, gp.speed_mph, gp.recorded_at') + ->from($db->quoteName('#__mokosuitefield_gps_pings', 'gp')) + ->where('gp.vehicle_id = ' . (int) $vehicleId) + ->where('DATE(gp.recorded_at) = ' . $db->quote($date)) + ->order('gp.recorded_at ASC')); + + return $db->loadObjectList() ?: []; + } + + /** + * Get vehicles currently exceeding speed threshold. + */ + public static function getSpeeding(float $thresholdMph = 70): array + { + $db = Factory::getContainer()->get(DatabaseInterface::class); + + $db->setQuery($db->getQuery(true) + ->select('v.id AS vehicle_id, v.name AS vehicle_name, v.license_plate') + ->select('gp.latitude, gp.longitude, gp.speed_mph, gp.recorded_at') + ->select('cd.name AS assigned_tech') + ->from($db->quoteName('#__mokosuitefield_vehicles', 'v')) + ->join('INNER', '(SELECT g1.* FROM #__mokosuitefield_gps_pings g1' + . ' INNER JOIN (SELECT vehicle_id, MAX(recorded_at) AS max_at' + . ' FROM #__mokosuitefield_gps_pings GROUP BY vehicle_id) g2' + . ' ON g1.vehicle_id = g2.vehicle_id AND g1.recorded_at = g2.max_at) AS gp' + . ' ON gp.vehicle_id = v.id') + ->join('LEFT', $db->quoteName('#__mokosuitefield_technicians', 't') . ' ON t.vehicle_id = v.id') + ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = t.contact_id') + ->where($db->quoteName('v.status') . ' = ' . $db->quote('active')) + ->where('gp.speed_mph > ' . (float) $thresholdMph) + ->where('gp.recorded_at > DATE_SUB(NOW(), INTERVAL 10 MINUTE)') + ->order('gp.speed_mph DESC')); + + return $db->loadObjectList() ?: []; + } +} -- 2.52.0 From e01a08c664c93af3acca072d6fba1def045e7eeb Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 21 Jun 2026 14:22:18 +0000 Subject: [PATCH 2/4] chore(version): auto-bump patch 01.06.01-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 79f6dd3..03bc2ef 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 01.06.00 +# VERSION: 01.06.01 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index ccb9ae9..e27e100 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 01.06.00 + 01.06.01 2026-06-12 Moko Consulting hello@mokoconsulting.tech -- 2.52.0 From 1983d1c4efa46bba562726cd93e5b6ebe006b2cb Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 21 Jun 2026 14:22:27 +0000 Subject: [PATCH 3/4] chore(version): pre-release bump to 01.06.02-dev [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- source/pkg_mokosuitefield.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 03bc2ef..3c2e9d4 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 01.06.01 +# VERSION: 01.06.02 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/source/pkg_mokosuitefield.xml b/source/pkg_mokosuitefield.xml index e27e100..af1348a 100644 --- a/source/pkg_mokosuitefield.xml +++ b/source/pkg_mokosuitefield.xml @@ -2,7 +2,7 @@ Package - MokoSuite Field mokosuitefield - 01.06.01 + 01.06.02 2026-06-12 Moko Consulting hello@mokoconsulting.tech -- 2.52.0 From d307630e99592455e9d2a07a9fa14358651ef0ff Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 21 Jun 2026 14:06:48 +0000 Subject: [PATCH 4/4] chore: sync issue-branch.yml from Template-Generic [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 3c2e9d4..75a6963 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 01.06.02 +# VERSION: 01.00.00 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" -- 2.52.0