feat: initial scaffold — package manifest with dlid/updateservers + first helper
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s

This commit is contained in:
Jonathan Miller
2026-06-21 17:26:57 -05:00
parent c32cde04c5
commit cfb48eea49
2 changed files with 151 additions and 0 deletions
@@ -0,0 +1,125 @@
<?php
namespace Moko\Plugin\System\MokoSuiteRealty\Helper;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\Database\DatabaseInterface;
/**
* Property listing management — CRUD, status workflow, price history, days on market.
*/
class ListingHelper
{
/**
* Get listings with filters.
*/
public static function getListings(array $filters = [], int $limit = 20, int $offset = 0): array
{
$db = Factory::getContainer()->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;
}
}
}
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<extension type="package" method="upgrade">
<name>Package - MokoSuite Realty</name>
<packagename>mokosuiterealty</packagename>
<version>01.00.00</version>
<creationDate>2026-06-21</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GNU General Public License version 3 or later; see LICENSE</license>
<description>MokoSuite Realty - real estate management: listings, showings, offers, commissions. Layer 2 add-on (requires CRM).</description>
<php_minimum>8.3</php_minimum>
<dlid prefix="dlid=" suffix=""/>
<blockChildUninstall>true</blockChildUninstall>
<scriptfile>script.php</scriptfile>
<files folder="packages">
<file type="plugin" id="plg_system_mokosuiterealty" group="system">plg_system_mokosuiterealty.zip</file>
<file type="component" id="com_mokosuiterealty">com_mokosuiterealty.zip</file>
</files>
<updateservers>
<server type="extension" priority="1" name="Package - MokoSuite Realty">https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteRealty/updates.xml</server>
</updateservers>
</extension>