getDatabase(); $fmt = match ($groupBy) { 'day' => '%Y-%m-%d', 'week' => '%Y-W%v', 'year' => '%Y', default => '%Y-%m' }; $db->setQuery($db->getQuery(true) ->select('DATE_FORMAT(inv.created, ' . $db->quote($fmt) . ') AS period') ->select('COUNT(*) AS invoice_count, COALESCE(SUM(inv.total), 0) AS revenue, COALESCE(SUM(inv.amount_paid), 0) AS collected') ->from($db->quoteName('#__mokosuiteclient_erp_invoices', 'inv')) ->where($db->quoteName('inv.created') . ' >= ' . $db->quote($from)) ->where($db->quoteName('inv.created') . ' <= ' . $db->quote($to . ' 23:59:59')) ->where($db->quoteName('inv.type') . ' = ' . $db->quote('standard')) ->group('period')->order('period ASC')); return $db->loadObjectList() ?: []; } public function getTopCustomers(string $from, string $to, int $limit = 10): array { $db = $this->getDatabase(); $db->setQuery($db->getQuery(true) ->select('cd.name AS contact_name, COALESCE(SUM(inv.total), 0) AS total_revenue, COUNT(*) AS invoice_count') ->from($db->quoteName('#__mokosuiteclient_erp_invoices', 'inv')) ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = inv.contact_id') ->where($db->quoteName('inv.created') . ' >= ' . $db->quote($from)) ->where($db->quoteName('inv.created') . ' <= ' . $db->quote($to . ' 23:59:59')) ->group('inv.contact_id')->order('total_revenue DESC'), 0, $limit); return $db->loadObjectList() ?: []; } public function getTopProducts(string $from, string $to, int $limit = 10): array { $db = $this->getDatabase(); $db->setQuery($db->getQuery(true) ->select('p.sku, c.title AS product_name, COALESCE(SUM(ii.quantity), 0) AS qty_sold, COALESCE(SUM(ii.line_total), 0) AS revenue') ->from($db->quoteName('#__mokosuiteclient_erp_invoice_items', 'ii')) ->join('INNER', $db->quoteName('#__mokosuiteclient_erp_invoices', 'inv') . ' ON inv.id = ii.invoice_id') ->join('LEFT', $db->quoteName('#__mokosuiteclient_erp_products', 'p') . ' ON p.id = ii.product_id') ->join('LEFT', $db->quoteName('#__content', 'c') . ' ON c.id = p.article_id') ->where($db->quoteName('inv.created') . ' >= ' . $db->quote($from)) ->where($db->quoteName('inv.created') . ' <= ' . $db->quote($to . ' 23:59:59')) ->where($db->quoteName('ii.product_id') . ' IS NOT NULL') ->group('ii.product_id')->order('revenue DESC'), 0, $limit); return $db->loadObjectList() ?: []; } public function getPipelineReport(string $from, string $to): array { $db = $this->getDatabase(); $db->setQuery($db->getQuery(true) ->select('status, COUNT(*) AS cnt, COALESCE(SUM(value), 0) AS total_value') ->from($db->quoteName('#__mokosuiteclient_erp_deals')) ->where($db->quoteName('created') . ' >= ' . $db->quote($from)) ->where($db->quoteName('created') . ' <= ' . $db->quote($to . ' 23:59:59')) ->group('status')); return $db->loadObjectList('status') ?: []; } public function getAgingReceivables(): array { $db = $this->getDatabase(); $db->setQuery($db->getQuery(true) ->select('inv.id, inv.ref, inv.total, inv.amount_paid, inv.due_date, (inv.total - inv.amount_paid) AS balance, DATEDIFF(CURDATE(), inv.due_date) AS days_overdue') ->select('cd.name AS contact_name') ->from($db->quoteName('#__mokosuiteclient_erp_invoices', 'inv')) ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = inv.contact_id') ->where($db->quoteName('inv.status') . ' IN (' . $db->quote('sent') . ',' . $db->quote('partial') . ',' . $db->quote('overdue') . ')') ->where('(inv.total - inv.amount_paid) > 0') ->order('days_overdue DESC')); return $db->loadObjectList() ?: []; } }