feat: scaffold MokoSuiteTaxi Joomla 6 package #1
+12
@@ -0,0 +1,12 @@
|
||||
# MokoSuite standard ignores
|
||||
.claude/
|
||||
.mcp.json
|
||||
TODO.md
|
||||
vendor/
|
||||
node_modules/
|
||||
.env
|
||||
*.min.css
|
||||
*.min.js
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.ppk
|
||||
@@ -0,0 +1,19 @@
|
||||
<!--
|
||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
INGROUP: MokoSuiteTaxi.Documentation
|
||||
BRIEF: Version history using Keep a Changelog
|
||||
-->
|
||||
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **Repository** -- initial repo creation with scaffolding
|
||||
- **System Plugin** -- Extension class, service provider, 6 helpers
|
||||
- **SQL Schema** -- 8 tables: vehicles, drivers, zones, fares, rides, ratings, dispatch, shifts
|
||||
- **Admin Component** -- 6 views: dashboard, rides, vehicles, drivers, zones, fares
|
||||
- **Webservices Plugin** -- 7 API routes: rides, vehicles, drivers, zones, fares, dispatch, ratings
|
||||
- **Configuration** -- 16 settings across general, dispatch, fares, drivers, booking
|
||||
- **Access Control** -- 21 permissions for granular role-based access
|
||||
@@ -0,0 +1,32 @@
|
||||
# MokoSuiteTaxi
|
||||
|
||||
Ride-hailing, dispatch, fleet management, fare zones, and driver scheduling for Joomla 6.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Package** | `pkg_mokosuitetaxi` |
|
||||
| **Layer** | 2 (requires: Client, CRM) |
|
||||
| **Language** | PHP 8.3+ |
|
||||
| **Branch** | develop on `dev`, merge to `main` (protected) |
|
||||
| **Wiki** | [MokoSuiteTaxi Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteTaxi/wiki) |
|
||||
|
||||
## Architecture
|
||||
|
||||
Joomla **package** -- Layer 2 add-on. CRM contacts as riders/drivers, zone-based dispatch with surge pricing.
|
||||
|
||||
## Rules
|
||||
|
||||
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, `*.min.css`/`*.min.js`
|
||||
- **Attribution**: `Authored-by: Moko Consulting`
|
||||
- **Workflow directory**: `.mokogitea/`
|
||||
- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoCLI/wiki)
|
||||
- **Changelog**: `[Unreleased]` only -- release system assigns versions
|
||||
|
||||
## Coding Standards
|
||||
|
||||
- PHP 8.3+ / Joomla 6 patterns
|
||||
- `$this->getDatabase()` in models, `Factory::getContainer()->get(DatabaseInterface::class)` in helpers
|
||||
- `Factory::getApplication()->getIdentity()` for user
|
||||
- FOR UPDATE locking on ride acceptance/dispatch to prevent race conditions
|
||||
@@ -1,3 +1,44 @@
|
||||
# MokoSuiteTaxi
|
||||
<!--
|
||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-->
|
||||
|
||||
MokoSuite Taxi — ride-hailing, dispatch, fleet management, fare zones, driver scheduling for Joomla 6
|
||||
# MokoSuite Taxi
|
||||
|
||||
Ride-hailing, dispatch, fleet management, fare zones, and driver scheduling module for MokoSuite on Joomla 6.
|
||||
|
||||
## Overview
|
||||
|
||||
MokoSuiteTaxi is a Layer 2 module in the MokoSuite platform, building on MokoSuiteClient (Layer 0) and MokoSuiteCRM (Layer 1) to provide complete taxi and ride-hailing operations management.
|
||||
|
||||
## Features
|
||||
|
||||
- **Ride Management** -- on-demand, scheduled, airport, and corporate ride types
|
||||
- **Dispatch Engine** -- auto-dispatch with zone-aware driver matching and FOR UPDATE locking
|
||||
- **Fleet Management** -- vehicle tracking, insurance/inspection expiry alerts, maintenance status
|
||||
- **Driver Management** -- profiles linked to CRM contacts, approval workflow, background checks
|
||||
- **Fare Zones** -- GeoJSON polygon boundaries with radius fallback, zone-based pricing
|
||||
- **Dynamic Pricing** -- surge multiplier based on demand/supply ratio, night surcharges, peak hours
|
||||
- **Ratings** -- bidirectional rider/driver ratings with feedback tags
|
||||
- **Shift Scheduling** -- driver shift management with earnings tracking
|
||||
|
||||
## Requirements
|
||||
|
||||
- Joomla 6.x
|
||||
- PHP 8.3+
|
||||
- MokoSuiteClient (Layer 0)
|
||||
- MokoSuiteCRM (Layer 1)
|
||||
|
||||
## Installation
|
||||
|
||||
Install via Joomla Extension Manager using the package file `pkg_mokosuitetaxi.zip`.
|
||||
|
||||
## License
|
||||
|
||||
GNU General Public License v3.0 or later -- see [LICENSE](LICENSE).
|
||||
|
||||
## Links
|
||||
|
||||
- [Documentation](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteTaxi/wiki)
|
||||
- [Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteTaxi/issues)
|
||||
- [MokoSuite Platform](https://mokoconsulting.tech)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<access component="com_mokosuitetaxi">
|
||||
<section name="component">
|
||||
<action name="core.admin" title="JACTION_ADMIN" />
|
||||
<action name="core.options" title="JACTION_OPTIONS" />
|
||||
<action name="core.manage" title="JACTION_MANAGE" />
|
||||
<action name="core.create" title="JACTION_CREATE" />
|
||||
<action name="core.delete" title="JACTION_DELETE" />
|
||||
<action name="core.edit" title="JACTION_EDIT" />
|
||||
<action name="taxi.rides.manage" title="Manage Rides" />
|
||||
<action name="taxi.rides.dispatch" title="Dispatch Rides" />
|
||||
<action name="taxi.rides.cancel" title="Cancel Rides" />
|
||||
<action name="taxi.rides.complete" title="Complete Rides" />
|
||||
<action name="taxi.drivers.manage" title="Manage Drivers" />
|
||||
<action name="taxi.drivers.approve" title="Approve Drivers" />
|
||||
<action name="taxi.drivers.suspend" title="Suspend Drivers" />
|
||||
<action name="taxi.vehicles.manage" title="Manage Vehicles" />
|
||||
<action name="taxi.zones.manage" title="Manage Zones" />
|
||||
<action name="taxi.fares.manage" title="Manage Fare Rules" />
|
||||
<action name="taxi.dispatch.view" title="View Dispatch Queue" />
|
||||
<action name="taxi.ratings.view" title="View Ratings" />
|
||||
<action name="taxi.shifts.manage" title="Manage Shifts" />
|
||||
<action name="taxi.reports" title="View Reports" />
|
||||
<action name="taxi.settings" title="Manage Settings" />
|
||||
</section>
|
||||
</access>
|
||||
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<config>
|
||||
<fieldset name="general" label="General">
|
||||
<field name="default_currency" type="text" default="USD" label="Default Currency" />
|
||||
<field name="distance_unit" type="list" default="mi" label="Distance Unit">
|
||||
<option value="mi">Miles</option>
|
||||
<option value="km">Kilometres</option>
|
||||
</field>
|
||||
<field name="timezone" type="text" default="UTC" label="Operating Timezone" />
|
||||
</fieldset>
|
||||
<fieldset name="dispatch" label="Dispatch">
|
||||
<field name="auto_dispatch" type="radio" default="1" label="Auto-Dispatch Rides" class="btn-group btn-group-yesno">
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
<field name="driver_search_radius" type="number" default="10" label="Driver Search Radius" hint="In distance units" />
|
||||
<field name="ride_timeout_minutes" type="number" default="5" label="Ride Accept Timeout (min)" />
|
||||
<field name="max_dispatch_attempts" type="number" default="5" label="Max Dispatch Attempts" />
|
||||
<field name="max_active_rides" type="number" default="1" label="Max Concurrent Rides per Driver" />
|
||||
</fieldset>
|
||||
<fieldset name="fares" label="Fares">
|
||||
<field name="base_fare" type="number" default="2.50" step="0.01" label="Default Base Fare" />
|
||||
<field name="per_distance" type="number" default="1.50" step="0.01" label="Per Distance Unit Rate" />
|
||||
<field name="per_minute" type="number" default="0.25" step="0.01" label="Per Minute Rate" />
|
||||
<field name="surge_enabled" type="radio" default="1" label="Enable Surge Pricing" class="btn-group btn-group-yesno">
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
<field name="max_surge_multiplier" type="number" default="3.0" step="0.1" label="Max Surge Multiplier" />
|
||||
<field name="cancellation_fee" type="number" default="5.00" step="0.01" label="Cancellation Fee" />
|
||||
</fieldset>
|
||||
<fieldset name="drivers" label="Drivers">
|
||||
<field name="min_driver_rating" type="number" default="4.0" step="0.1" label="Minimum Driver Rating" />
|
||||
<field name="commission_rate" type="number" default="20" step="1" label="Platform Commission (%)" />
|
||||
<field name="require_background_check" type="radio" default="1" label="Require Background Check" class="btn-group btn-group-yesno">
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
</fieldset>
|
||||
<fieldset name="booking" label="Booking">
|
||||
<field name="enable_scheduled_rides" type="radio" default="1" label="Allow Scheduled Rides" class="btn-group btn-group-yesno">
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
<field name="advance_booking_days" type="number" default="7" label="Max Advance Booking (days)" />
|
||||
<field name="enable_ride_sharing" type="radio" default="0" label="Enable Ride Sharing" class="btn-group btn-group-yesno">
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
</fieldset>
|
||||
</config>
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* Authored-by: Moko Consulting
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Extension\ComponentInterface;
|
||||
use Joomla\CMS\Extension\MVCComponent;
|
||||
use Joomla\CMS\Dispatcher\ComponentDispatcherFactoryInterface;
|
||||
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
|
||||
use Joomla\DI\Container;
|
||||
use Joomla\DI\ServiceProviderInterface;
|
||||
|
||||
return new class implements ServiceProviderInterface {
|
||||
public function register(Container $container): void {
|
||||
$container->set(ComponentInterface::class, function (Container $container) {
|
||||
$c = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class));
|
||||
$c->setMVCFactory($container->get(MVCFactoryInterface::class));
|
||||
return $c;
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* Authored-by: Moko Consulting
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoSuiteTaxi\Administrator\Controller;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\MVC\Controller\BaseController;
|
||||
|
||||
class DisplayController extends BaseController
|
||||
{
|
||||
protected $default_view = 'taxidashboard';
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* Authored-by: Moko Consulting
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoSuiteTaxi\Administrator\Model;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
||||
|
||||
class TaxiDashboardModel extends BaseDatabaseModel
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* Authored-by: Moko Consulting
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoSuiteTaxi\Administrator\View\TaxiDashboard;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
public function display($tpl = null): void
|
||||
{
|
||||
ToolbarHelper::title('MokoSuite Taxi - Dashboard');
|
||||
parent::display($tpl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* Authored-by: Moko Consulting
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoSuiteTaxi\Administrator\View\TaxiDrivers;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
public function display($tpl = null): void
|
||||
{
|
||||
ToolbarHelper::title('MokoSuite Taxi - Drivers');
|
||||
parent::display($tpl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* Authored-by: Moko Consulting
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoSuiteTaxi\Administrator\View\TaxiFares;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
public function display($tpl = null): void
|
||||
{
|
||||
ToolbarHelper::title('MokoSuite Taxi - Fare Rules');
|
||||
parent::display($tpl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* Authored-by: Moko Consulting
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoSuiteTaxi\Administrator\View\TaxiRides;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
public function display($tpl = null): void
|
||||
{
|
||||
ToolbarHelper::title('MokoSuite Taxi - Rides');
|
||||
parent::display($tpl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* Authored-by: Moko Consulting
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoSuiteTaxi\Administrator\View\TaxiVehicles;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
public function display($tpl = null): void
|
||||
{
|
||||
ToolbarHelper::title('MokoSuite Taxi - Vehicles');
|
||||
parent::display($tpl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* Authored-by: Moko Consulting
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoSuiteTaxi\Administrator\View\TaxiZones;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
public function display($tpl = null): void
|
||||
{
|
||||
ToolbarHelper::title('MokoSuite Taxi - Zones');
|
||||
parent::display($tpl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php defined('_JEXEC') or die; ?><div><h2>Dashboard</h2><p>Coming soon.</p></div>
|
||||
@@ -0,0 +1 @@
|
||||
<?php defined('_JEXEC') or die; ?><div><h2>Drivers</h2><p>Coming soon.</p></div>
|
||||
@@ -0,0 +1 @@
|
||||
<?php defined('_JEXEC') or die; ?><div><h2>Fares</h2><p>Coming soon.</p></div>
|
||||
@@ -0,0 +1 @@
|
||||
<?php defined('_JEXEC') or die; ?><div><h2>Rides</h2><p>Coming soon.</p></div>
|
||||
@@ -0,0 +1 @@
|
||||
<?php defined('_JEXEC') or die; ?><div><h2>Vehicles</h2><p>Coming soon.</p></div>
|
||||
@@ -0,0 +1 @@
|
||||
<?php defined('_JEXEC') or die; ?><div><h2>Zones</h2><p>Coming soon.</p></div>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<extension type="component" method="upgrade">
|
||||
<name>MokoSuite Taxi</name>
|
||||
<author>Moko Consulting</author>
|
||||
<creationDate>2026-06-27</creationDate>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting.</copyright>
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<version>06.00.00</version>
|
||||
<namespace path="src">Moko\Component\MokoSuiteTaxi</namespace>
|
||||
<administration>
|
||||
<files folder="admin"><folder>services</folder><folder>src</folder><folder>tmpl</folder></files>
|
||||
<menu>COM_MOKOSUITETAXI</menu>
|
||||
</administration>
|
||||
</extension>
|
||||
@@ -0,0 +1,2 @@
|
||||
PLG_SYSTEM_MOKOSUITETAXI="System - MokoSuite Taxi"
|
||||
PLG_SYSTEM_MOKOSUITETAXI_DESC="Ride-hailing, dispatch, fleet management, fare zones, and driver scheduling"
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
PLG_SYSTEM_MOKOSUITETAXI="System - MokoSuite Taxi"
|
||||
PLG_SYSTEM_MOKOSUITETAXI_DESC="Ride-hailing, dispatch, fleet management, fare zones, and driver scheduling"
|
||||
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<extension type="plugin" group="system" method="upgrade">
|
||||
<name>System - MokoSuite Taxi</name>
|
||||
<element>mokosuitetaxi</element>
|
||||
<author>Moko Consulting</author>
|
||||
<creationDate>2026-06-27</creationDate>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>06.00.00</version>
|
||||
<php_minimum>8.3</php_minimum>
|
||||
<description>PLG_SYSTEM_MOKOSUITETAXI_DESC</description>
|
||||
<namespace path="src">Moko\Plugin\System\MokoSuiteTaxi</namespace>
|
||||
<files>
|
||||
<folder>src</folder>
|
||||
<folder>services</folder>
|
||||
<folder>language</folder>
|
||||
<folder>sql</folder>
|
||||
</files>
|
||||
<languages folder="language">
|
||||
<language tag="en-GB">en-GB/plg_system_mokosuitetaxi.ini</language>
|
||||
<language tag="en-GB">en-GB/plg_system_mokosuitetaxi.sys.ini</language>
|
||||
</languages>
|
||||
<install><sql><file driver="mysql" charset="utf8">sql/install.mysql.sql</file></sql></install>
|
||||
<uninstall><sql><file driver="mysql" charset="utf8">sql/uninstall.mysql.sql</file></sql></uninstall>
|
||||
<config>
|
||||
<fields name="params">
|
||||
<fieldset name="basic" label="Taxi Defaults">
|
||||
<field name="default_currency" type="text" default="USD" label="Default Currency" />
|
||||
<field name="distance_unit" type="list" default="mi" label="Distance Unit">
|
||||
<option value="mi">Miles</option>
|
||||
<option value="km">Kilometres</option>
|
||||
</field>
|
||||
<field name="auto_dispatch" type="radio" default="1" label="Auto-Dispatch Rides" class="btn-group btn-group-yesno">
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
<field name="driver_search_radius" type="number" default="10" label="Driver Search Radius" hint="In distance units" />
|
||||
</fieldset>
|
||||
<fieldset name="fares" label="Fare Settings">
|
||||
<field name="base_fare" type="number" default="2.50" step="0.01" label="Default Base Fare" />
|
||||
<field name="per_distance" type="number" default="1.50" step="0.01" label="Per Distance Unit Rate" />
|
||||
<field name="per_minute" type="number" default="0.25" step="0.01" label="Per Minute Rate" />
|
||||
<field name="surge_enabled" type="radio" default="1" label="Enable Surge Pricing" class="btn-group btn-group-yesno">
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
<field name="max_surge_multiplier" type="number" default="3.0" step="0.1" label="Max Surge Multiplier" />
|
||||
<field name="cancellation_fee" type="number" default="5.00" step="0.01" label="Cancellation Fee" />
|
||||
</fieldset>
|
||||
<fieldset name="drivers" label="Driver Settings">
|
||||
<field name="min_driver_rating" type="number" default="4.0" step="0.1" label="Minimum Driver Rating" />
|
||||
<field name="commission_rate" type="number" default="20" step="1" label="Platform Commission (%)" />
|
||||
<field name="ride_timeout_minutes" type="number" default="5" label="Ride Accept Timeout (min)" />
|
||||
<field name="max_active_rides" type="number" default="1" label="Max Concurrent Rides per Driver" />
|
||||
</fieldset>
|
||||
<fieldset name="booking" label="Booking">
|
||||
<field name="advance_booking_days" type="number" default="7" label="Max Advance Booking (days)" />
|
||||
<field name="enable_scheduled_rides" type="radio" default="1" label="Allow Scheduled Rides" class="btn-group btn-group-yesno">
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
</fieldset>
|
||||
</fields>
|
||||
</config>
|
||||
</extension>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* Authored-by: Moko Consulting
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Extension\PluginInterface;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Plugin\PluginHelper;
|
||||
use Joomla\DI\Container;
|
||||
use Joomla\DI\ServiceProviderInterface;
|
||||
use Joomla\Event\DispatcherInterface;
|
||||
use Moko\Plugin\System\MokoSuiteTaxi\Extension\Taxi;
|
||||
|
||||
return new class implements ServiceProviderInterface
|
||||
{
|
||||
public function register(Container $container): void
|
||||
{
|
||||
$container->set(
|
||||
PluginInterface::class,
|
||||
function (Container $container) {
|
||||
$dispatcher = $container->get(DispatcherInterface::class);
|
||||
$plugin = new Taxi($dispatcher, (array) PluginHelper::getPlugin('system', 'mokosuitetaxi'));
|
||||
$plugin->setApplication(Factory::getApplication());
|
||||
return $plugin;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,216 @@
|
||||
--
|
||||
-- MokoSuite Taxi Tables
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuitetaxi_vehicles` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`plate_number` VARCHAR(20) NOT NULL,
|
||||
`vin` VARCHAR(17) NOT NULL DEFAULT '',
|
||||
`make` VARCHAR(100) NOT NULL,
|
||||
`model` VARCHAR(100) NOT NULL,
|
||||
`year` SMALLINT UNSIGNED NOT NULL,
|
||||
`color` VARCHAR(50) NOT NULL DEFAULT '',
|
||||
`vehicle_type` ENUM('sedan','suv','van','luxury','minibus','motorcycle') NOT NULL DEFAULT 'sedan',
|
||||
`capacity` TINYINT UNSIGNED NOT NULL DEFAULT 4,
|
||||
`fuel_type` ENUM('gasoline','diesel','electric','hybrid') NOT NULL DEFAULT 'gasoline',
|
||||
`status` ENUM('available','in_service','maintenance','retired') NOT NULL DEFAULT 'available',
|
||||
`insurance_expiry` DATE DEFAULT NULL,
|
||||
`inspection_expiry` DATE DEFAULT NULL,
|
||||
`odometer_km` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`photo` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
`notes` TEXT,
|
||||
`published` TINYINT NOT NULL DEFAULT 1,
|
||||
`created` DATETIME NOT NULL,
|
||||
`modified` DATETIME DEFAULT NULL,
|
||||
`created_by` INT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_plate` (`plate_number`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_type` (`vehicle_type`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuitetaxi_drivers` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`contact_id` INT DEFAULT NULL COMMENT 'FK to #__contact_details',
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`phone` VARCHAR(50) NOT NULL DEFAULT '',
|
||||
`email` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`license_number` VARCHAR(50) NOT NULL,
|
||||
`license_expiry` DATE DEFAULT NULL,
|
||||
`license_class` VARCHAR(10) NOT NULL DEFAULT '',
|
||||
`vehicle_id` INT UNSIGNED DEFAULT NULL,
|
||||
`status` ENUM('active','inactive','suspended','pending_approval') NOT NULL DEFAULT 'pending_approval',
|
||||
`rating` DECIMAL(3,2) NOT NULL DEFAULT 5.00,
|
||||
`total_rides` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`total_earnings` DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
`commission_rate` DECIMAL(5,2) DEFAULT NULL COMMENT 'Override platform default',
|
||||
`photo` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
`background_check_date` DATE DEFAULT NULL,
|
||||
`published` TINYINT NOT NULL DEFAULT 1,
|
||||
`created` DATETIME NOT NULL,
|
||||
`modified` DATETIME DEFAULT NULL,
|
||||
`created_by` INT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_license` (`license_number`),
|
||||
KEY `idx_contact` (`contact_id`),
|
||||
KEY `idx_vehicle` (`vehicle_id`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_rating` (`rating`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuitetaxi_zones` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`description` TEXT,
|
||||
`zone_type` ENUM('pickup','dropoff','service','surge','restricted') NOT NULL DEFAULT 'service',
|
||||
`boundary_geojson` JSON COMMENT 'GeoJSON polygon defining zone boundary',
|
||||
`center_lat` DECIMAL(10,7) DEFAULT NULL,
|
||||
`center_lng` DECIMAL(10,7) DEFAULT NULL,
|
||||
`radius_km` DECIMAL(8,2) DEFAULT NULL COMMENT 'Circular zone fallback if no polygon',
|
||||
`fare_multiplier` DECIMAL(4,2) NOT NULL DEFAULT 1.00,
|
||||
`active_hours_start` TIME DEFAULT NULL,
|
||||
`active_hours_end` TIME DEFAULT NULL,
|
||||
`published` TINYINT NOT NULL DEFAULT 1,
|
||||
`ordering` INT NOT NULL DEFAULT 0,
|
||||
`created` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_type` (`zone_type`),
|
||||
KEY `idx_center` (`center_lat`, `center_lng`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuitetaxi_fares` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`title` VARCHAR(255) NOT NULL,
|
||||
`vehicle_type` ENUM('sedan','suv','van','luxury','minibus','motorcycle','all') NOT NULL DEFAULT 'all',
|
||||
`zone_id` INT UNSIGNED DEFAULT NULL COMMENT 'Zone-specific pricing, NULL = default',
|
||||
`base_fare` DECIMAL(10,2) NOT NULL DEFAULT 2.50,
|
||||
`per_km` DECIMAL(10,2) NOT NULL DEFAULT 1.50,
|
||||
`per_minute` DECIMAL(10,2) NOT NULL DEFAULT 0.25,
|
||||
`minimum_fare` DECIMAL(10,2) NOT NULL DEFAULT 5.00,
|
||||
`booking_fee` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||
`peak_multiplier` DECIMAL(4,2) NOT NULL DEFAULT 1.00,
|
||||
`peak_hours_start` TIME DEFAULT NULL,
|
||||
`peak_hours_end` TIME DEFAULT NULL,
|
||||
`night_surcharge` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||
`airport_surcharge` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||
`effective_from` DATE DEFAULT NULL,
|
||||
`effective_to` DATE DEFAULT NULL,
|
||||
`published` TINYINT NOT NULL DEFAULT 1,
|
||||
`created` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_vehicle_type` (`vehicle_type`),
|
||||
KEY `idx_zone` (`zone_id`),
|
||||
KEY `idx_effective` (`effective_from`, `effective_to`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuitetaxi_rides` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`ride_ref` VARCHAR(20) NOT NULL,
|
||||
`rider_contact_id` INT DEFAULT NULL COMMENT 'FK to #__contact_details',
|
||||
`rider_name` VARCHAR(255) NOT NULL,
|
||||
`rider_phone` VARCHAR(50) NOT NULL DEFAULT '',
|
||||
`driver_id` INT UNSIGNED DEFAULT NULL,
|
||||
`vehicle_id` INT UNSIGNED DEFAULT NULL,
|
||||
`status` ENUM('requested','dispatched','accepted','arriving','in_progress','completed','cancelled','no_driver') NOT NULL DEFAULT 'requested',
|
||||
`ride_type` ENUM('on_demand','scheduled','airport','corporate') NOT NULL DEFAULT 'on_demand',
|
||||
`vehicle_type_requested` ENUM('sedan','suv','van','luxury','minibus','motorcycle','any') NOT NULL DEFAULT 'any',
|
||||
`pickup_address` VARCHAR(500) NOT NULL,
|
||||
`pickup_lat` DECIMAL(10,7) DEFAULT NULL,
|
||||
`pickup_lng` DECIMAL(10,7) DEFAULT NULL,
|
||||
`dropoff_address` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
`dropoff_lat` DECIMAL(10,7) DEFAULT NULL,
|
||||
`dropoff_lng` DECIMAL(10,7) DEFAULT NULL,
|
||||
`pickup_zone_id` INT UNSIGNED DEFAULT NULL,
|
||||
`dropoff_zone_id` INT UNSIGNED DEFAULT NULL,
|
||||
`scheduled_at` DATETIME DEFAULT NULL,
|
||||
`dispatched_at` DATETIME DEFAULT NULL,
|
||||
`accepted_at` DATETIME DEFAULT NULL,
|
||||
`arrived_at` DATETIME DEFAULT NULL,
|
||||
`started_at` DATETIME DEFAULT NULL,
|
||||
`completed_at` DATETIME DEFAULT NULL,
|
||||
`cancelled_at` DATETIME DEFAULT NULL,
|
||||
`cancelled_by` ENUM('rider','driver','system') DEFAULT NULL,
|
||||
`cancel_reason` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
`distance_km` DECIMAL(10,2) DEFAULT NULL,
|
||||
`duration_minutes` DECIMAL(10,2) DEFAULT NULL,
|
||||
`fare_id` INT UNSIGNED DEFAULT NULL,
|
||||
`base_fare` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||
`distance_charge` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||
`time_charge` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||
`surge_multiplier` DECIMAL(4,2) NOT NULL DEFAULT 1.00,
|
||||
`surcharges` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||
`discount` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||
`total_fare` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||
`driver_payout` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||
`platform_fee` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||
`payment_method` ENUM('cash','card','wallet','corporate') NOT NULL DEFAULT 'cash',
|
||||
`payment_status` ENUM('pending','paid','refunded') NOT NULL DEFAULT 'pending',
|
||||
`passenger_count` TINYINT UNSIGNED NOT NULL DEFAULT 1,
|
||||
`notes` TEXT,
|
||||
`route_polyline` TEXT COMMENT 'Encoded polyline of actual route',
|
||||
`created` DATETIME NOT NULL,
|
||||
`created_by` INT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_ref` (`ride_ref`),
|
||||
KEY `idx_rider` (`rider_contact_id`),
|
||||
KEY `idx_driver` (`driver_id`),
|
||||
KEY `idx_vehicle` (`vehicle_id`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_type` (`ride_type`),
|
||||
KEY `idx_created` (`created`),
|
||||
KEY `idx_scheduled` (`scheduled_at`),
|
||||
KEY `idx_pickup_zone` (`pickup_zone_id`),
|
||||
KEY `idx_dropoff_zone` (`dropoff_zone_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuitetaxi_ratings` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`ride_id` INT UNSIGNED NOT NULL,
|
||||
`rated_by` ENUM('rider','driver') NOT NULL,
|
||||
`rating` TINYINT UNSIGNED NOT NULL COMMENT '1-5 stars',
|
||||
`comment` TEXT,
|
||||
`tags` VARCHAR(500) NOT NULL DEFAULT '' COMMENT 'Comma-separated feedback tags',
|
||||
`created` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_ride_rater` (`ride_id`, `rated_by`),
|
||||
KEY `idx_rating` (`rating`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuitetaxi_dispatch` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`ride_id` INT UNSIGNED NOT NULL,
|
||||
`driver_id` INT UNSIGNED NOT NULL,
|
||||
`status` ENUM('offered','accepted','rejected','expired') NOT NULL DEFAULT 'offered',
|
||||
`offered_at` DATETIME NOT NULL,
|
||||
`responded_at` DATETIME DEFAULT NULL,
|
||||
`distance_to_pickup_km` DECIMAL(10,2) DEFAULT NULL,
|
||||
`eta_minutes` DECIMAL(10,2) DEFAULT NULL,
|
||||
`attempt_number` TINYINT UNSIGNED NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_ride` (`ride_id`),
|
||||
KEY `idx_driver` (`driver_id`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_offered` (`offered_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuitetaxi_shifts` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`driver_id` INT UNSIGNED NOT NULL,
|
||||
`vehicle_id` INT UNSIGNED DEFAULT NULL,
|
||||
`start_time` DATETIME NOT NULL,
|
||||
`end_time` DATETIME DEFAULT NULL,
|
||||
`status` ENUM('scheduled','active','completed','cancelled') NOT NULL DEFAULT 'scheduled',
|
||||
`start_lat` DECIMAL(10,7) DEFAULT NULL,
|
||||
`start_lng` DECIMAL(10,7) DEFAULT NULL,
|
||||
`end_lat` DECIMAL(10,7) DEFAULT NULL,
|
||||
`end_lng` DECIMAL(10,7) DEFAULT NULL,
|
||||
`total_rides` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`total_distance_km` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||
`total_earnings` DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
`created` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_driver` (`driver_id`),
|
||||
KEY `idx_vehicle` (`vehicle_id`),
|
||||
KEY `idx_times` (`start_time`, `end_time`),
|
||||
KEY `idx_status` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
@@ -0,0 +1,12 @@
|
||||
--
|
||||
-- MokoSuite Taxi — Uninstall
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `#__mokosuitetaxi_shifts`;
|
||||
DROP TABLE IF EXISTS `#__mokosuitetaxi_dispatch`;
|
||||
DROP TABLE IF EXISTS `#__mokosuitetaxi_ratings`;
|
||||
DROP TABLE IF EXISTS `#__mokosuitetaxi_rides`;
|
||||
DROP TABLE IF EXISTS `#__mokosuitetaxi_fares`;
|
||||
DROP TABLE IF EXISTS `#__mokosuitetaxi_zones`;
|
||||
DROP TABLE IF EXISTS `#__mokosuitetaxi_drivers`;
|
||||
DROP TABLE IF EXISTS `#__mokosuitetaxi_vehicles`;
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* Authored-by: Moko Consulting
|
||||
*/
|
||||
|
||||
namespace Moko\Plugin\System\MokoSuiteTaxi\Extension;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Plugin\CMSPlugin;
|
||||
use Joomla\Event\SubscriberInterface;
|
||||
|
||||
class Taxi extends CMSPlugin implements SubscriberInterface
|
||||
{
|
||||
protected $autoloadLanguage = true;
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* Authored-by: Moko Consulting
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Moko\Plugin\System\MokoSuiteTaxi\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
|
||||
class DispatchHelper
|
||||
{
|
||||
public static function findNearbyDrivers(
|
||||
float $lat,
|
||||
float $lng,
|
||||
float $radiusKm = 10.0,
|
||||
string $vehicleType = 'any',
|
||||
int $limit = 10
|
||||
): array {
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->select([
|
||||
'd.id', 'd.name', 'd.rating', 'd.vehicle_id',
|
||||
'v.plate_number', 'v.vehicle_type', 'v.capacity',
|
||||
's.start_lat AS lat', 's.start_lng AS lng',
|
||||
])
|
||||
->from($db->quoteName('#__mokosuitetaxi_drivers', 'd'))
|
||||
->join('INNER', $db->quoteName('#__mokosuitetaxi_shifts', 's')
|
||||
. ' ON s.driver_id = d.id AND s.status = ' . $db->quote('active'))
|
||||
->join('LEFT', $db->quoteName('#__mokosuitetaxi_vehicles', 'v')
|
||||
. ' ON v.id = d.vehicle_id')
|
||||
->where($db->quoteName('d.status') . ' = ' . $db->quote('active'))
|
||||
->where($db->quoteName('d.published') . ' = 1')
|
||||
->where('s.start_lat IS NOT NULL')
|
||||
->where('s.start_lng IS NOT NULL')
|
||||
->order('d.rating DESC')
|
||||
->setLimit($limit);
|
||||
|
||||
if ($vehicleType !== 'any') {
|
||||
$query->where($db->quoteName('v.vehicle_type') . ' = ' . $db->quote($vehicleType));
|
||||
}
|
||||
|
||||
$db->setQuery($query);
|
||||
$drivers = $db->loadObjectList() ?: [];
|
||||
|
||||
$nearby = [];
|
||||
|
||||
foreach ($drivers as $driver) {
|
||||
$distance = TaxiHelper::haversineDistance($lat, $lng, (float) $driver->lat, (float) $driver->lng);
|
||||
|
||||
if ($distance <= $radiusKm) {
|
||||
$driver->distance_km = round($distance, 2);
|
||||
$driver->eta_minutes = round($distance * 2.5, 1);
|
||||
$nearby[] = $driver;
|
||||
}
|
||||
}
|
||||
|
||||
usort($nearby, fn($a, $b) => $a->distance_km <=> $b->distance_km);
|
||||
|
||||
return $nearby;
|
||||
}
|
||||
|
||||
public static function dispatchRide(int $rideId, int $driverId): object
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$now = Factory::getDate()->toSql();
|
||||
|
||||
// FOR UPDATE lock to prevent race condition on ride acceptance
|
||||
$db->transactionStart();
|
||||
|
||||
try {
|
||||
$db->setQuery('SELECT id, status FROM ' . $db->quoteName('#__mokosuitetaxi_rides')
|
||||
. ' WHERE id = ' . (int) $rideId . ' FOR UPDATE');
|
||||
$ride = $db->loadObject();
|
||||
|
||||
if (!$ride || !in_array($ride->status, ['requested', 'dispatched'], true)) {
|
||||
$db->transactionRollback();
|
||||
throw new \RuntimeException('Ride is no longer available for dispatch');
|
||||
}
|
||||
|
||||
$attempt = 1;
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('COALESCE(MAX(attempt_number), 0)')
|
||||
->from('#__mokosuitetaxi_dispatch')
|
||||
->where('ride_id = ' . (int) $rideId));
|
||||
$attempt = (int) $db->loadResult() + 1;
|
||||
|
||||
$dispatch = (object) [
|
||||
'ride_id' => $rideId,
|
||||
'driver_id' => $driverId,
|
||||
'status' => 'offered',
|
||||
'offered_at' => $now,
|
||||
'attempt_number' => $attempt,
|
||||
];
|
||||
|
||||
$db->insertObject('#__mokosuitetaxi_dispatch', $dispatch, 'id');
|
||||
|
||||
$update = (object) [
|
||||
'id' => $rideId,
|
||||
'status' => 'dispatched',
|
||||
'driver_id' => $driverId,
|
||||
'dispatched_at' => $now,
|
||||
];
|
||||
$db->updateObject('#__mokosuitetaxi_rides', $update, 'id');
|
||||
|
||||
$db->transactionCommit();
|
||||
|
||||
return $dispatch;
|
||||
} catch (\Throwable $e) {
|
||||
$db->transactionRollback();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public static function acceptRide(int $rideId, int $driverId): bool
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$now = Factory::getDate()->toSql();
|
||||
|
||||
$db->transactionStart();
|
||||
|
||||
try {
|
||||
$db->setQuery('SELECT id, status, driver_id FROM ' . $db->quoteName('#__mokosuitetaxi_rides')
|
||||
. ' WHERE id = ' . (int) $rideId . ' FOR UPDATE');
|
||||
$ride = $db->loadObject();
|
||||
|
||||
if (!$ride || $ride->status !== 'dispatched' || (int) $ride->driver_id !== $driverId) {
|
||||
$db->transactionRollback();
|
||||
return false;
|
||||
}
|
||||
|
||||
$db->updateObject('#__mokosuitetaxi_rides', (object) [
|
||||
'id' => $rideId,
|
||||
'status' => 'accepted',
|
||||
'accepted_at' => $now,
|
||||
], 'id');
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->update('#__mokosuitetaxi_dispatch')
|
||||
->set($db->quoteName('status') . ' = ' . $db->quote('accepted'))
|
||||
->set($db->quoteName('responded_at') . ' = ' . $db->quote($now))
|
||||
->where('ride_id = ' . (int) $rideId)
|
||||
->where('driver_id = ' . (int) $driverId)
|
||||
->where($db->quoteName('status') . ' = ' . $db->quote('offered'));
|
||||
$db->setQuery($query)->execute();
|
||||
|
||||
$db->transactionCommit();
|
||||
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
$db->transactionRollback();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public static function getActiveRidesForDriver(int $driverId): array
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('*')
|
||||
->from('#__mokosuitetaxi_rides')
|
||||
->where('driver_id = ' . (int) $driverId)
|
||||
->where($db->quoteName('status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['dispatched', 'accepted', 'arriving', 'in_progress'])) . ')')
|
||||
->order('created DESC'));
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* Authored-by: Moko Consulting
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Moko\Plugin\System\MokoSuiteTaxi\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
|
||||
class FareHelper
|
||||
{
|
||||
public static function calculateFare(
|
||||
float $distanceKm,
|
||||
float $durationMinutes,
|
||||
string $vehicleType = 'sedan',
|
||||
?int $zoneId = null,
|
||||
float $surgeMultiplier = 1.0
|
||||
): object {
|
||||
$rule = self::getApplicableRule($vehicleType, $zoneId);
|
||||
|
||||
$baseFare = (float) $rule->base_fare;
|
||||
$distanceCharge = $distanceKm * (float) $rule->per_km;
|
||||
$timeCharge = $durationMinutes * (float) $rule->per_minute;
|
||||
|
||||
$subtotal = ($baseFare + $distanceCharge + $timeCharge) * $surgeMultiplier;
|
||||
$subtotal += (float) $rule->booking_fee;
|
||||
|
||||
$hour = (int) date('H');
|
||||
if ($hour >= 22 || $hour < 6) {
|
||||
$subtotal += (float) $rule->night_surcharge;
|
||||
}
|
||||
|
||||
$subtotal *= (float) ($rule->peak_multiplier ?? 1.0);
|
||||
$total = max($subtotal, (float) $rule->minimum_fare);
|
||||
|
||||
return (object) [
|
||||
'fare_id' => (int) $rule->id,
|
||||
'base_fare' => $baseFare,
|
||||
'distance_charge' => round($distanceCharge, 2),
|
||||
'time_charge' => round($timeCharge, 2),
|
||||
'surge_multiplier' => $surgeMultiplier,
|
||||
'surcharges' => round((float) $rule->night_surcharge + (float) $rule->booking_fee, 2),
|
||||
'total_fare' => round($total, 2),
|
||||
];
|
||||
}
|
||||
|
||||
public static function estimateFare(
|
||||
float $pickupLat,
|
||||
float $pickupLng,
|
||||
float $dropoffLat,
|
||||
float $dropoffLng,
|
||||
string $vehicleType = 'sedan'
|
||||
): object {
|
||||
$distanceKm = TaxiHelper::haversineDistance($pickupLat, $pickupLng, $dropoffLat, $dropoffLng);
|
||||
$distanceKm *= 1.3; // road distance approximation
|
||||
$durationMinutes = $distanceKm * 2.0;
|
||||
|
||||
$estimate = self::calculateFare($distanceKm, $durationMinutes, $vehicleType);
|
||||
$estimate->estimated_distance_km = round($distanceKm, 2);
|
||||
$estimate->estimated_duration_minutes = round($durationMinutes, 1);
|
||||
|
||||
return $estimate;
|
||||
}
|
||||
|
||||
public static function getApplicableRule(string $vehicleType = 'sedan', ?int $zoneId = null): object
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$now = Factory::getDate()->format('Y-m-d');
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->select('*')
|
||||
->from('#__mokosuitetaxi_fares')
|
||||
->where($db->quoteName('published') . ' = 1')
|
||||
->where('(' . $db->quoteName('effective_from') . ' IS NULL OR ' . $db->quoteName('effective_from') . ' <= ' . $db->quote($now) . ')')
|
||||
->where('(' . $db->quoteName('effective_to') . ' IS NULL OR ' . $db->quoteName('effective_to') . ' >= ' . $db->quote($now) . ')')
|
||||
->order('zone_id DESC, vehicle_type DESC')
|
||||
->setLimit(1);
|
||||
|
||||
if ($zoneId) {
|
||||
$query->where('(' . $db->quoteName('zone_id') . ' = ' . (int) $zoneId . ' OR ' . $db->quoteName('zone_id') . ' IS NULL)');
|
||||
} else {
|
||||
$query->where($db->quoteName('zone_id') . ' IS NULL');
|
||||
}
|
||||
|
||||
$query->where('(' . $db->quoteName('vehicle_type') . ' = ' . $db->quote($vehicleType) . ' OR ' . $db->quoteName('vehicle_type') . ' = ' . $db->quote('all') . ')');
|
||||
|
||||
$db->setQuery($query);
|
||||
$rule = $db->loadObject();
|
||||
|
||||
if (!$rule) {
|
||||
return (object) [
|
||||
'id' => 0, 'base_fare' => 2.50, 'per_km' => 1.50, 'per_minute' => 0.25,
|
||||
'minimum_fare' => 5.00, 'booking_fee' => 0.00, 'peak_multiplier' => 1.00,
|
||||
'night_surcharge' => 0.00, 'airport_surcharge' => 0.00,
|
||||
];
|
||||
}
|
||||
|
||||
return $rule;
|
||||
}
|
||||
|
||||
public static function calculateDriverPayout(float $totalFare, ?float $driverCommissionRate = null): object
|
||||
{
|
||||
if ($driverCommissionRate === null) {
|
||||
$params = Factory::getApplication()->bootPlugin('mokosuitetaxi', 'system')->params;
|
||||
$driverCommissionRate = (float) ($params->get('commission_rate', 20));
|
||||
}
|
||||
|
||||
$platformFee = round($totalFare * ($driverCommissionRate / 100), 2);
|
||||
$driverPayout = round($totalFare - $platformFee, 2);
|
||||
|
||||
return (object) [
|
||||
'total_fare' => $totalFare,
|
||||
'commission_rate' => $driverCommissionRate,
|
||||
'platform_fee' => $platformFee,
|
||||
'driver_payout' => $driverPayout,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* Authored-by: Moko Consulting
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Moko\Plugin\System\MokoSuiteTaxi\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
|
||||
class RideHelper
|
||||
{
|
||||
private const STATUS_FLOW = [
|
||||
'requested' => ['dispatched', 'cancelled', 'no_driver'],
|
||||
'dispatched' => ['accepted', 'cancelled', 'no_driver'],
|
||||
'accepted' => ['arriving', 'cancelled'],
|
||||
'arriving' => ['in_progress', 'cancelled'],
|
||||
'in_progress' => ['completed', 'cancelled'],
|
||||
'completed' => [],
|
||||
'cancelled' => [],
|
||||
'no_driver' => ['requested'],
|
||||
];
|
||||
|
||||
public static function requestRide(array $data): object
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$now = Factory::getDate()->toSql();
|
||||
$user = Factory::getApplication()->getIdentity();
|
||||
|
||||
$ride = (object) [
|
||||
'ride_ref' => TaxiHelper::generateRideRef(),
|
||||
'rider_contact_id' => isset($data['rider_contact_id']) ? (int) $data['rider_contact_id'] : null,
|
||||
'rider_name' => $data['rider_name'],
|
||||
'rider_phone' => $data['rider_phone'] ?? '',
|
||||
'status' => 'requested',
|
||||
'ride_type' => $data['ride_type'] ?? 'on_demand',
|
||||
'vehicle_type_requested' => $data['vehicle_type'] ?? 'any',
|
||||
'pickup_address' => $data['pickup_address'],
|
||||
'pickup_lat' => $data['pickup_lat'] ?? null,
|
||||
'pickup_lng' => $data['pickup_lng'] ?? null,
|
||||
'dropoff_address' => $data['dropoff_address'] ?? '',
|
||||
'dropoff_lat' => $data['dropoff_lat'] ?? null,
|
||||
'dropoff_lng' => $data['dropoff_lng'] ?? null,
|
||||
'scheduled_at' => $data['scheduled_at'] ?? null,
|
||||
'passenger_count' => (int) ($data['passenger_count'] ?? 1),
|
||||
'payment_method' => $data['payment_method'] ?? 'cash',
|
||||
'notes' => $data['notes'] ?? '',
|
||||
'created' => $now,
|
||||
'created_by' => $user ? (int) $user->id : 0,
|
||||
];
|
||||
|
||||
if ($ride->pickup_lat && $ride->pickup_lng && $ride->dropoff_lat && $ride->dropoff_lng) {
|
||||
$estimate = FareHelper::estimateFare(
|
||||
(float) $ride->pickup_lat, (float) $ride->pickup_lng,
|
||||
(float) $ride->dropoff_lat, (float) $ride->dropoff_lng,
|
||||
$ride->vehicle_type_requested === 'any' ? 'sedan' : $ride->vehicle_type_requested
|
||||
);
|
||||
$ride->fare_id = $estimate->fare_id;
|
||||
$ride->base_fare = $estimate->base_fare;
|
||||
$ride->distance_charge = $estimate->distance_charge;
|
||||
$ride->time_charge = $estimate->time_charge;
|
||||
$ride->surcharges = $estimate->surcharges;
|
||||
$ride->total_fare = $estimate->total_fare;
|
||||
}
|
||||
|
||||
$db->insertObject('#__mokosuitetaxi_rides', $ride, 'id');
|
||||
|
||||
return $ride;
|
||||
}
|
||||
|
||||
public static function updateStatus(int $rideId, string $newStatus): bool
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$now = Factory::getDate()->toSql();
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('id, status, driver_id')
|
||||
->from('#__mokosuitetaxi_rides')
|
||||
->where('id = ' . (int) $rideId));
|
||||
$ride = $db->loadObject();
|
||||
|
||||
if (!$ride) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$allowed = self::STATUS_FLOW[$ride->status] ?? [];
|
||||
|
||||
if (!in_array($newStatus, $allowed, true)) {
|
||||
throw new \InvalidArgumentException(
|
||||
"Cannot transition from '{$ride->status}' to '{$newStatus}'"
|
||||
);
|
||||
}
|
||||
|
||||
$update = (object) ['id' => $rideId, 'status' => $newStatus];
|
||||
|
||||
$timestampMap = [
|
||||
'arriving' => 'arrived_at',
|
||||
'in_progress' => 'started_at',
|
||||
'completed' => 'completed_at',
|
||||
'cancelled' => 'cancelled_at',
|
||||
];
|
||||
|
||||
if (isset($timestampMap[$newStatus])) {
|
||||
$field = $timestampMap[$newStatus];
|
||||
$update->$field = $now;
|
||||
}
|
||||
|
||||
$db->updateObject('#__mokosuitetaxi_rides', $update, 'id');
|
||||
|
||||
if ($newStatus === 'completed' && $ride->driver_id) {
|
||||
self::updateDriverStats((int) $ride->driver_id);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function completeRide(int $rideId, float $distanceKm, float $durationMinutes): object
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$now = Factory::getDate()->toSql();
|
||||
|
||||
$db->setQuery($db->getQuery(true)->select('*')->from('#__mokosuitetaxi_rides')->where('id = ' . (int) $rideId));
|
||||
$ride = $db->loadObject();
|
||||
|
||||
if (!$ride || $ride->status !== 'in_progress') {
|
||||
throw new \RuntimeException('Ride is not in progress');
|
||||
}
|
||||
|
||||
$vehicleType = $ride->vehicle_type_requested === 'any' ? 'sedan' : $ride->vehicle_type_requested;
|
||||
$fare = FareHelper::calculateFare($distanceKm, $durationMinutes, $vehicleType, null, (float) $ride->surge_multiplier);
|
||||
$payout = FareHelper::calculateDriverPayout($fare->total_fare);
|
||||
|
||||
$update = (object) [
|
||||
'id' => $rideId,
|
||||
'status' => 'completed',
|
||||
'completed_at' => $now,
|
||||
'distance_km' => $distanceKm,
|
||||
'duration_minutes' => $durationMinutes,
|
||||
'base_fare' => $fare->base_fare,
|
||||
'distance_charge' => $fare->distance_charge,
|
||||
'time_charge' => $fare->time_charge,
|
||||
'surcharges' => $fare->surcharges,
|
||||
'total_fare' => $fare->total_fare,
|
||||
'driver_payout' => $payout->driver_payout,
|
||||
'platform_fee' => $payout->platform_fee,
|
||||
];
|
||||
|
||||
$db->updateObject('#__mokosuitetaxi_rides', $update, 'id');
|
||||
|
||||
if ($ride->driver_id) {
|
||||
self::updateDriverStats((int) $ride->driver_id);
|
||||
}
|
||||
|
||||
return $update;
|
||||
}
|
||||
|
||||
public static function getById(int $rideId): ?object
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->select([
|
||||
'r.*',
|
||||
'd.name AS driver_name', 'd.phone AS driver_phone', 'd.rating AS driver_rating',
|
||||
'v.plate_number', 'v.make AS vehicle_make', 'v.model AS vehicle_model', 'v.color AS vehicle_color',
|
||||
])
|
||||
->from($db->quoteName('#__mokosuitetaxi_rides', 'r'))
|
||||
->join('LEFT', $db->quoteName('#__mokosuitetaxi_drivers', 'd') . ' ON d.id = r.driver_id')
|
||||
->join('LEFT', $db->quoteName('#__mokosuitetaxi_vehicles', 'v') . ' ON v.id = r.vehicle_id')
|
||||
->where('r.id = ' . (int) $rideId);
|
||||
|
||||
$db->setQuery($query);
|
||||
|
||||
return $db->loadObject() ?: null;
|
||||
}
|
||||
|
||||
private static function updateDriverStats(int $driverId): void
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select(['COUNT(*) AS total_rides', 'COALESCE(SUM(driver_payout), 0) AS total_earnings'])
|
||||
->from('#__mokosuitetaxi_rides')
|
||||
->where('driver_id = ' . (int) $driverId)
|
||||
->where($db->quoteName('status') . ' = ' . $db->quote('completed')));
|
||||
$stats = $db->loadObject();
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('COALESCE(AVG(rating), 5.00)')
|
||||
->from('#__mokosuitetaxi_ratings')
|
||||
->join('INNER', '#__mokosuitetaxi_rides AS r ON r.id = ride_id')
|
||||
->where('r.driver_id = ' . (int) $driverId)
|
||||
->where($db->quoteName('rated_by') . ' = ' . $db->quote('rider')));
|
||||
$avgRating = (float) $db->loadResult();
|
||||
|
||||
$db->updateObject('#__mokosuitetaxi_drivers', (object) [
|
||||
'id' => $driverId,
|
||||
'total_rides' => (int) $stats->total_rides,
|
||||
'total_earnings' => (float) $stats->total_earnings,
|
||||
'rating' => round($avgRating, 2),
|
||||
'modified' => Factory::getDate()->toSql(),
|
||||
], 'id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* Authored-by: Moko Consulting
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Moko\Plugin\System\MokoSuiteTaxi\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
|
||||
class TaxiHelper
|
||||
{
|
||||
public static function getDashboard(): object
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$now = Factory::getDate()->toSql();
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('COUNT(*)')
|
||||
->from('#__mokosuitetaxi_drivers')
|
||||
->where($db->quoteName('status') . ' = ' . $db->quote('active'))
|
||||
->where($db->quoteName('published') . ' = 1'));
|
||||
$activeDrivers = (int) $db->loadResult();
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('COUNT(*)')
|
||||
->from('#__mokosuitetaxi_rides')
|
||||
->where($db->quoteName('status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['requested', 'dispatched', 'accepted', 'arriving', 'in_progress'])) . ')'));
|
||||
$activeRides = (int) $db->loadResult();
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('COUNT(*)')
|
||||
->from('#__mokosuitetaxi_vehicles')
|
||||
->where($db->quoteName('status') . ' = ' . $db->quote('available'))
|
||||
->where($db->quoteName('published') . ' = 1'));
|
||||
$availableVehicles = (int) $db->loadResult();
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('COALESCE(SUM(' . $db->quoteName('total_fare') . '), 0)')
|
||||
->from('#__mokosuitetaxi_rides')
|
||||
->where($db->quoteName('status') . ' = ' . $db->quote('completed'))
|
||||
->where($db->quoteName('payment_status') . ' = ' . $db->quote('paid'))
|
||||
->where('DATE(' . $db->quoteName('completed_at') . ') = CURDATE()'));
|
||||
$todayRevenue = (float) $db->loadResult();
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('COUNT(*)')
|
||||
->from('#__mokosuitetaxi_rides')
|
||||
->where($db->quoteName('status') . ' = ' . $db->quote('completed'))
|
||||
->where('DATE(' . $db->quoteName('completed_at') . ') = CURDATE()'));
|
||||
$todayCompleted = (int) $db->loadResult();
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('COALESCE(AVG(' . $db->quoteName('r.rating') . '), 0)')
|
||||
->from($db->quoteName('#__mokosuitetaxi_ratings', 'r'))
|
||||
->where($db->quoteName('r.rated_by') . ' = ' . $db->quote('rider'))
|
||||
->where('DATE(' . $db->quoteName('r.created') . ') >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)'));
|
||||
$avgRating = round((float) $db->loadResult(), 2);
|
||||
|
||||
return (object) [
|
||||
'active_drivers' => $activeDrivers,
|
||||
'active_rides' => $activeRides,
|
||||
'available_vehicles' => $availableVehicles,
|
||||
'today_revenue' => $todayRevenue,
|
||||
'today_completed' => $todayCompleted,
|
||||
'avg_rating_30d' => $avgRating,
|
||||
];
|
||||
}
|
||||
|
||||
public static function generateRideRef(): string
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$prefix = 'TX';
|
||||
|
||||
do {
|
||||
$ref = $prefix . strtoupper(bin2hex(random_bytes(4)));
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('COUNT(*)')
|
||||
->from('#__mokosuitetaxi_rides')
|
||||
->where($db->quoteName('ride_ref') . ' = ' . $db->quote($ref)));
|
||||
} while ((int) $db->loadResult() > 0);
|
||||
|
||||
return $ref;
|
||||
}
|
||||
|
||||
public static function haversineDistance(float $lat1, float $lng1, float $lat2, float $lng2): float
|
||||
{
|
||||
$earthRadius = 6371.0;
|
||||
$dLat = deg2rad($lat2 - $lat1);
|
||||
$dLng = deg2rad($lng2 - $lng1);
|
||||
$a = sin($dLat / 2) * sin($dLat / 2) +
|
||||
cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
|
||||
sin($dLng / 2) * sin($dLng / 2);
|
||||
|
||||
return $earthRadius * 2 * atan2(sqrt($a), sqrt(1 - $a));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* Authored-by: Moko Consulting
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Moko\Plugin\System\MokoSuiteTaxi\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
|
||||
class VehicleHelper
|
||||
{
|
||||
public static function getById(int $vehicleId): ?object
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->select([
|
||||
'v.*',
|
||||
'd.name AS assigned_driver',
|
||||
'd.id AS assigned_driver_id',
|
||||
])
|
||||
->from($db->quoteName('#__mokosuitetaxi_vehicles', 'v'))
|
||||
->join('LEFT', $db->quoteName('#__mokosuitetaxi_drivers', 'd')
|
||||
. ' ON d.vehicle_id = v.id AND d.status = ' . $db->quote('active'))
|
||||
->where('v.id = ' . (int) $vehicleId);
|
||||
|
||||
$db->setQuery($query);
|
||||
|
||||
return $db->loadObject() ?: null;
|
||||
}
|
||||
|
||||
public static function getFleetSummary(): object
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select([
|
||||
$db->quoteName('status'),
|
||||
'COUNT(*) AS count',
|
||||
])
|
||||
->from('#__mokosuitetaxi_vehicles')
|
||||
->where($db->quoteName('published') . ' = 1')
|
||||
->group('status'));
|
||||
|
||||
$rows = $db->loadObjectList('status') ?: [];
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select([
|
||||
$db->quoteName('vehicle_type'),
|
||||
'COUNT(*) AS count',
|
||||
])
|
||||
->from('#__mokosuitetaxi_vehicles')
|
||||
->where($db->quoteName('published') . ' = 1')
|
||||
->group('vehicle_type'));
|
||||
|
||||
$byType = $db->loadObjectList('vehicle_type') ?: [];
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('COUNT(*)')
|
||||
->from('#__mokosuitetaxi_vehicles')
|
||||
->where($db->quoteName('published') . ' = 1')
|
||||
->where('(' . $db->quoteName('insurance_expiry') . ' <= DATE_ADD(CURDATE(), INTERVAL 30 DAY)'
|
||||
. ' OR ' . $db->quoteName('inspection_expiry') . ' <= DATE_ADD(CURDATE(), INTERVAL 30 DAY))'));
|
||||
$expiringCount = (int) $db->loadResult();
|
||||
|
||||
return (object) [
|
||||
'by_status' => $rows,
|
||||
'by_type' => $byType,
|
||||
'expiring_soon' => $expiringCount,
|
||||
'total' => array_sum(array_map(fn($r) => (int) $r->count, (array) $rows)),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getExpiringDocuments(int $days = 30): array
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$deadline = Factory::getDate('+' . $days . ' days')->format('Y-m-d');
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('id, plate_number, make, model, insurance_expiry, inspection_expiry')
|
||||
->from('#__mokosuitetaxi_vehicles')
|
||||
->where($db->quoteName('published') . ' = 1')
|
||||
->where('(' . $db->quoteName('insurance_expiry') . ' <= ' . $db->quote($deadline)
|
||||
. ' OR ' . $db->quoteName('inspection_expiry') . ' <= ' . $db->quote($deadline) . ')')
|
||||
->order('LEAST(COALESCE(insurance_expiry, ' . $db->quote('9999-12-31') . '), COALESCE(inspection_expiry, ' . $db->quote('9999-12-31') . ')) ASC'));
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* Authored-by: Moko Consulting
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Moko\Plugin\System\MokoSuiteTaxi\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
|
||||
class ZoneHelper
|
||||
{
|
||||
public static function detectZone(float $lat, float $lng): ?object
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('*')
|
||||
->from('#__mokosuitetaxi_zones')
|
||||
->where($db->quoteName('published') . ' = 1')
|
||||
->where('center_lat IS NOT NULL')
|
||||
->where('center_lng IS NOT NULL')
|
||||
->order('ordering ASC'));
|
||||
|
||||
$zones = $db->loadObjectList() ?: [];
|
||||
|
||||
foreach ($zones as $zone) {
|
||||
$radius = (float) ($zone->radius_km ?: 50.0);
|
||||
$distance = TaxiHelper::haversineDistance($lat, $lng, (float) $zone->center_lat, (float) $zone->center_lng);
|
||||
|
||||
if ($distance <= $radius) {
|
||||
$zone->distance_from_center = round($distance, 2);
|
||||
return $zone;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function getSurgeMultiplier(int $zoneId): float
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('COUNT(*)')
|
||||
->from('#__mokosuitetaxi_rides')
|
||||
->where($db->quoteName('pickup_zone_id') . ' = ' . (int) $zoneId)
|
||||
->where($db->quoteName('status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['requested', 'dispatched'])) . ')')
|
||||
->where($db->quoteName('created') . ' >= DATE_SUB(NOW(), INTERVAL 15 MINUTE)'));
|
||||
$demandCount = (int) $db->loadResult();
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('COUNT(DISTINCT d.id)')
|
||||
->from($db->quoteName('#__mokosuitetaxi_drivers', 'd'))
|
||||
->join('INNER', $db->quoteName('#__mokosuitetaxi_shifts', 's') . ' ON s.driver_id = d.id AND s.status = ' . $db->quote('active'))
|
||||
->where($db->quoteName('d.status') . ' = ' . $db->quote('active')));
|
||||
$supplyCount = max((int) $db->loadResult(), 1);
|
||||
|
||||
$ratio = $demandCount / $supplyCount;
|
||||
|
||||
if ($ratio <= 0.5) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
if ($ratio <= 1.0) {
|
||||
return 1.25;
|
||||
}
|
||||
|
||||
if ($ratio <= 2.0) {
|
||||
return 1.5;
|
||||
}
|
||||
|
||||
$params = Factory::getApplication()->bootPlugin('mokosuitetaxi', 'system')->params;
|
||||
$maxSurge = (float) $params->get('max_surge_multiplier', 3.0);
|
||||
|
||||
return min(1.5 + ($ratio - 2.0) * 0.5, $maxSurge);
|
||||
}
|
||||
|
||||
public static function listZones(string $type = ''): array
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->select('*')
|
||||
->from('#__mokosuitetaxi_zones')
|
||||
->where($db->quoteName('published') . ' = 1')
|
||||
->order('ordering ASC');
|
||||
|
||||
if ($type) {
|
||||
$query->where($db->quoteName('zone_type') . ' = ' . $db->quote($type));
|
||||
}
|
||||
|
||||
$db->setQuery($query);
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0"?>
|
||||
<extension type="plugin" group="webservices" method="upgrade">
|
||||
<name>Web Services - MokoSuite Taxi</name>
|
||||
<element>mokosuitetaxi</element>
|
||||
<author>Moko Consulting</author>
|
||||
<version>06.00.00</version>
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<namespace path="src">Moko\Plugin\WebServices\MokoSuiteTaxi</namespace>
|
||||
<files><folder>src</folder><folder>services</folder></files>
|
||||
</extension>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* Authored-by: Moko Consulting
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Extension\PluginInterface;
|
||||
use Joomla\CMS\Plugin\PluginHelper;
|
||||
use Joomla\DI\Container;
|
||||
use Joomla\DI\ServiceProviderInterface;
|
||||
use Joomla\Event\DispatcherInterface;
|
||||
use Moko\Plugin\WebServices\MokoSuiteTaxi\Extension\MokoSuiteTaxi;
|
||||
|
||||
return new class implements ServiceProviderInterface {
|
||||
public function register(Container $container): void {
|
||||
$container->set(PluginInterface::class, function (Container $container) {
|
||||
return new MokoSuiteTaxi($container->get(DispatcherInterface::class), (array) PluginHelper::getPlugin('webservices', 'mokosuitetaxi'));
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* Authored-by: Moko Consulting
|
||||
*/
|
||||
|
||||
namespace Moko\Plugin\WebServices\MokoSuiteTaxi\Extension;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Plugin\CMSPlugin;
|
||||
use Joomla\Event\SubscriberInterface;
|
||||
|
||||
class MokoSuiteTaxi extends CMSPlugin implements SubscriberInterface
|
||||
{
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return ['onBeforeApiRoute' => 'onBeforeApiRoute'];
|
||||
}
|
||||
|
||||
public function onBeforeApiRoute(&$event): void
|
||||
{
|
||||
$router = $event->getArgument('router');
|
||||
$opts = ['component' => 'com_mokosuitetaxi'];
|
||||
|
||||
$router->createCRUDRoutes('v1/mokosuitetaxi/rides', 'rides', $opts);
|
||||
$router->createCRUDRoutes('v1/mokosuitetaxi/vehicles', 'vehicles', $opts);
|
||||
$router->createCRUDRoutes('v1/mokosuitetaxi/drivers', 'drivers', $opts);
|
||||
$router->createCRUDRoutes('v1/mokosuitetaxi/zones', 'zones', $opts);
|
||||
$router->createCRUDRoutes('v1/mokosuitetaxi/fares', 'fares', $opts);
|
||||
$router->createCRUDRoutes('v1/mokosuitetaxi/dispatch', 'dispatch', $opts);
|
||||
$router->createCRUDRoutes('v1/mokosuitetaxi/ratings', 'ratings', $opts);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<extension type="package" method="upgrade">
|
||||
<name>Package - MokoSuite Taxi</name>
|
||||
<packagename>mokosuitetaxi</packagename>
|
||||
<version>06.00.00</version>
|
||||
<creationDate>2026-06-27</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GNU General Public License version 3 or later; see LICENSE</license>
|
||||
<description>Ride-hailing, dispatch, fleet management, fare zones, and driver scheduling</description>
|
||||
<php_minimum>8.3</php_minimum>
|
||||
<dlid prefix="dlid=" suffix=""/>
|
||||
<blockChildUninstall>true</blockChildUninstall>
|
||||
<files folder="packages">
|
||||
<file type="plugin" id="plg_system_mokosuitetaxi" group="system">plg_system_mokosuitetaxi.zip</file>
|
||||
</files>
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="Package - MokoSuite Taxi">https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteTaxi/updates.xml</server>
|
||||
</updateservers>
|
||||
</extension>
|
||||
Reference in New Issue
Block a user