Add FieldEquipment and FieldEstimates API controllers
- FieldEquipmentController: equipment CRUD, vehicles, truck stock, service agreements - FieldEstimatesController: estimates CRUD, convert-to-WO, route optimization API
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
namespace Moko\Component\MokoSuiteField\Api\Controller;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\MVC\Controller\BaseController;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
|
||||
/**
|
||||
* Equipment + Vehicles + Service Agreements API.
|
||||
*
|
||||
* GET /equipment — List equipment
|
||||
* GET /equipment/{id} — Equipment detail with service history
|
||||
* GET /vehicles — List vehicles (fleet)
|
||||
* GET /vehicles/{id}/stock — Truck stock for a vehicle
|
||||
* GET /agreements — List service agreements
|
||||
* GET /agreements/{id} — Agreement detail with WO history
|
||||
*/
|
||||
class FieldEquipmentController extends BaseController
|
||||
{
|
||||
private function requireAuth(string $action = 'core.manage'): void
|
||||
{
|
||||
$user = Factory::getApplication()->getIdentity();
|
||||
if (!$user || $user->guest || (!$user->authorise('core.admin') && !$user->authorise($action, 'com_mokosuitefield'))) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Access denied.']);
|
||||
Factory::getApplication()->close();
|
||||
}
|
||||
}
|
||||
|
||||
public function listEquipment(): void
|
||||
{
|
||||
$this->requireAuth('core.manage');
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$input = Factory::getApplication()->getInput();
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->select('eq.*, loc.name AS location_name, loc.address')
|
||||
->select('cd.name AS customer_name')
|
||||
->from($db->quoteName('#__mokosuitefield_equipment', 'eq'))
|
||||
->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = eq.location_id')
|
||||
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = eq.contact_id')
|
||||
->order('eq.name ASC');
|
||||
|
||||
$type = $input->getString('type', '');
|
||||
if ($type) $query->where($db->quoteName('eq.type') . ' = ' . $db->quote($type));
|
||||
|
||||
$status = $input->getString('status', '');
|
||||
if ($status) $query->where($db->quoteName('eq.status') . ' = ' . $db->quote($status));
|
||||
|
||||
$locationId = $input->getInt('location_id', 0);
|
||||
if ($locationId) $query->where('eq.location_id = ' . $locationId);
|
||||
|
||||
$db->setQuery($query, 0, 100);
|
||||
$this->sendJson($db->loadObjectList() ?: []);
|
||||
}
|
||||
|
||||
public function getEquipment(): void
|
||||
{
|
||||
$this->requireAuth('core.manage');
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$id = Factory::getApplication()->getInput()->getInt('id', 0);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('eq.*, loc.name AS location_name, loc.address, cd.name AS customer_name')
|
||||
->from($db->quoteName('#__mokosuitefield_equipment', 'eq'))
|
||||
->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = eq.location_id')
|
||||
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = eq.contact_id')
|
||||
->where('eq.id = ' . $id));
|
||||
$equipment = $db->loadObject();
|
||||
|
||||
if (!$equipment) {
|
||||
http_response_code(404);
|
||||
$this->sendJson(['error' => 'Equipment not found']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Service history
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('wo.id, wo.wo_number, wo.description, wo.status, wo.completed_at, wo.trade')
|
||||
->from($db->quoteName('#__mokosuitefield_work_orders', 'wo'))
|
||||
->where('wo.equipment_id = ' . $id)
|
||||
->order('wo.scheduled_date DESC'), 0, 20);
|
||||
$equipment->service_history = $db->loadObjectList() ?: [];
|
||||
|
||||
$this->sendJson($equipment);
|
||||
}
|
||||
|
||||
public function listVehicles(): void
|
||||
{
|
||||
$this->requireAuth('core.manage');
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('v.*, t_cd.name AS tech_name')
|
||||
->from($db->quoteName('#__mokosuitefield_vehicles', 'v'))
|
||||
->join('LEFT', $db->quoteName('#__mokosuitefield_technicians', 't') . ' ON t.id = v.technician_id')
|
||||
->join('LEFT', $db->quoteName('#__contact_details', 't_cd') . ' ON t_cd.id = t.contact_id')
|
||||
->order('v.vehicle_name ASC'));
|
||||
|
||||
$this->sendJson($db->loadObjectList() ?: []);
|
||||
}
|
||||
|
||||
public function truckStock(): void
|
||||
{
|
||||
$this->requireAuth('core.manage');
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$vehicleId = Factory::getApplication()->getInput()->getInt('id', 0);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('ts.*, p.title AS product_name')
|
||||
->from($db->quoteName('#__mokosuitefield_truck_stock', 'ts'))
|
||||
->join('LEFT', $db->quoteName('#__mokosuite_crm_products', 'p') . ' ON p.id = ts.product_id')
|
||||
->where('ts.vehicle_id = ' . $vehicleId)
|
||||
->order('p.title ASC'));
|
||||
|
||||
$this->sendJson($db->loadObjectList() ?: []);
|
||||
}
|
||||
|
||||
public function listAgreements(): void
|
||||
{
|
||||
$this->requireAuth('core.manage');
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$input = Factory::getApplication()->getInput();
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->select('sa.*, cd.name AS customer_name, loc.address')
|
||||
->from($db->quoteName('#__mokosuitefield_service_agreements', 'sa'))
|
||||
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = sa.contact_id')
|
||||
->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = sa.location_id')
|
||||
->order('sa.end_date ASC');
|
||||
|
||||
$status = $input->getString('status', '');
|
||||
if ($status) $query->where($db->quoteName('sa.status') . ' = ' . $db->quote($status));
|
||||
|
||||
$db->setQuery($query, 0, 100);
|
||||
$this->sendJson($db->loadObjectList() ?: []);
|
||||
}
|
||||
|
||||
public function getAgreement(): void
|
||||
{
|
||||
$this->requireAuth('core.manage');
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$id = Factory::getApplication()->getInput()->getInt('id', 0);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('sa.*, cd.name AS customer_name, loc.name AS location_name, loc.address')
|
||||
->from($db->quoteName('#__mokosuitefield_service_agreements', 'sa'))
|
||||
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = sa.contact_id')
|
||||
->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = sa.location_id')
|
||||
->where('sa.id = ' . $id));
|
||||
$agreement = $db->loadObject();
|
||||
|
||||
if (!$agreement) {
|
||||
http_response_code(404);
|
||||
$this->sendJson(['error' => 'Agreement not found']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Work orders under this agreement
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('wo.id, wo.wo_number, wo.description, wo.status, wo.scheduled_date, wo.trade')
|
||||
->from($db->quoteName('#__mokosuitefield_work_orders', 'wo'))
|
||||
->where('wo.agreement_id = ' . $id)
|
||||
->order('wo.scheduled_date DESC'), 0, 30);
|
||||
$agreement->work_orders = $db->loadObjectList() ?: [];
|
||||
|
||||
$this->sendJson($agreement);
|
||||
}
|
||||
|
||||
private function sendJson(mixed $data): void
|
||||
{
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($data, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
|
||||
Factory::getApplication()->close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
namespace Moko\Component\MokoSuiteField\Api\Controller;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\MVC\Controller\BaseController;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
|
||||
/**
|
||||
* Estimates + Route API.
|
||||
*
|
||||
* GET /estimates — List estimates
|
||||
* POST /estimates — Create estimate from field
|
||||
* PATCH /estimates/{id}/status — Update estimate status (approve/reject)
|
||||
* POST /estimates/{id}/convert— Convert estimate to work order
|
||||
* GET /route/{techId} — Get optimized daily route
|
||||
* POST /route/{techId}/optimize — Trigger route optimization
|
||||
*/
|
||||
class FieldEstimatesController extends BaseController
|
||||
{
|
||||
private function requireAuth(string $action = 'core.manage'): void
|
||||
{
|
||||
$user = Factory::getApplication()->getIdentity();
|
||||
if (!$user || $user->guest || (!$user->authorise('core.admin') && !$user->authorise($action, 'com_mokosuitefield'))) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Access denied.']);
|
||||
Factory::getApplication()->close();
|
||||
}
|
||||
}
|
||||
|
||||
public function listEstimates(): void
|
||||
{
|
||||
$this->requireAuth('core.manage');
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$input = Factory::getApplication()->getInput();
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->select('e.*, cd.name AS customer_name, loc.address')
|
||||
->from($db->quoteName('#__mokosuitefield_estimates', 'e'))
|
||||
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = e.contact_id')
|
||||
->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = e.location_id')
|
||||
->order('e.created DESC');
|
||||
|
||||
$status = $input->getString('status', '');
|
||||
if ($status) $query->where($db->quoteName('e.status') . ' = ' . $db->quote($status));
|
||||
|
||||
$techId = $input->getInt('technician_id', 0);
|
||||
if ($techId) $query->where('e.technician_id = ' . $techId);
|
||||
|
||||
$db->setQuery($query, 0, 100);
|
||||
$this->sendJson($db->loadObjectList() ?: []);
|
||||
}
|
||||
|
||||
public function createEstimate(): void
|
||||
{
|
||||
$this->requireAuth('core.create');
|
||||
$input = Factory::getApplication()->getInput();
|
||||
|
||||
$estimateId = \Moko\Plugin\System\MokoSuiteField\Helper\EstimateHelper::createEstimate(
|
||||
$input->getInt('contact_id', 0),
|
||||
$input->getInt('location_id', 0),
|
||||
$input->getString('trade', 'general'),
|
||||
$input->getString('description', ''),
|
||||
json_decode($input->getString('line_items', '[]'), true) ?: []
|
||||
);
|
||||
|
||||
$this->sendJson(['success' => true, 'estimate_id' => $estimateId]);
|
||||
}
|
||||
|
||||
public function updateStatus(): void
|
||||
{
|
||||
$this->requireAuth('core.edit');
|
||||
$input = Factory::getApplication()->getInput();
|
||||
$id = $input->getInt('id', 0);
|
||||
$status = $input->getString('status', '');
|
||||
|
||||
if (!in_array($status, ['sent', 'approved', 'rejected', 'expired'])) {
|
||||
http_response_code(400);
|
||||
$this->sendJson(['error' => 'Invalid status']);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$update = (object) [
|
||||
'id' => $id,
|
||||
'status' => $status,
|
||||
];
|
||||
|
||||
if ($status === 'approved') {
|
||||
$update->approved_at = Factory::getDate()->toSql();
|
||||
}
|
||||
|
||||
$db->updateObject('#__mokosuitefield_estimates', $update, 'id');
|
||||
$this->sendJson(['success' => true]);
|
||||
}
|
||||
|
||||
public function convertToWorkOrder(): void
|
||||
{
|
||||
$this->requireAuth('core.create');
|
||||
$id = Factory::getApplication()->getInput()->getInt('id', 0);
|
||||
|
||||
$woId = \Moko\Plugin\System\MokoSuiteField\Helper\EstimateHelper::convertToWorkOrder($id);
|
||||
|
||||
if (!$woId) {
|
||||
http_response_code(400);
|
||||
$this->sendJson(['error' => 'Could not convert estimate']);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->sendJson(['success' => true, 'work_order_id' => $woId]);
|
||||
}
|
||||
|
||||
public function getRoute(): void
|
||||
{
|
||||
$this->requireAuth('core.manage');
|
||||
$techId = Factory::getApplication()->getInput()->getInt('tech_id', 0);
|
||||
$date = Factory::getApplication()->getInput()->getString('date', date('Y-m-d'));
|
||||
|
||||
$route = \Moko\Plugin\System\MokoSuiteField\Helper\RouteHelper::getTechRoute($techId, $date);
|
||||
$metrics = \Moko\Plugin\System\MokoSuiteField\Helper\RouteHelper::estimateRouteMetrics($techId, $date);
|
||||
|
||||
$this->sendJson([
|
||||
'route' => $route,
|
||||
'metrics' => $metrics,
|
||||
]);
|
||||
}
|
||||
|
||||
public function optimizeRoute(): void
|
||||
{
|
||||
$this->requireAuth('core.manage');
|
||||
$techId = Factory::getApplication()->getInput()->getInt('tech_id', 0);
|
||||
$date = Factory::getApplication()->getInput()->getString('date', date('Y-m-d'));
|
||||
|
||||
$optimized = \Moko\Plugin\System\MokoSuiteField\Helper\RouteHelper::optimizeRoute($techId, $date);
|
||||
$metrics = \Moko\Plugin\System\MokoSuiteField\Helper\RouteHelper::estimateRouteMetrics($techId, $date);
|
||||
|
||||
$this->sendJson([
|
||||
'route' => $optimized,
|
||||
'metrics' => $metrics,
|
||||
]);
|
||||
}
|
||||
|
||||
private function sendJson(mixed $data): void
|
||||
{
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($data, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
|
||||
Factory::getApplication()->close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user