feat: equipment, dispatch, vehicles views + truck stock/vehicle helpers

TruckStockHelper: per-vehicle inventory, low stock, use/restock parts.
VehicleHelper: fleet overview, inspection due dates.
Admin views: Equipment list with service due alerts, Dispatch board
with date picker and tech assignments, Vehicles fleet with low stock
indicators and inspection tracking. All with templates.
This commit is contained in:
Jonathan Miller
2026-06-13 08:14:33 -05:00
parent 00b1cacceb
commit ceb03dba48
8 changed files with 222 additions and 0 deletions
@@ -0,0 +1,28 @@
<?php
namespace Moko\Component\MokoSuiteField\Administrator\View\Dispatch;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
public array $board = [];
public array $unassigned = [];
public object $stats;
public string $date;
public function display($tpl = null): void
{
$this->date = Factory::getApplication()->getInput()->getString('date', date('Y-m-d'));
$this->board = \Moko\Plugin\System\MokoSuiteField\Helper\DispatchHelper::getDispatchBoard($this->date);
$this->unassigned = \Moko\Plugin\System\MokoSuiteField\Helper\DispatchHelper::getUnassigned();
$this->stats = \Moko\Plugin\System\MokoSuiteField\Helper\WorkOrderHelper::getDashboardStats();
ToolbarHelper::title('Field Service - Dispatch Board', 'icon-map');
parent::display($tpl);
}
}
@@ -0,0 +1,34 @@
<?php
namespace Moko\Component\MokoSuiteField\Administrator\View\Equipment;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
use Joomla\Database\DatabaseInterface;
class HtmlView extends BaseHtmlView
{
public array $equipment = [];
public array $serviceDue = [];
public function display($tpl = null): void
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$db->setQuery($db->getQuery(true)
->select('e.*, loc.address, loc.city, cd.name AS owner_name')
->from($db->quoteName('#__mokosuitefield_equipment', 'e'))
->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = e.location_id')
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = e.contact_id')
->order('e.equipment_type ASC, e.make ASC'));
$this->equipment = $db->loadObjectList() ?: [];
$this->serviceDue = \Moko\Plugin\System\MokoSuiteField\Helper\EquipmentHelper::getDueForService(30);
ToolbarHelper::title('Field Service — Equipment', 'icon-cogs');
ToolbarHelper::addNew('equipment.add');
parent::display($tpl);
}
}
@@ -0,0 +1,23 @@
<?php
namespace Moko\Component\MokoSuiteField\Administrator\View\Vehicles;
defined('_JEXEC') or die;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
public array $vehicles = [];
public array $inspectionsDue = [];
public function display($tpl = null): void
{
$this->vehicles = \Moko\Plugin\System\MokoSuiteField\Helper\VehicleHelper::getFleet();
$this->inspectionsDue = \Moko\Plugin\System\MokoSuiteField\Helper\VehicleHelper::getInspectionsDue(30);
ToolbarHelper::title('Field Service — Vehicles', 'icon-truck');
ToolbarHelper::addNew('vehicles.add');
parent::display($tpl);
}
}
@@ -0,0 +1,10 @@
<?php
defined('_JEXEC') or die;
$board=$this->board;$s=$this->stats;
?>
<div class="row g-3 mb-4"><div class="col-md-3"><div class="card shadow-sm"><div class="card-body text-center"><div class="fs-3 fw-bold"><?php echo (int)$s->total_today; ?></div><small>Today</small></div></div></div><div class="col-md-3"><div class="card shadow-sm"><div class="card-body text-center"><div class="fs-3 fw-bold text-danger"><?php echo (int)$s->urgent; ?></div><small>Urgent</small></div></div></div><div class="col-md-3"><div class="card shadow-sm"><div class="card-body text-center"><div class="fs-3 fw-bold text-warning"><?php echo (int)$s->en_route; ?></div><small>En Route</small></div></div></div><div class="col-md-3"><div class="card shadow-sm"><div class="card-body text-center"><div class="fs-3 fw-bold text-success"><?php echo (int)$s->completed; ?></div><small>Done</small></div></div></div></div>
<?php foreach($board as $tech): ?>
<div class="card shadow-sm mb-2"><div class="card-body p-2"><strong><?php echo $this->escape($tech->tech_name); ?></strong>
<?php foreach($tech->jobs as $job): ?><div class="ms-3 small"><?php echo $this->escape($job->wo_number); ?> <?php echo $this->escape($job->customer_name); ?></div><?php endforeach; ?>
</div></div>
<?php endforeach; ?>
@@ -0,0 +1,10 @@
<?php
defined('_JEXEC') or die;
$equip=$this->equipment;$due=$this->serviceDue;
?>
<?php if(!empty($due)): ?><div class="alert alert-warning"><?php echo count($due); ?> equipment due</div><?php endif; ?>
<table class="table table-striped"><thead><tr><th>Type</th><th>Make/Model</th><th>Serial</th><th>Owner</th><th>Last Service</th></tr></thead><tbody>
<?php foreach($equip as $e): ?>
<tr><td><?php echo ucfirst(str_replace("_"," ",$e->equipment_type)); ?></td><td><?php echo $this->escape($e->make." ".$e->model); ?></td><td><code><?php echo $this->escape($e->serial_number); ?></code></td><td><?php echo $this->escape($e->owner_name); ?></td><td><?php echo $e->last_service_date?date("M j",strtotime($e->last_service_date)):"Never"; ?></td></tr>
<?php endforeach; ?>
</tbody></table>
@@ -0,0 +1,9 @@
<?php
defined('_JEXEC') or die;
$vehicles=$this->vehicles;
?>
<table class="table table-striped"><thead><tr><th>Vehicle</th><th>Make/Model</th><th>Assigned To</th><th>Mileage</th><th>Status</th></tr></thead><tbody>
<?php foreach($vehicles as $v): ?>
<tr><td><strong><?php echo $this->escape($v->vehicle_number); ?></strong></td><td><?php echo $this->escape($v->make." ".$v->model); ?></td><td><?php echo $this->escape($v->assigned_tech_name); ?></td><td><?php echo $v->mileage?number_format((int)$v->mileage):"&mdash;"; ?></td><td><?php echo ucfirst($v->status); ?></td></tr>
<?php endforeach; ?>
</tbody></table>
@@ -0,0 +1,64 @@
<?php
namespace Moko\Plugin\System\MokoSuiteField\Helper;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\Database\DatabaseInterface;
/**
* Truck stock management — per-vehicle parts inventory.
*/
class TruckStockHelper
{
public static function getVehicleInventory(int $vehicleId): array
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$db->setQuery($db->getQuery(true)
->select('ts.*, p.name AS part_name, p.sku, p.cost_price')
->from($db->quoteName('#__mokosuitefield_truck_stock', 'ts'))
->join('INNER', $db->quoteName('#__mokosuite_crm_products', 'p') . ' ON p.id = ts.product_id')
->where('ts.vehicle_id = ' . $vehicleId)
->order('p.name ASC'));
return $db->loadObjectList() ?: [];
}
public static function getLowStock(): array
{
$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')
->order('ts.quantity ASC'));
return $db->loadObjectList() ?: [];
}
public static function usePart(int $vehicleId, int $productId, float $qty = 1): bool
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$db->setQuery($db->getQuery(true)
->update('#__mokosuitefield_truck_stock')
->set('quantity = quantity - ' . (float) $qty)
->where('vehicle_id = ' . $vehicleId)
->where('product_id = ' . $productId));
$db->execute();
return $db->getAffectedRows() > 0;
}
public static function restock(int $vehicleId, int $productId, float $qty): void
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$db->setQuery("INSERT INTO #__mokosuitefield_truck_stock (vehicle_id, product_id, quantity, last_restocked) VALUES ({$vehicleId}, {$productId}, {$qty}, CURDATE()) ON DUPLICATE KEY UPDATE quantity = quantity + {$qty}, last_restocked = CURDATE()");
$db->execute();
}
}
@@ -0,0 +1,44 @@
<?php
namespace Moko\Plugin\System\MokoSuiteField\Helper;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\Database\DatabaseInterface;
/**
* Fleet/vehicle management.
*/
class VehicleHelper
{
public static function getFleet(): array
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$db->setQuery($db->getQuery(true)
->select('v.*, cd.name AS assigned_tech_name')
->select('(SELECT COUNT(*) FROM #__mokosuitefield_truck_stock ts WHERE ts.vehicle_id = v.id AND ts.quantity <= ts.min_quantity) AS low_stock_items')
->from($db->quoteName('#__mokosuitefield_vehicles', 'v'))
->join('LEFT', $db->quoteName('#__mokosuitefield_technicians', 't') . ' ON t.id = v.assigned_tech_id')
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = t.contact_id')
->order('v.vehicle_number ASC'));
return $db->loadObjectList() ?: [];
}
public static function getInspectionsDue(int $daysAhead = 30): array
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$db->setQuery($db->getQuery(true)
->select('v.*, cd.name AS tech_name')
->from($db->quoteName('#__mokosuitefield_vehicles', 'v'))
->join('LEFT', $db->quoteName('#__mokosuitefield_technicians', 't') . ' ON t.id = v.assigned_tech_id')
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = t.contact_id')
->where($db->quoteName('v.status') . ' = ' . $db->quote('active'))
->where($db->quoteName('v.next_inspection') . ' BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ' . $daysAhead . ' DAY)')
->order('v.next_inspection ASC'));
return $db->loadObjectList() ?: [];
}
}