From cfb48eea4977c019a1eb5bd0a6c62ed6cab87f92 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 21 Jun 2026 17:26:57 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20initial=20scaffold=20=E2=80=94=20packag?= =?UTF-8?q?e=20manifest=20with=20dlid/updateservers=20+=20first=20helper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Helper/ListingHelper.php | 125 ++++++++++++++++++ source/pkg_mokosuiterealty.xml | 26 ++++ 2 files changed, 151 insertions(+) create mode 100644 source/packages/plg_system_mokosuiterealty/src/Helper/ListingHelper.php create mode 100644 source/pkg_mokosuiterealty.xml diff --git a/source/packages/plg_system_mokosuiterealty/src/Helper/ListingHelper.php b/source/packages/plg_system_mokosuiterealty/src/Helper/ListingHelper.php new file mode 100644 index 0000000..5ab3b26 --- /dev/null +++ b/source/packages/plg_system_mokosuiterealty/src/Helper/ListingHelper.php @@ -0,0 +1,125 @@ +get(DatabaseInterface::class); + + $query = $db->getQuery(true) + ->select('l.*, cd_agent.name AS agent_name') + ->select('(SELECT COUNT(*) FROM #__mokosuiterealty_listing_photos p WHERE p.listing_id = l.id) AS photo_count') + ->select('DATEDIFF(NOW(), l.listed_at) AS days_on_market') + ->from($db->quoteName('#__mokosuiterealty_listings', 'l')) + ->join('LEFT', $db->quoteName('#__contact_details', 'cd_agent') . ' ON cd_agent.id = l.agent_id') + ->order('l.listed_at DESC'); + + if (!empty($filters['status'])) { + $query->where($db->quoteName('l.status') . ' = ' . $db->quote($filters['status'])); + } + if (!empty($filters['type'])) { + $query->where($db->quoteName('l.property_type') . ' = ' . $db->quote($filters['type'])); + } + if (!empty($filters['min_price'])) { + $query->where('l.price >= ' . (float) $filters['min_price']); + } + if (!empty($filters['max_price'])) { + $query->where('l.price <= ' . (float) $filters['max_price']); + } + if (!empty($filters['bedrooms'])) { + $query->where('l.bedrooms >= ' . (int) $filters['bedrooms']); + } + if (!empty($filters['city'])) { + $query->where($db->quoteName('l.city') . ' = ' . $db->quote($filters['city'])); + } + if (!empty($filters['agent_id'])) { + $query->where('l.agent_id = ' . (int) $filters['agent_id']); + } + + $db->setQuery($query, $offset, min(max(1, $limit), 100)); + + return $db->loadObjectList() ?: []; + } + + /** + * Get listing dashboard stats. + */ + public static function getDashboard(): object + { + $db = Factory::getContainer()->get(DatabaseInterface::class); + + $db->setQuery($db->getQuery(true) + ->select('COUNT(*) AS total_listings') + ->select('SUM(CASE WHEN status = ' . $db->quote('active') . ' THEN 1 ELSE 0 END) AS active') + ->select('SUM(CASE WHEN status = ' . $db->quote('pending') . ' THEN 1 ELSE 0 END) AS pending') + ->select('SUM(CASE WHEN status = ' . $db->quote('sold') . ' THEN 1 ELSE 0 END) AS sold') + ->select('AVG(CASE WHEN status = ' . $db->quote('active') . ' THEN price ELSE NULL END) AS avg_active_price') + ->select('AVG(CASE WHEN status = ' . $db->quote('sold') . ' THEN DATEDIFF(sold_at, listed_at) ELSE NULL END) AS avg_days_to_sell') + ->from('#__mokosuiterealty_listings')); + + $stats = $db->loadObject(); + $stats->avg_active_price = round((float) ($stats->avg_active_price ?? 0), 0); + $stats->avg_days_to_sell = round((float) ($stats->avg_days_to_sell ?? 0), 0); + + return $stats; + } + + /** + * Record a price change and log the history. + */ + public static function changePrice(int $listingId, float $newPrice, ?string $reason = null): bool + { + if ($newPrice <= 0) { + throw new \InvalidArgumentException('Price must be positive.'); + } + + $db = Factory::getContainer()->get(DatabaseInterface::class); + + // Get current price + $db->setQuery($db->getQuery(true)->select('price')->from('#__mokosuiterealty_listings')->where('id = ' . (int) $listingId)); + $oldPrice = (float) $db->loadResult(); + + if ($oldPrice === $newPrice) { + return false; + } + + $db->transactionStart(); + + try { + // Update price + $db->setQuery($db->getQuery(true) + ->update('#__mokosuiterealty_listings') + ->set('price = ' . (float) $newPrice) + ->where('id = ' . (int) $listingId)); + $db->execute(); + + // Log history + $db->insertObject('#__mokosuiterealty_price_history', (object) [ + 'listing_id' => $listingId, + 'old_price' => $oldPrice, + 'new_price' => $newPrice, + 'reason' => $reason, + 'changed_at' => Factory::getDate()->toSql(), + 'changed_by' => (int) Factory::getApplication()->getIdentity()->id, + ]); + + $db->transactionCommit(); + return true; + } catch (\Throwable $e) { + $db->transactionRollback(); + return false; + } + } +} diff --git a/source/pkg_mokosuiterealty.xml b/source/pkg_mokosuiterealty.xml new file mode 100644 index 0000000..c34715a --- /dev/null +++ b/source/pkg_mokosuiterealty.xml @@ -0,0 +1,26 @@ + + + Package - MokoSuite Realty + mokosuiterealty + 01.00.00 + 2026-06-21 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GNU General Public License version 3 or later; see LICENSE + MokoSuite Realty - real estate management: listings, showings, offers, commissions. Layer 2 add-on (requires CRM). + 8.3 + + true + script.php + + + plg_system_mokosuiterealty.zip + com_mokosuiterealty.zip + + + + https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteRealty/updates.xml + +