feat: FieldReportsController API #18
@@ -0,0 +1,122 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Field Service Reports API.
|
||||
*
|
||||
* GET /reports/tech-performance — Technician performance metrics
|
||||
* GET /reports/revenue — Revenue by trade/period
|
||||
* GET /reports/parts-usage — Parts consumption summary
|
||||
* GET /reports/sla-compliance — SLA compliance rates
|
||||
*/
|
||||
class FieldReportsController extends BaseController
|
||||
{
|
||||
private function requireAuth(): void
|
||||
{
|
||||
$user = Factory::getApplication()->getIdentity();
|
||||
if (!$user || $user->guest || (!$user->authorise('core.admin') && !$user->authorise('core.manage', 'com_mokosuitefield'))) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Access denied']);
|
||||
Factory::getApplication()->close();
|
||||
}
|
||||
}
|
||||
|
||||
public function techPerformance(): void
|
||||
{
|
||||
$this->requireAuth();
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$input = Factory::getApplication()->getInput();
|
||||
$from = $input->getString('from', date('Y-m-01'));
|
||||
$to = $input->getString('to', date('Y-m-d'));
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('t.id, cd.name AS tech_name, t.trade')
|
||||
->select('COUNT(wo.id) AS total_jobs')
|
||||
->select('SUM(CASE WHEN wo.status = ' . $db->quote('completed') . ' THEN 1 ELSE 0 END) AS completed')
|
||||
->select('COALESCE(AVG(TIMESTAMPDIFF(MINUTE, wo.dispatched_at, wo.completed_at)), 0) AS avg_resolution_min')
|
||||
->select('COALESCE(SUM(te.hours), 0) AS total_hours')
|
||||
->from($db->quoteName('#__mokosuitefield_technicians', 't'))
|
||||
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = t.contact_id')
|
||||
->join('LEFT', $db->quoteName('#__mokosuitefield_work_orders', 'wo') . ' ON wo.technician_id = t.id AND wo.scheduled_date BETWEEN ' . $db->quote($from) . ' AND ' . $db->quote($to))
|
||||
->join('LEFT', $db->quoteName('#__mokosuitefield_time_entries', 'te') . ' ON te.wo_id = wo.id')
|
||||
->group('t.id')
|
||||
->order('completed DESC'));
|
||||
|
||||
$techs = $db->loadObjectList() ?: [];
|
||||
|
||||
foreach ($techs as &$tech) {
|
||||
$tech->completion_rate = (int) $tech->total_jobs > 0
|
||||
? round((int) $tech->completed / (int) $tech->total_jobs * 100, 1) : 0;
|
||||
}
|
||||
|
||||
$this->sendJson($techs);
|
||||
}
|
||||
|
||||
public function revenueByTrade(): void
|
||||
{
|
||||
$this->requireAuth();
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$input = Factory::getApplication()->getInput();
|
||||
$from = $input->getString('from', date('Y-m-01'));
|
||||
$to = $input->getString('to', date('Y-m-d'));
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('wo.trade')
|
||||
->select('COUNT(wo.id) AS job_count')
|
||||
->select('COALESCE(SUM(i.total), 0) AS revenue')
|
||||
->select('COALESCE(AVG(i.total), 0) AS avg_invoice')
|
||||
->from($db->quoteName('#__mokosuitefield_work_orders', 'wo'))
|
||||
->join('LEFT', $db->quoteName('#__mokosuite_crm_invoices', 'i') . ' ON i.id = wo.invoice_id')
|
||||
->where('wo.scheduled_date BETWEEN ' . $db->quote($from) . ' AND ' . $db->quote($to))
|
||||
->where($db->quoteName('wo.status') . ' = ' . $db->quote('completed'))
|
||||
->group('wo.trade')
|
||||
->order('revenue DESC'));
|
||||
|
||||
$this->sendJson($db->loadObjectList() ?: []);
|
||||
}
|
||||
|
||||
public function partsUsage(): void
|
||||
{
|
||||
$this->requireAuth();
|
||||
$parts = \Moko\Plugin\System\MokoSuiteField\Helper\PartsHelper::getCommonParts('', 30);
|
||||
$lowStock = \Moko\Plugin\System\MokoSuiteField\Helper\PartsHelper::getLowStockParts(20);
|
||||
|
||||
$this->sendJson(['top_used' => $parts, 'low_stock' => $lowStock]);
|
||||
}
|
||||
|
||||
public function slaCompliance(): void
|
||||
{
|
||||
$this->requireAuth();
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$input = Factory::getApplication()->getInput();
|
||||
$from = $input->getString('from', date('Y-m-01'));
|
||||
$to = $input->getString('to', date('Y-m-d'));
|
||||
$slaHours = $input->getInt('sla_hours', 24);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('COUNT(*) AS total')
|
||||
->select('SUM(CASE WHEN TIMESTAMPDIFF(HOUR, wo.created, wo.completed_at) <= ' . (int) $slaHours . ' THEN 1 ELSE 0 END) AS within_sla')
|
||||
->from($db->quoteName('#__mokosuitefield_work_orders', 'wo'))
|
||||
->where($db->quoteName('wo.status') . ' = ' . $db->quote('completed'))
|
||||
->where('wo.scheduled_date BETWEEN ' . $db->quote($from) . ' AND ' . $db->quote($to)));
|
||||
|
||||
$stats = $db->loadObject() ?: (object) ['total' => 0, 'within_sla' => 0];
|
||||
$stats->sla_pct = (int) $stats->total > 0 ? round((int) $stats->within_sla / (int) $stats->total * 100, 1) : 0;
|
||||
$stats->sla_target_hours = $slaHours;
|
||||
|
||||
$this->sendJson($stats);
|
||||
}
|
||||
|
||||
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