diff --git a/source/packages/com_mokowaas/api/src/Controller/ErpActivitiesController.php b/source/packages/com_mokowaas/api/src/Controller/ErpActivitiesController.php new file mode 100644 index 00000000..dcecdc9c --- /dev/null +++ b/source/packages/com_mokowaas/api/src/Controller/ErpActivitiesController.php @@ -0,0 +1,193 @@ +requireAuth(); + + $app = Factory::getApplication(); + $db = Factory::getDbo(); + $input = $app->getInput(); + + $query = $db->getQuery(true) + ->select('a.*, cd.name AS contact_name, u.name AS user_name') + ->from($db->quoteName('#__mokowaas_erp_activities', 'a')) + ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = a.contact_id') + ->join('LEFT', $db->quoteName('#__users', 'u') . ' ON u.id = a.user_id') + ->order('a.created DESC'); + + $contactId = $input->getInt('contact_id', 0); + + if ($contactId) + { + $query->where($db->quoteName('a.contact_id') . ' = ' . $contactId); + } + + $dealId = $input->getInt('deal_id', 0); + + if ($dealId) + { + $query->where($db->quoteName('a.deal_id') . ' = ' . $dealId); + } + + $type = $input->get('type', '', 'CMD'); + + if ($type) + { + $query->where($db->quoteName('a.type') . ' = ' . $db->quote($type)); + } + + $limit = $input->getInt('limit', 50); + $offset = $input->getInt('offset', 0); + + $db->setQuery($query, $offset, $limit); + $items = $db->loadObjectList(); + + $this->sendJson(200, ['data' => $items, 'total' => \count($items)]); + } + + /** + * Create a new activity. + */ + public function add(): void + { + $this->requireAuth(); + + $input = json_decode(Factory::getApplication()->getInput()->json->getRaw(), true); + $db = Factory::getDbo(); + + $contactId = (int) ($input['contact_id'] ?? 0); + $type = $input['type'] ?? 'note'; + + if (!$contactId) + { + $this->sendJson(400, ['error' => 'contact_id is required']); + + return; + } + + if (!\in_array($type, ['call', 'email', 'meeting', 'note', 'task'], true)) + { + $this->sendJson(400, ['error' => 'type must be one of: call, email, meeting, note, task']); + + return; + } + + $obj = (object) [ + 'contact_id' => $contactId, + 'deal_id' => ($input['deal_id'] ?? null) ? (int) $input['deal_id'] : null, + 'type' => $type, + 'subject' => $input['subject'] ?? '', + 'body' => $input['body'] ?? null, + 'due_date' => $input['due_date'] ?? null, + 'completed' => (int) ($input['completed'] ?? 0), + 'user_id' => (int) Factory::getUser()->id, + 'created' => Factory::getDate()->toSql(), + ]; + + $db->insertObject('#__mokowaas_erp_activities', $obj, 'id'); + + $this->sendJson(201, ['data' => $obj, 'id' => $db->insertid()]); + } + + /** + * Update an activity. + */ + public function edit(): void + { + $this->requireAuth(); + + $id = Factory::getApplication()->getInput()->getInt('id', 0); + $input = json_decode(Factory::getApplication()->getInput()->json->getRaw(), true); + $db = Factory::getDbo(); + + $allowed = ['subject', 'body', 'due_date', 'completed', 'type']; + $updates = []; + + foreach ($allowed as $field) + { + if (isset($input[$field])) + { + $updates[$field] = $input[$field]; + } + } + + if (empty($updates)) + { + $this->sendJson(400, ['error' => 'No valid fields to update']); + + return; + } + + $obj = (object) array_merge(['id' => $id], $updates); + $db->updateObject('#__mokowaas_erp_activities', $obj, 'id'); + + $this->sendJson(200, ['data' => $obj]); + } + + /** + * Delete an activity. + */ + public function delete(): void + { + $this->requireAuth(); + + $id = Factory::getApplication()->getInput()->getInt('id', 0); + $db = Factory::getDbo(); + + $db->setQuery( + $db->getQuery(true) + ->delete($db->quoteName('#__mokowaas_erp_activities')) + ->where($db->quoteName('id') . ' = ' . $id) + ); + $db->execute(); + + $this->sendJson(200, ['message' => 'Activity deleted', 'id' => $id]); + } + + private function requireAuth(): void + { + $user = Factory::getApplication()->getIdentity(); + + if (!$user->authorise('core.manage', 'com_mokowaas')) + { + $this->sendJson(403, ['error' => 'Not authorized']); + $this->app->close(); + } + } + + private function sendJson(int $code, array $data): void + { + $app = Factory::getApplication(); + $app->setHeader('Content-Type', 'application/json', true); + $app->setHeader('Status', (string) $code, true); + echo json_encode($data, JSON_UNESCAPED_SLASHES); + } +} diff --git a/source/packages/com_mokowaas/api/src/Controller/ErpDealsController.php b/source/packages/com_mokowaas/api/src/Controller/ErpDealsController.php new file mode 100644 index 00000000..bb031213 --- /dev/null +++ b/source/packages/com_mokowaas/api/src/Controller/ErpDealsController.php @@ -0,0 +1,263 @@ +requireAuth(); + + $app = Factory::getApplication(); + $db = Factory::getDbo(); + $input = $app->getInput(); + + $query = $db->getQuery(true) + ->select('d.*, ps.title AS stage_title, ps.color AS stage_color, ps.probability AS stage_probability') + ->select('cd.name AS contact_name') + ->from($db->quoteName('#__mokowaas_erp_deals', 'd')) + ->join('LEFT', $db->quoteName('#__mokowaas_erp_pipeline_stages', 'ps') . ' ON ps.id = d.stage_id') + ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = d.contact_id') + ->order('d.created DESC'); + + $status = $input->get('status', '', 'CMD'); + + if ($status) + { + $query->where($db->quoteName('d.status') . ' = ' . $db->quote($status)); + } + + $stageId = $input->getInt('stage_id', 0); + + if ($stageId) + { + $query->where($db->quoteName('d.stage_id') . ' = ' . $stageId); + } + + $contactId = $input->getInt('contact_id', 0); + + if ($contactId) + { + $query->where($db->quoteName('d.contact_id') . ' = ' . $contactId); + } + + $assignedTo = $input->getInt('assigned_to', 0); + + if ($assignedTo) + { + $query->where($db->quoteName('d.assigned_to') . ' = ' . $assignedTo); + } + + $limit = $input->getInt('limit', 50); + $offset = $input->getInt('offset', 0); + + $db->setQuery($query, $offset, $limit); + $items = $db->loadObjectList(); + + $this->sendJson(200, ['data' => $items, 'total' => \count($items)]); + } + + /** + * Get a single deal by ID. + */ + public function displayItem(): void + { + $this->requireAuth(); + + $id = Factory::getApplication()->getInput()->getInt('id', 0); + $db = Factory::getDbo(); + + $db->setQuery( + $db->getQuery(true) + ->select('d.*, ps.title AS stage_title, ps.color AS stage_color') + ->select('cd.name AS contact_name') + ->from($db->quoteName('#__mokowaas_erp_deals', 'd')) + ->join('LEFT', $db->quoteName('#__mokowaas_erp_pipeline_stages', 'ps') . ' ON ps.id = d.stage_id') + ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = d.contact_id') + ->where($db->quoteName('d.id') . ' = ' . $id) + ); + + $item = $db->loadObject(); + + if (!$item) + { + $this->sendJson(404, ['error' => 'Deal not found']); + + return; + } + + // Include activities for this deal + $db->setQuery( + $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__mokowaas_erp_activities')) + ->where($db->quoteName('deal_id') . ' = ' . $id) + ->order('created DESC'), + 0, 20 + ); + + $item->activities = $db->loadObjectList(); + + $this->sendJson(200, ['data' => $item]); + } + + /** + * Create a new deal. + */ + public function add(): void + { + $this->requireAuth(); + + $input = json_decode(Factory::getApplication()->getInput()->json->getRaw(), true); + $db = Factory::getDbo(); + $now = Factory::getDate()->toSql(); + + $title = trim($input['title'] ?? ''); + $contactId = (int) ($input['contact_id'] ?? 0); + + if (!$title || !$contactId) + { + $this->sendJson(400, ['error' => 'title and contact_id are required']); + + return; + } + + $obj = (object) [ + 'title' => $title, + 'contact_id' => $contactId, + 'stage_id' => (int) ($input['stage_id'] ?? 1), + 'value' => (float) ($input['value'] ?? 0), + 'currency' => $input['currency'] ?? 'USD', + 'probability' => isset($input['probability']) ? (int) $input['probability'] : null, + 'expected_close' => $input['expected_close'] ?? null, + 'assigned_to' => (int) ($input['assigned_to'] ?? Factory::getUser()->id), + 'status' => 'open', + 'notes' => $input['notes'] ?? null, + 'created' => $now, + 'created_by' => (int) Factory::getUser()->id, + ]; + + $db->insertObject('#__mokowaas_erp_deals', $obj, 'id'); + + $this->sendJson(201, ['data' => $obj, 'id' => $db->insertid()]); + } + + /** + * Update a deal (including stage moves for pipeline drag-and-drop). + */ + public function edit(): void + { + $this->requireAuth(); + + $id = Factory::getApplication()->getInput()->getInt('id', 0); + $input = json_decode(Factory::getApplication()->getInput()->json->getRaw(), true); + $db = Factory::getDbo(); + + $allowed = [ + 'title', 'stage_id', 'value', 'currency', 'probability', + 'expected_close', 'assigned_to', 'status', 'lost_reason', 'notes', + ]; + + $updates = []; + + foreach ($allowed as $field) + { + if (isset($input[$field])) + { + $updates[$field] = $input[$field]; + } + } + + if (empty($updates)) + { + $this->sendJson(400, ['error' => 'No valid fields to update']); + + return; + } + + $updates['modified'] = Factory::getDate()->toSql(); + + // Auto-set closed date on won/lost + if (isset($updates['status']) && \in_array($updates['status'], ['won', 'lost'], true)) + { + $updates['closed'] = Factory::getDate()->toSql(); + } + + $obj = (object) array_merge(['id' => $id], $updates); + $db->updateObject('#__mokowaas_erp_deals', $obj, 'id'); + + $this->sendJson(200, ['data' => $obj]); + } + + /** + * Delete a deal and its activities. + */ + public function delete(): void + { + $this->requireAuth(); + + $id = Factory::getApplication()->getInput()->getInt('id', 0); + $db = Factory::getDbo(); + + // Delete linked activities first + $db->setQuery( + $db->getQuery(true) + ->delete($db->quoteName('#__mokowaas_erp_activities')) + ->where($db->quoteName('deal_id') . ' = ' . $id) + ); + $db->execute(); + + $db->setQuery( + $db->getQuery(true) + ->delete($db->quoteName('#__mokowaas_erp_deals')) + ->where($db->quoteName('id') . ' = ' . $id) + ); + $db->execute(); + + $this->sendJson(200, ['message' => 'Deal deleted', 'id' => $id]); + } + + private function requireAuth(): void + { + $user = Factory::getApplication()->getIdentity(); + + if (!$user->authorise('core.manage', 'com_mokowaas')) + { + $this->sendJson(403, ['error' => 'Not authorized']); + $this->app->close(); + } + } + + private function sendJson(int $code, array $data): void + { + $app = Factory::getApplication(); + $app->setHeader('Content-Type', 'application/json', true); + $app->setHeader('Status', (string) $code, true); + echo json_encode($data, JSON_UNESCAPED_SLASHES); + } +} diff --git a/source/packages/com_mokowaas/api/src/Controller/ErpPipelineController.php b/source/packages/com_mokowaas/api/src/Controller/ErpPipelineController.php new file mode 100644 index 00000000..d1ed601d --- /dev/null +++ b/source/packages/com_mokowaas/api/src/Controller/ErpPipelineController.php @@ -0,0 +1,101 @@ +getIdentity(); + + if (!$user->authorise('core.manage', 'com_mokowaas')) + { + $this->sendJson(403, ['error' => 'Not authorized']); + + return; + } + + $db = Factory::getDbo(); + + // Get all stages + $db->setQuery( + $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__mokowaas_erp_pipeline_stages')) + ->where($db->quoteName('published') . ' = 1') + ->order($db->quoteName('ordering') . ' ASC') + ); + + $stages = $db->loadObjectList(); + + // Get deal counts and values per stage (open deals only) + $db->setQuery( + $db->getQuery(true) + ->select($db->quoteName('stage_id')) + ->select('COUNT(*) AS ' . $db->quoteName('deal_count')) + ->select('COALESCE(SUM(' . $db->quoteName('value') . '), 0) AS ' . $db->quoteName('total_value')) + ->from($db->quoteName('#__mokowaas_erp_deals')) + ->where($db->quoteName('status') . ' = ' . $db->quote('open')) + ->group($db->quoteName('stage_id')) + ); + + $dealStats = []; + + foreach ($db->loadObjectList() as $row) + { + $dealStats[(int) $row->stage_id] = $row; + } + + // Merge stats into stages + foreach ($stages as &$stage) + { + $stats = $dealStats[(int) $stage->id] ?? null; + $stage->deal_count = $stats ? (int) $stats->deal_count : 0; + $stage->total_value = $stats ? (float) $stats->total_value : 0.00; + $stage->weighted_value = $stage->total_value * ($stage->probability / 100); + } + + // Summary totals + $totalDeals = array_sum(array_column($stages, 'deal_count')); + $totalValue = array_sum(array_column($stages, 'total_value')); + $weightedValue = array_sum(array_column($stages, 'weighted_value')); + + $this->sendJson(200, [ + 'stages' => $stages, + 'summary' => [ + 'total_deals' => $totalDeals, + 'total_value' => $totalValue, + 'weighted_value' => round($weightedValue, 2), + ], + ]); + } + + private function sendJson(int $code, array $data): void + { + $app = Factory::getApplication(); + $app->setHeader('Content-Type', 'application/json', true); + $app->setHeader('Status', (string) $code, true); + echo json_encode($data, JSON_UNESCAPED_SLASHES); + } +} diff --git a/source/packages/com_mokowaas/api/src/Controller/ErpRolesController.php b/source/packages/com_mokowaas/api/src/Controller/ErpRolesController.php new file mode 100644 index 00000000..1b12b2d4 --- /dev/null +++ b/source/packages/com_mokowaas/api/src/Controller/ErpRolesController.php @@ -0,0 +1,256 @@ +requireAuth(); + + $app = Factory::getApplication(); + $db = Factory::getDbo(); + $input = $app->getInput(); + + $query = $db->getQuery(true) + ->select('r.*, cd.name AS contact_name, cd.email_to AS contact_email') + ->from($db->quoteName('#__mokowaas_erp_roles', 'r')) + ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = r.contact_id') + ->order('r.created DESC'); + + // Filters + $role = $input->get('role', '', 'CMD'); + + if ($role) + { + $query->where($db->quoteName('r.role') . ' = ' . $db->quote($role)); + } + + $status = $input->get('status', '', 'CMD'); + + if ($status) + { + $query->where($db->quoteName('r.status') . ' = ' . $db->quote($status)); + } + + $contactId = $input->getInt('contact_id', 0); + + if ($contactId) + { + $query->where($db->quoteName('r.contact_id') . ' = ' . $contactId); + } + + $limit = $input->getInt('limit', 50); + $offset = $input->getInt('offset', 0); + + $db->setQuery($query, $offset, $limit); + $items = $db->loadObjectList(); + + $this->sendJson(200, ['data' => $items, 'total' => \count($items)]); + } + + /** + * Get a single role by ID. + */ + public function displayItem(): void + { + $this->requireAuth(); + + $id = Factory::getApplication()->getInput()->getInt('id', 0); + $db = Factory::getDbo(); + + $db->setQuery( + $db->getQuery(true) + ->select('r.*, cd.name AS contact_name, cd.email_to AS contact_email') + ->from($db->quoteName('#__mokowaas_erp_roles', 'r')) + ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = r.contact_id') + ->where($db->quoteName('r.id') . ' = ' . $id) + ); + + $item = $db->loadObject(); + + if (!$item) + { + $this->sendJson(404, ['error' => 'Role not found']); + + return; + } + + $this->sendJson(200, ['data' => $item]); + } + + /** + * Create a new ERP role. + */ + public function add(): void + { + $this->requireAuth(); + + $app = Factory::getApplication(); + $input = json_decode($app->getInput()->json->getRaw(), true); + + $contactId = (int) ($input['contact_id'] ?? 0); + $role = $input['role'] ?? ''; + + if (!$contactId || !\in_array($role, ['customer', 'vendor', 'prospect'], true)) + { + $this->sendJson(400, ['error' => 'contact_id and valid role (customer, vendor, prospect) are required']); + + return; + } + + $db = Factory::getDbo(); + + // Check duplicate + $db->setQuery( + $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__mokowaas_erp_roles')) + ->where($db->quoteName('contact_id') . ' = ' . $contactId) + ->where($db->quoteName('role') . ' = ' . $db->quote($role)) + ); + + if ((int) $db->loadResult() > 0) + { + $this->sendJson(409, ['error' => 'Role already exists for this contact']); + + return; + } + + $prefixes = ['customer' => 'CU', 'vendor' => 'VN', 'prospect' => 'PR']; + $prefix = $prefixes[$role]; + + // Use ErpHelper if available, otherwise inline + $year = date('Y'); + $like = $db->quote($prefix . $year . '-%'); + + $db->setQuery( + $db->getQuery(true) + ->select('MAX(' . $db->quoteName('code') . ')') + ->from($db->quoteName('#__mokowaas_erp_roles')) + ->where($db->quoteName('role') . ' = ' . $db->quote($role)) + ->where($db->quoteName('code') . ' LIKE ' . $like) + ); + + $maxCode = $db->loadResult(); + $seq = $maxCode ? ((int) end(explode('-', $maxCode)) + 1) : 1; + $code = sprintf('%s%s-%03d', $prefix, $year, $seq); + + $obj = (object) [ + 'contact_id' => $contactId, + 'role' => $role, + 'status' => $input['status'] ?? ($role === 'prospect' ? 'lead' : 'active'), + 'code' => $code, + 'credit_limit' => $input['credit_limit'] ?? null, + 'payment_terms_days' => (int) ($input['payment_terms_days'] ?? 30), + 'notes' => $input['notes'] ?? null, + 'created' => Factory::getDate()->toSql(), + 'created_by' => (int) Factory::getUser()->id, + ]; + + $db->insertObject('#__mokowaas_erp_roles', $obj, 'id'); + + $this->sendJson(201, ['data' => $obj, 'id' => $db->insertid()]); + } + + /** + * Update an existing role. + */ + public function edit(): void + { + $this->requireAuth(); + + $id = Factory::getApplication()->getInput()->getInt('id', 0); + $input = json_decode(Factory::getApplication()->getInput()->json->getRaw(), true); + $db = Factory::getDbo(); + + $allowed = ['status', 'credit_limit', 'payment_terms_days', 'notes']; + $updates = []; + + foreach ($allowed as $field) + { + if (isset($input[$field])) + { + $updates[$field] = $input[$field]; + } + } + + if (empty($updates)) + { + $this->sendJson(400, ['error' => 'No valid fields to update']); + + return; + } + + $updates['modified'] = Factory::getDate()->toSql(); + + $obj = (object) array_merge(['id' => $id], $updates); + $db->updateObject('#__mokowaas_erp_roles', $obj, 'id'); + + $this->sendJson(200, ['data' => $obj]); + } + + /** + * Delete a role. + */ + public function delete(): void + { + $this->requireAuth(); + + $id = Factory::getApplication()->getInput()->getInt('id', 0); + $db = Factory::getDbo(); + + $db->setQuery( + $db->getQuery(true) + ->delete($db->quoteName('#__mokowaas_erp_roles')) + ->where($db->quoteName('id') . ' = ' . $id) + ); + $db->execute(); + + $this->sendJson(200, ['message' => 'Role deleted', 'id' => $id]); + } + + private function requireAuth(): void + { + $user = Factory::getApplication()->getIdentity(); + + if (!$user->authorise('core.manage', 'com_mokowaas')) + { + $this->sendJson(403, ['error' => 'Not authorized']); + $this->app->close(); + } + } + + private function sendJson(int $code, array $data): void + { + $app = Factory::getApplication(); + $app->setHeader('Content-Type', 'application/json', true); + $app->setHeader('Status', (string) $code, true); + echo json_encode($data, JSON_UNESCAPED_SLASHES); + } +} diff --git a/source/packages/plg_webservices_mokowaas/src/Extension/MokoWaaSApi.php b/source/packages/plg_webservices_mokowaas/src/Extension/MokoWaaSApi.php index 37235506..a14454e2 100644 --- a/source/packages/plg_webservices_mokowaas/src/Extension/MokoWaaSApi.php +++ b/source/packages/plg_webservices_mokowaas/src/Extension/MokoWaaSApi.php @@ -124,5 +124,30 @@ final class MokoWaaSApi extends CMSPlugin implements SubscriberInterface 'provision', ['component' => 'com_mokowaas'] ); + + // ERP routes + $router->createCRUDRoutes( + 'v1/mokowaas/erp/roles', + 'erproles', + ['component' => 'com_mokowaas'] + ); + + $router->createCRUDRoutes( + 'v1/mokowaas/erp/deals', + 'erpdeals', + ['component' => 'com_mokowaas'] + ); + + $router->createCRUDRoutes( + 'v1/mokowaas/erp/activities', + 'erpactivities', + ['component' => 'com_mokowaas'] + ); + + $router->createCRUDRoutes( + 'v1/mokowaas/erp/pipeline', + 'erppipeline', + ['component' => 'com_mokowaas'] + ); } }