diff --git a/source/packages/com_mokosuitefield/api/src/Controller/FieldEquipmentController.php b/source/packages/com_mokosuitefield/api/src/Controller/FieldEquipmentController.php new file mode 100644 index 0000000..881eece --- /dev/null +++ b/source/packages/com_mokosuitefield/api/src/Controller/FieldEquipmentController.php @@ -0,0 +1,178 @@ +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(); + } +} diff --git a/source/packages/com_mokosuitefield/api/src/Controller/FieldEstimatesController.php b/source/packages/com_mokosuitefield/api/src/Controller/FieldEstimatesController.php new file mode 100644 index 0000000..5e598c9 --- /dev/null +++ b/source/packages/com_mokosuitefield/api/src/Controller/FieldEstimatesController.php @@ -0,0 +1,150 @@ +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(); + } +}