feat: webservices plugin, task scheduler, router, config, access

FieldAutomation task plugin: service reminders, agreement renewal alerts,
equipment warranty expiry, truck stock reorder.
MokoSuiteFieldApi webservices: 6 CRUD routes (workorders, technicians,
equipment, agreements, estimates, locations). Router, config.xml, access.xml.
This commit is contained in:
Jonathan Miller
2026-06-13 07:10:18 -05:00
parent a03b584b67
commit b39abf8bd5
6 changed files with 234 additions and 0 deletions
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<access component="com_mokosuitefield">
<section name="component">
<action name="core.admin" title="JACTION_ADMIN" />
<action name="core.manage" title="JACTION_MANAGE" />
<action name="core.create" title="JACTION_CREATE" />
<action name="core.edit" title="JACTION_EDIT" />
<action name="field.dispatch" title="Dispatch Work Orders" />
<action name="field.estimates" title="Create Estimates" />
</section>
</access>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<config>
<fieldset name="basic" label="Field Service Settings">
<field name="company_name" type="text" default="" label="Company Name" />
<field name="default_trade" type="list" default="general" label="Default Trade">
<option value="general">General</option>
<option value="plumbing">Plumbing</option>
<option value="electrical">Electrical</option>
<option value="hvac">HVAC</option>
</field>
<field name="wo_prefix" type="text" default="WO" label="Work Order Prefix" />
</fieldset>
<fieldset name="dispatch" label="Dispatch">
<field name="auto_dispatch" type="radio" default="0" label="Auto-Dispatch" class="btn-group btn-group-yesno"><option value="1">JYES</option><option value="0">JNO</option></field>
<field name="default_service_radius" type="number" default="30" label="Default Service Radius (miles)" />
</fieldset>
<fieldset name="billing" label="Billing">
<field name="default_labor_rate" type="number" default="125" step="0.01" label="Default Labor Rate ($/hr)" />
<field name="overtime_multiplier" type="number" default="1.5" step="0.1" label="Overtime Multiplier" />
<field name="travel_charge" type="number" default="0" step="0.01" label="Travel Charge ($)" />
</fieldset>
</config>
@@ -0,0 +1,21 @@
<?php
namespace Moko\Component\MokoSuiteField\Site\Service;
defined('_JEXEC') or die;
use Joomla\CMS\Component\Router\RouterBase;
class Router extends RouterBase
{
public function build(&$query): array
{
$segments = [];
if (isset($query['view'])) { $segments[] = $query['view']; unset($query['view']); }
if (isset($query['id'])) { $segments[] = $query['id']; unset($query['id']); }
return $segments;
}
public function parse(&$segments): array
{
$vars = [];
if (!empty($segments[0])) $vars['view'] = array_shift($segments);
if (!empty($segments[0]) && is_numeric($segments[0])) $vars['id'] = (int) array_shift($segments);
return $vars;
}
}
@@ -0,0 +1,144 @@
<?php
namespace Moko\Plugin\Task\MokoSuiteField\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Component\Scheduler\Administrator\Event\ExecuteTaskEvent;
use Joomla\Component\Scheduler\Administrator\Task\Status;
use Joomla\Component\Scheduler\Administrator\Traits\TaskPluginTrait;
use Joomla\Database\DatabaseInterface;
use Joomla\Event\SubscriberInterface;
/**
* Field service scheduled tasks: service reminders, agreement renewals,
* equipment maintenance alerts, truck stock reorder.
*/
class FieldAutomation extends CMSPlugin implements SubscriberInterface
{
use TaskPluginTrait;
protected const TASKS_MAP = [
'mokosuite.field.service.reminders' => [
'langConstPrefix' => 'PLG_TASK_MOKOSUITEFIELD_SERVICE_REMINDERS',
'method' => 'sendServiceReminders',
],
'mokosuite.field.agreement.renewal' => [
'langConstPrefix' => 'PLG_TASK_MOKOSUITEFIELD_AGREEMENT_RENEWAL',
'method' => 'checkAgreementRenewals',
],
'mokosuite.field.equipment.maintenance' => [
'langConstPrefix' => 'PLG_TASK_MOKOSUITEFIELD_EQUIPMENT_MAINTENANCE',
'method' => 'checkEquipmentMaintenance',
],
'mokosuite.field.truck.reorder' => [
'langConstPrefix' => 'PLG_TASK_MOKOSUITEFIELD_TRUCK_REORDER',
'method' => 'checkTruckStock',
],
];
public static function getSubscribedEvents(): array
{
return [
'onExecuteTask' => 'standardRoutineHandler',
'onContentPrepareForm' => 'enhanceTaskItemForm',
'onTaskOptionsList' => 'advertiseRoutines',
];
}
private function sendServiceReminders(ExecuteTaskEvent $event): int
{
$equipment = \Moko\Plugin\System\MokoSuiteField\Helper\EquipmentHelper::getDueForService(14);
if (empty($equipment)) return Status::OK;
$body = "Equipment due for service (next 14 days):\n\n";
foreach ($equipment as $e) {
$body .= "- {$e->equipment_type} ({$e->make} {$e->model}) at {$e->address}, {$e->owner_name}\n";
$body .= " Due: " . date('M j, Y', strtotime($e->next_service_date)) . "\n";
}
$mailer = Factory::getMailer();
$mailer->addRecipient(Factory::getApplication()->get('mailfrom'));
$mailer->setSubject('Field Service: ' . count($equipment) . ' equipment due for maintenance');
$mailer->setBody($body);
$mailer->Send();
Log::add("Field equipment reminders: " . count($equipment) . " due", Log::INFO, 'mokosuite.field');
return Status::OK;
}
private function checkAgreementRenewals(ExecuteTaskEvent $event): int
{
$expiring = \Moko\Plugin\System\MokoSuiteField\Helper\ServiceAgreementHelper::getExpiring(30);
foreach ($expiring as $a) {
if (!$a->email_to) continue;
$mailer = Factory::getMailer();
$mailer->addRecipient($a->email_to, $a->customer_name);
$mailer->setSubject('Service Agreement Renewal — ' . $a->title);
$mailer->setBody(
"Hi {$a->customer_name},\n\n"
. "Your service agreement \"{$a->title}\" expires on " . date('F j, Y', strtotime($a->end_date)) . ".\n\n"
. "Please contact us to renew.\n"
);
$mailer->Send();
}
Log::add("Field agreement renewals: " . count($expiring) . " expiring", Log::INFO, 'mokosuite.field');
return Status::OK;
}
private function checkEquipmentMaintenance(ExecuteTaskEvent $event): int
{
$warranty = \Moko\Plugin\System\MokoSuiteField\Helper\EquipmentHelper::getWarrantyExpiring(90);
if (!empty($warranty)) {
$body = "Equipment warranties expiring (90 days):\n\n";
foreach ($warranty as $e) {
$body .= "- {$e->make} {$e->model} (SN: {$e->serial_number}) — {$e->owner_name}\n";
$body .= " Warranty expires: " . date('M j, Y', strtotime($e->warranty_expiry)) . "\n";
}
$mailer = Factory::getMailer();
$mailer->addRecipient(Factory::getApplication()->get('mailfrom'));
$mailer->setSubject('Field: ' . count($warranty) . ' equipment warranties expiring');
$mailer->setBody($body);
$mailer->Send();
}
return Status::OK;
}
private function checkTruckStock(ExecuteTaskEvent $event): int
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$db->setQuery($db->getQuery(true)
->select('ts.*, p.name AS part_name, p.sku, v.vehicle_number')
->from($db->quoteName('#__mokosuitefield_truck_stock', 'ts'))
->join('INNER', $db->quoteName('#__mokosuite_crm_products', 'p') . ' ON p.id = ts.product_id')
->join('INNER', $db->quoteName('#__mokosuitefield_vehicles', 'v') . ' ON v.id = ts.vehicle_id')
->where('ts.quantity <= ts.min_quantity'));
$low = $db->loadObjectList() ?: [];
if (!empty($low)) {
$body = "Truck stock below reorder point:\n\n";
foreach ($low as $item) {
$body .= "- Vehicle {$item->vehicle_number}: {$item->part_name} ({$item->sku}) — {$item->quantity} remaining (min: {$item->min_quantity})\n";
}
$mailer = Factory::getMailer();
$mailer->addRecipient(Factory::getApplication()->get('mailfrom'));
$mailer->setSubject('Field: ' . count($low) . ' truck stock items need reorder');
$mailer->setBody($body);
$mailer->Send();
}
Log::add("Field truck stock: " . count($low) . " items low", Log::INFO, 'mokosuite.field');
return Status::OK;
}
}
@@ -0,0 +1,27 @@
<?php
namespace Moko\Plugin\WebServices\MokoSuiteField\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Event\Application\BeforeApiRouteEvent;
use Joomla\Event\SubscriberInterface;
final class MokoSuiteFieldApi extends CMSPlugin implements SubscriberInterface
{
public static function getSubscribedEvents(): array
{
return ['onBeforeApiRoute' => 'onBeforeApiRoute'];
}
public function onBeforeApiRoute(BeforeApiRouteEvent $event): void
{
$router = $event->getRouter();
$router->createCRUDRoutes('v1/mokosuite/field/workorders', 'fieldworkorders', ['component' => 'com_mokosuitefield']);
$router->createCRUDRoutes('v1/mokosuite/field/technicians', 'fieldtechnicians', ['component' => 'com_mokosuitefield']);
$router->createCRUDRoutes('v1/mokosuite/field/equipment', 'fieldequipment', ['component' => 'com_mokosuitefield']);
$router->createCRUDRoutes('v1/mokosuite/field/agreements', 'fieldagreements', ['component' => 'com_mokosuitefield']);
$router->createCRUDRoutes('v1/mokosuite/field/estimates', 'fieldestimates', ['component' => 'com_mokosuitefield']);
$router->createCRUDRoutes('v1/mokosuite/field/locations', 'fieldlocations', ['component' => 'com_mokosuitefield']);
}
}
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<updates><update>
<name>Package - MokoSuite Field</name>
<element>pkg_mokosuitefield</element>
<type>package</type>
<version>01.01.00</version>
<targetplatform name="joomla" version="6.[0-9]" />
<php_minimum>8.3</php_minimum>
</update></updates>