From d0b9a4015721fca2aff464c880aeca11efcf606c Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 21 May 2026 11:55:13 -0500 Subject: [PATCH 1/3] feat(component): implement core logic for models, table, and templates - LocationTable: alias generation, lat/lng validation, timestamps - Admin LocationsModel: filters for published, category, search, ordering - Site LocationsModel: search, city/state filter, Haversine radius search - Admin list template: render location rows with edit links - Map module dispatcher: load locations with coordinates for markers - Search module dispatcher: distinct cities/states, radius options - Site template: website, hours, image display with Schema.org Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../admin/src/Model/LocationsModel.php | 44 +++++++++++++++-- .../admin/src/Table/LocationTable.php | 47 +++++++++++++++++-- .../admin/tmpl/locations/default.php | 26 +++++++++- .../src/Dispatcher/Dispatcher.php | 40 ++++++++++++++-- .../src/Dispatcher/Dispatcher.php | 30 +++++++++++- .../tmpl/default.php | 32 +++++++++++-- 6 files changed, 202 insertions(+), 17 deletions(-) diff --git a/src/packages/com_mokojoomstorelocator/admin/src/Model/LocationsModel.php b/src/packages/com_mokojoomstorelocator/admin/src/Model/LocationsModel.php index ee3312a..d660c57 100644 --- a/src/packages/com_mokojoomstorelocator/admin/src/Model/LocationsModel.php +++ b/src/packages/com_mokojoomstorelocator/admin/src/Model/LocationsModel.php @@ -59,10 +59,46 @@ class LocationsModel extends ListModel $query->select('a.*') ->from($db->quoteName('#__mokojoomstorelocator_locations', 'a')); - // TODO: Add filter by published state - // TODO: Add filter by category - // TODO: Add search filter - // TODO: Add ordering clause + $published = $this->getState('filter.published'); + + if (is_numeric($published)) + { + $query->where($db->quoteName('a.published') . ' = :published') + ->bind(':published', $published, \Joomla\Database\ParameterType::INTEGER); + } + elseif ($published === '') + { + $query->where($db->quoteName('a.published') . ' IN (0, 1)'); + } + + $catId = (int) $this->getState('filter.category_id'); + + if ($catId > 0) + { + $query->where($db->quoteName('a.catid') . ' = :catid') + ->bind(':catid', $catId, \Joomla\Database\ParameterType::INTEGER); + } + + $search = $this->getState('filter.search'); + + if (!empty($search)) + { + $search = '%' . trim($search) . '%'; + $query->where( + '(' . $db->quoteName('a.title') . ' LIKE :search1' + . ' OR ' . $db->quoteName('a.city') . ' LIKE :search2' + . ' OR ' . $db->quoteName('a.address') . ' LIKE :search3' + . ' OR ' . $db->quoteName('a.postcode') . ' LIKE :search4)' + ) + ->bind(':search1', $search) + ->bind(':search2', $search) + ->bind(':search3', $search) + ->bind(':search4', $search); + } + + $orderCol = $this->state->get('list.ordering', 'a.ordering'); + $orderDir = $this->state->get('list.direction', 'ASC'); + $query->order($db->escape($orderCol) . ' ' . $db->escape($orderDir)); return $query; } diff --git a/src/packages/com_mokojoomstorelocator/admin/src/Table/LocationTable.php b/src/packages/com_mokojoomstorelocator/admin/src/Table/LocationTable.php index 10d81b2..25f2763 100644 --- a/src/packages/com_mokojoomstorelocator/admin/src/Table/LocationTable.php +++ b/src/packages/com_mokojoomstorelocator/admin/src/Table/LocationTable.php @@ -43,10 +43,49 @@ class LocationTable extends Table */ public function check(): bool { - // TODO: Validate title is not empty - // TODO: Auto-generate alias from title if empty - // TODO: Validate latitude/longitude ranges - // TODO: Set created/modified timestamps + if (empty($this->title)) + { + $this->setError('A location title is required.'); + + return false; + } + + if (empty($this->alias)) + { + $this->alias = $this->title; + } + + $this->alias = \Joomla\CMS\Filter\OutputFilter::stringURLSafe($this->alias); + + if (empty($this->alias)) + { + $this->alias = \Joomla\CMS\Factory::getDate()->format('Y-m-d-H-i-s'); + } + + if ($this->latitude !== null && ($this->latitude < -90 || $this->latitude > 90)) + { + $this->setError('Latitude must be between -90 and 90.'); + + return false; + } + + if ($this->longitude !== null && ($this->longitude < -180 || $this->longitude > 180)) + { + $this->setError('Longitude must be between -180 and 180.'); + + return false; + } + + $now = \Joomla\CMS\Factory::getDate()->toSql(); + + if (empty($this->created) || $this->created === '0000-00-00 00:00:00') + { + $this->created = $now; + $this->created_by = \Joomla\CMS\Factory::getApplication()->getIdentity()->id ?? 0; + } + + $this->modified = $now; + $this->modified_by = \Joomla\CMS\Factory::getApplication()->getIdentity()->id ?? 0; return parent::check(); } diff --git a/src/packages/com_mokojoomstorelocator/admin/tmpl/locations/default.php b/src/packages/com_mokojoomstorelocator/admin/tmpl/locations/default.php index ab535a8..bf98c9a 100644 --- a/src/packages/com_mokojoomstorelocator/admin/tmpl/locations/default.php +++ b/src/packages/com_mokojoomstorelocator/admin/tmpl/locations/default.php @@ -54,7 +54,31 @@ use Joomla\CMS\Router\Route; - + items as $i => $item) : ?> + + + id, false, 'cid', 'cb', $item->title); ?> + + + + escape($item->title); ?> + +
escape($item->alias); ?>
+ + + escape($item->city); ?> + + + escape($item->state); ?> + + + published, $i, 'locations.', true, 'cb'); ?> + + + id; ?> + + + diff --git a/src/packages/mod_mokojoomstorelocator_map/src/Dispatcher/Dispatcher.php b/src/packages/mod_mokojoomstorelocator_map/src/Dispatcher/Dispatcher.php index 910ef89..f19a3c5 100644 --- a/src/packages/mod_mokojoomstorelocator_map/src/Dispatcher/Dispatcher.php +++ b/src/packages/mod_mokojoomstorelocator_map/src/Dispatcher/Dispatcher.php @@ -34,9 +34,43 @@ class Dispatcher extends AbstractModuleDispatcher implements HelperFactoryAwareI { $data = parent::getLayoutData(); - // TODO: Load published locations with coordinates from the component table - // TODO: Build marker data array for the map JS - $data['locations'] = []; + $db = \Joomla\CMS\Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class); + $query = $db->getQuery(true); + + $query->select([ + $db->quoteName('id'), + $db->quoteName('title'), + $db->quoteName('address'), + $db->quoteName('city'), + $db->quoteName('state'), + $db->quoteName('postcode'), + $db->quoteName('phone'), + $db->quoteName('latitude'), + $db->quoteName('longitude'), + ]) + ->from($db->quoteName('#__mokojoomstorelocator_locations')) + ->where($db->quoteName('published') . ' = 1') + ->where($db->quoteName('latitude') . ' IS NOT NULL') + ->where($db->quoteName('longitude') . ' IS NOT NULL'); + + $db->setQuery($query); + $locations = $db->loadObjectList(); + + $markers = []; + + foreach ($locations as $loc) + { + $markers[] = [ + 'id' => (int) $loc->id, + 'title' => $loc->title, + 'address' => trim($loc->address . ', ' . $loc->city . ', ' . $loc->state . ' ' . $loc->postcode, ', '), + 'phone' => $loc->phone, + 'lat' => (float) $loc->latitude, + 'lng' => (float) $loc->longitude, + ]; + } + + $data['locations'] = $markers; return $data; } diff --git a/src/packages/mod_mokojoomstorelocator_search/src/Dispatcher/Dispatcher.php b/src/packages/mod_mokojoomstorelocator_search/src/Dispatcher/Dispatcher.php index 9c4a39e..6cf2095 100644 --- a/src/packages/mod_mokojoomstorelocator_search/src/Dispatcher/Dispatcher.php +++ b/src/packages/mod_mokojoomstorelocator_search/src/Dispatcher/Dispatcher.php @@ -34,8 +34,34 @@ class Dispatcher extends AbstractModuleDispatcher implements HelperFactoryAwareI { $data = parent::getLayoutData(); - // TODO: Load distinct cities/states for filter dropdowns - // TODO: Build radius options array from params + $db = \Joomla\CMS\Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class); + $table = $db->quoteName('#__mokojoomstorelocator_locations'); + + // Load distinct cities + $query = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('city')) + ->from($table) + ->where($db->quoteName('published') . ' = 1') + ->where($db->quoteName('city') . ' != ' . $db->quote('')) + ->order($db->quoteName('city') . ' ASC'); + $db->setQuery($query); + $data['cities'] = $db->loadColumn(); + + // Load distinct states + $query = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('state')) + ->from($table) + ->where($db->quoteName('published') . ' = 1') + ->where($db->quoteName('state') . ' != ' . $db->quote('')) + ->order($db->quoteName('state') . ' ASC'); + $db->setQuery($query); + $data['states'] = $db->loadColumn(); + + // Parse radius options from params + $params = $data['params']; + $radiusString = $params->get('radius_options', '5,10,25,50,100'); + $data['radius_options'] = array_map('intval', explode(',', $radiusString)); + $data['radius_unit'] = $params->get('radius_unit', 'miles'); return $data; } diff --git a/src/packages/mod_mokojoomstorelocator_search/tmpl/default.php b/src/packages/mod_mokojoomstorelocator_search/tmpl/default.php index 3a82d3b..ab9a0e4 100644 --- a/src/packages/mod_mokojoomstorelocator_search/tmpl/default.php +++ b/src/packages/mod_mokojoomstorelocator_search/tmpl/default.php @@ -29,9 +29,35 @@ $params = $displayData['params']; /> - - - + get('show_city_filter', 1) && !empty($displayData['cities'])) : ?> +
+ + +
+ + + get('show_radius_filter', 1) && !empty($displayData['radius_options'])) : ?> +
+ + +
+ + + + + + + +
+
+
+

+
+
+

+ + title,address,city,state,postcode,country,latitude,longitude,phone,email,website,hours,description + +
+ + + + +
+
+
+ + + + diff --git a/src/packages/com_mokojoomstorelocator/mokojoomstorelocator.xml b/src/packages/com_mokojoomstorelocator/mokojoomstorelocator.xml index d1396e4..b9cf1e8 100644 --- a/src/packages/com_mokojoomstorelocator/mokojoomstorelocator.xml +++ b/src/packages/com_mokojoomstorelocator/mokojoomstorelocator.xml @@ -57,6 +57,43 @@ COM_MOKOJOOMSTORELOCATOR COM_MOKOJOOMSTORELOCATOR_LOCATIONS + COM_MOKOJOOMSTORELOCATOR_IMPORT + + + +
+ + + + + + + + + + + +
+
+
diff --git a/src/packages/mod_mokojoomstorelocator_search/language/en-GB/mod_mokojoomstorelocator_search.ini b/src/packages/mod_mokojoomstorelocator_search/language/en-GB/mod_mokojoomstorelocator_search.ini index 7d0a8c3..5c551b1 100644 --- a/src/packages/mod_mokojoomstorelocator_search/language/en-GB/mod_mokojoomstorelocator_search.ini +++ b/src/packages/mod_mokojoomstorelocator_search/language/en-GB/mod_mokojoomstorelocator_search.ini @@ -11,3 +11,11 @@ MOD_MOKOJOOMSTORELOCATOR_SEARCH_SHOW_RADIUS="Show Radius Filter" MOD_MOKOJOOMSTORELOCATOR_SEARCH_RADIUS_UNIT="Distance Unit" MOD_MOKOJOOMSTORELOCATOR_SEARCH_RADIUS_OPTIONS="Radius Options" MOD_MOKOJOOMSTORELOCATOR_SEARCH_RADIUS_OPTIONS_DESC="Comma-separated list of radius values (e.g., 5,10,25,50,100)" + +MOD_MOKOJOOMSTORELOCATOR_SEARCH_USE_LOCATION="Use My Location" +MOD_MOKOJOOMSTORELOCATOR_SEARCH_LOCATING="Finding your location..." +MOD_MOKOJOOMSTORELOCATOR_SEARCH_LOCATION_FOUND="Location found! Select a radius and search." +MOD_MOKOJOOMSTORELOCATOR_SEARCH_GEO_DENIED="Location access denied. Please enable location services." +MOD_MOKOJOOMSTORELOCATOR_SEARCH_GEO_UNAVAILABLE="Location information unavailable." +MOD_MOKOJOOMSTORELOCATOR_SEARCH_GEO_TIMEOUT="Location request timed out." +MOD_MOKOJOOMSTORELOCATOR_SEARCH_GEO_ERROR="Unable to determine your location." diff --git a/src/packages/mod_mokojoomstorelocator_search/tmpl/default.php b/src/packages/mod_mokojoomstorelocator_search/tmpl/default.php index ab9a0e4..70e91d2 100644 --- a/src/packages/mod_mokojoomstorelocator_search/tmpl/default.php +++ b/src/packages/mod_mokojoomstorelocator_search/tmpl/default.php @@ -12,29 +12,31 @@ use Joomla\CMS\Language\Text; use Joomla\CMS\Router\Route; /** @var array $displayData */ -$params = $displayData['params']; +$params = $displayData['params']; +$moduleId = $displayData['module']->id; ?> -