diff --git a/source/packages/com_mokosuitefield/admin/access.xml b/source/packages/com_mokosuitefield/admin/access.xml
new file mode 100644
index 0000000..6f4f869
--- /dev/null
+++ b/source/packages/com_mokosuitefield/admin/access.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/source/packages/com_mokosuitefield/admin/config.xml b/source/packages/com_mokosuitefield/admin/config.xml
new file mode 100644
index 0000000..0c4186f
--- /dev/null
+++ b/source/packages/com_mokosuitefield/admin/config.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
diff --git a/source/packages/com_mokosuitefield/site/src/Service/Router.php b/source/packages/com_mokosuitefield/site/src/Service/Router.php
new file mode 100644
index 0000000..408c728
--- /dev/null
+++ b/source/packages/com_mokosuitefield/site/src/Service/Router.php
@@ -0,0 +1,21 @@
+ [
+ '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;
+ }
+}
diff --git a/source/packages/plg_webservices_mokosuitefield/src/Extension/MokoSuiteFieldApi.php b/source/packages/plg_webservices_mokosuitefield/src/Extension/MokoSuiteFieldApi.php
new file mode 100644
index 0000000..4dfb1fa
--- /dev/null
+++ b/source/packages/plg_webservices_mokosuitefield/src/Extension/MokoSuiteFieldApi.php
@@ -0,0 +1,27 @@
+ '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']);
+ }
+}
diff --git a/source/updates.xml b/source/updates.xml
new file mode 100644
index 0000000..39bccee
--- /dev/null
+++ b/source/updates.xml
@@ -0,0 +1,9 @@
+
+
+Package - MokoSuite Field
+pkg_mokosuitefield
+package
+01.01.00
+
+8.3
+