From f4be611c51473359e6d41780f0313c446079e9d3 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 09 Jun 2026 15:19:24 +0000
Subject: [PATCH] =Switched the /base/options.php to the same pattern as Site.php: a class based approached rather than a filter. Updated Meta.php to play along with the defined fields from there in Meta::forOptions. Had to change openingHoursSpecificationsTrait.php to not use the translater functions __('text','textdomain') for now, as we load before init.

---
 checks.php                                                                    |   32 +-
 base/_setup.php                                                               |    2 
 inc/managers/SEO/render/Traits/_Properties/openingHoursSpecificationTrait.php |   12 
 assets/js/concise/FrontendFavourites.js                                       |   72 ++++-
 inc/meta/Meta.php                                                             |    7 
 base/Site.php                                                                 |    4 
 jvb.php                                                                       |    5 
 base/options.php                                                              |  138 +++++++++++
 inc/rest/routes/FavouritesRoutes.php                                          |  244 --------------------
 inc/registrar/config/Integration.php                                          |   11 
 inc/helpers/all.php                                                           |    3 
 inc/meta/Form.php                                                             |    9 
 inc/registrar/Fields.php                                                      |    2 
 inc/managers/FavouritesManager.php                                            |  110 +++++++++
 inc/registrar/Registrar.php                                                   |   12 
 15 files changed, 368 insertions(+), 295 deletions(-)

diff --git a/assets/js/concise/FrontendFavourites.js b/assets/js/concise/FrontendFavourites.js
index 47befbe..f986ff1 100644
--- a/assets/js/concise/FrontendFavourites.js
+++ b/assets/js/concise/FrontendFavourites.js
@@ -26,6 +26,11 @@
 				}
 			});
 
+		this.queue = {
+			add: new Map(),
+			remove: new Map(),
+		};
+
 		this.store = store.favourites;
 
 		// this.listStore = window.jvbStore.register(
@@ -71,39 +76,66 @@
 		// Update button icon
 		button.innerHTML = jvbSettings.icons[button.classList.contains('favourited') ? 'heart-filled' : 'heart'];
 
-		window.debouncer.schedule(
-			`favourite-${button.dataset.id}`,
-			this.postFavourite({
-				user: window.auth.getUser(),
-				target_id: button.dataset.id,
-				action: action,
-				type: button.dataset.type
-			}),
-			100
-		);
+		this.checkQueue(action, {
+			target_id: button.dataset.id,
+			action: action,
+			type: button.dataset.type
+		});
+		window.debouncer.schedule('favourites', this.postFavourites.bind(this), 200);
+
 
 	}
 
-	async postFavourite(args) {
-		await window.auth.fetch(
+	checkQueue(action, item) {
+		switch (action) {
+			case 'add':
+				if (this.queue.remove.has(item.target_id)) {
+					this.queue.remove.delete(item.target_id);
+				}
+				this.queue.add.set(item.target_id, item);
+				break;
+			case 'remove':
+				if (this.queue.add.has(item.target_id)) {
+					this.queue.add.delete(item.target_id);
+				}
+				this.queue.remove.set(item.target_id, item);
+				break;
+			default:
+				return;
+		}
+	}
+	async postFavourites() {
+		console.log(this.queue,'Posting favourites');
+		const response = await window.auth.fetch(
 			`${jvbSettings.api}favourites`,
 			{
 				method: 'POST',
 				headers: {
 					'X-Action-Nonce': window.auth.getNonce('favourites')
 				},
-				body: args
+				body: {
+					user: window.auth.getUser(),
+					... this.queue
+				}
 			}
 		);
 
-		if (args.action === 'add') {
-			await this.store.setItem(button.dataset.id, {
-				target_id: button.dataset.id,
-				action: action,
-				type: button.dataset.type,
-			}).then(()=>{});
+		if (response.ok) {
+			console.log('Posted favourites - clearing queue');
+			//Add or remove from store
+			for (let item of this.queue.add.entries()) {
+				await this.store.setItem(item.target_id,
+					item);
+			}
+			for (let item of this.queue.remove.entries()) {
+				await this.store.delete(item.target_id);
+			}
+			//Clear the favourite queue
+			this.queue.add.clear();
+			this.queue.remove.clear();
 		} else {
-			await this.store.delete(button.dataset.id).then(()=>{});
+			console.log(await response.json(), 'Something went wrong');
+			window.debouncer.schedule('favourites', this.postFavourites.bind(this), 200);
 		}
 	}
 
diff --git a/base/Site.php b/base/Site.php
index 43d194a..2276b00 100644
--- a/base/Site.php
+++ b/base/Site.php
@@ -62,6 +62,10 @@
 	 */
 	protected static bool $limit_hours = false;
 	/**
+	 * @var bool $has_hours Whether this site has business hours
+	 */
+	protected static bool $has_hours = false;
+	/**
 	 * @var bool $enthusiast Whether to scaffold enthusiasts (users that can interact with and save favourites)
 	 */
 	protected static bool $enthusiast = false;
diff --git a/base/_setup.php b/base/_setup.php
index 077e84f..544b7e0 100644
--- a/base/_setup.php
+++ b/base/_setup.php
@@ -16,7 +16,7 @@
 //require(JVB_DIR.'/base/users.php');
 require(JVB_DIR.'/base/Login.php');
 require(JVB_DIR.'/base/Membership.php');
-require(JVB_DIR.'/base/options.php');
+require(JVB_DIR.'/base/Options.php');
 require(JVB_DIR.'/base/SchemaHelper.php');
 
 $base = apply_filters('jvb_base', 'jvb_');
diff --git a/base/options.php b/base/options.php
index 44a43c5..50f0979 100644
--- a/base/options.php
+++ b/base/options.php
@@ -1,7 +1,145 @@
 <?php
+namespace JVBase\base;
+use JVBase\managers\Cache;
+use JVBase\meta\Form;
+use JVBase\meta\Meta;
+use JVBase\registrar\Fields;
+
 /**
  * Custom options for the site can be set here. They will need to be handled by the child plugin, but you can make use of the Meta.php this way
  */
 
 $options = apply_filters('jvb_options', []);
 define('JVB_OPTIONS', $options);
+
+
+class Options {
+	protected static Options $instance;
+	protected array $resetFields = [];
+	protected string $resetInterval = 'daily';
+	protected Fields $fields;
+	protected array $sections = [];
+	protected static array $values = [];
+
+	private function __construct(){
+		$this->setFields();
+		if (!empty($this->resetFields)) {
+			add_action('jvbDailyReset', [$this, 'resetOptions']);
+		}
+	}
+
+	public static function getInstance():Options {
+		if (!isset(self::$instance)) {
+			self::$instance = new self();
+			do_action('jvb_define_options');
+		}
+		return self::$instance;
+	}
+
+	public function setFields():void
+	{
+		$this->fields = new Fields('options');
+		if (Site::has('limit_hours')) {
+			$this->fields->addField(
+				'today_hours',
+				[
+					'type'	=> 'group',
+					'label'	=> 'Today\'s Hours',
+					'fields'	=> [
+						'time_start'	=> [
+							'type'	=> 'time',
+							'label'	=> 'Open',
+						],
+						'time_end'	=> [
+							'type'	=> 'time',
+							'label'	=> 'Closed',
+						]
+					]
+				]
+			);
+			$this->fields->addField(
+				'open_to_public',
+				[
+					'type'	=> 'true_false',
+					'label'	=> 'Open to Public?'
+				]
+			);
+
+			if (Site::hasAnyIntegration(['facebook','instagram'])) {
+				$this->fields->addField(
+					'post_to_social',
+					[
+						'type'	=> 'true_false',
+						'label'	=> 'Post to Socials?',
+					]
+				);
+			}
+		}
+
+		if (Site::hasIntegration('gmb') || Site::has('hours')) {
+			$this->fields->addCommon('hours');
+		}
+	}
+	public function fields():Fields
+	{
+		return $this->fields;
+	}
+
+	public function resetOptions():void
+	{
+		if (empty($this->resetFields)) {
+			return;
+		}
+		foreach ($this->resetFields as $field) {
+			self::delete($field);
+		}
+		Cache::for('options')->flush();
+	}
+
+	public static function get(string $fieldName):mixed
+	{
+		if (!array_key_exists($fieldName, self::$values)) {
+			$meta = Meta::forOptions();
+			self::$values[$fieldName] = $meta->get($fieldName);
+		}
+		return self::$values[$fieldName];
+	}
+	public static function delete(string $fieldName):void
+	{
+		if (array_key_exists($fieldName, self::$values)) {
+			unset(self::$values[$fieldName]);
+		}
+		$meta = Meta::forOptions();
+		$meta->delete($fieldName);
+	}
+	public static function set(string $fieldName, mixed $value):void
+	{
+		$meta = Meta::forOptions();
+		$meta->set($fieldName, $value);
+		self::$values = $value;
+	}
+
+	public static function render(array $fieldNames, array $options = [], bool $output = false):string
+	{
+		$meta = Meta::forOptions();
+		$result = Form::renderFormFrom(
+			$meta,
+			'options',
+			$options,
+			$fieldNames
+		);
+		if ($output) {
+			echo $result;
+		}
+		return $result;
+	}
+
+	public function getFields():array
+	{
+		return array_map(function ($field) {
+			return $field->getConfig();
+		},
+			$this->fields->getFields()
+		);
+	}
+}
diff --git a/checks.php b/checks.php
index d968100..79cc829 100644
--- a/checks.php
+++ b/checks.php
@@ -1,5 +1,6 @@
 <?php
 
+use JVBase\base\Options;
 use JVBase\base\Site;
 use JVBase\managers\Cache;
 use JVBase\registrar\Registrar;
@@ -18,22 +19,21 @@
 }
 
 
-//function jvbIsOpen():bool
-//{
-//
-//	if (!jvbCheck('limit_hours', JVB_SITE)) {
-//		return true;
-//	}
-//	if (get_option(BASE.'open_to_public') !== '1') {
-//		return false;
-//	}
-//	//Check if today_hours is set
-//	if (get_option(BASE.'today_hours')) {
-//		return jvbIsTimeBetween();
-//	}
-//	//Default to the stored settings
-//	return jvbIsCurrentlyOpen();
-//}
+
+function jvbIsOpen():bool
+{
+	if (!Site::has('limit_hours')) {
+		return true;
+	}
+	if (Options::get('open_to_public') !== 1) {
+		error_log('Not open to public');
+		return false;
+	}
+	if (Options::get('today_hours')) {
+		return jvbIsTimeBetween();
+	}
+	return jvbIsCurrentlyOpen();
+}
 
 
 function jvbTermHasPosts(int $termID, string $taxonomy):bool
diff --git a/inc/helpers/all.php b/inc/helpers/all.php
index 0891fab..274e00d 100644
--- a/inc/helpers/all.php
+++ b/inc/helpers/all.php
@@ -1,5 +1,7 @@
 <?php
 
+use JVBase\base\Site;
+
 if (!defined('ABSPATH')) {
 	exit;
 }
@@ -86,3 +88,4 @@
 		'post_type'		=> $type
 	]);
 }
+
diff --git a/inc/managers/FavouritesManager.php b/inc/managers/FavouritesManager.php
index 125c75a..c2f2c05 100644
--- a/inc/managers/FavouritesManager.php
+++ b/inc/managers/FavouritesManager.php
@@ -1,6 +1,7 @@
 <?php
 namespace JVBase\managers;
 
+use Exception;
 use JVBase\registrar\Registrar;
 
 if (!defined('ABSPATH')) {
@@ -18,8 +19,16 @@
 	public function __construct()
 	{
 		$this->defineTables();
+		$this->registerHooks();
 	}
 
+	protected function registerHooks():void
+	{
+		add_action('before_delete_post', [$this, 'cleanupPostFavourites']);
+		add_action('delete_term', [$this, 'cleanupTermFavourites'], 10, 3);
+		add_action('jvbUserRegistered', [$this, 'maybeAcceptListInvite'], 10, 3);
+		add_action('jvb_cleanupOrphanedFavourites', [$this, 'cleanupOrphanedFavourites']);
+	}
 	protected function defineTables():void
 	{
 		$this->defineFavouriteTable();
@@ -126,6 +135,7 @@
 				'list_id'		=> 'bigint(20) unsigned NOT NULL',
 				'user_id'		=> "{$table->getUserIDType()} NOT NULL",
 				'email'			=> 'varchar(255) NOT NULL',
+				'invite_token'	=> 'varchar(255) NOT NULL',
 				'permission'	=> "ENUM('view', 'edit') NOT NULL DEFAULT 'view'",
 				'status'		=> "ENUM('pending', 'accepted', 'rejected', 'revoked') NOT NULL DEFAULT 'pending'",
 				'created_at'	=> 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP',
@@ -372,6 +382,7 @@
 			} elseif ($newUser) {
 				$email = sanitize_email($newUser['email']);
 				$name = sanitize_text_field($newUser['name']);
+				$args['invite_token'] = wp_generate_password(32, false);
 				$args['email'] = $email;
 				//TODO: Magic Link setup
 			}
@@ -570,6 +581,59 @@
 					'shared_users'	=> $sharedWith
 				];
 			}
+
+	public function getFavouriteCounts(int $user_id): array
+	{
+		$results = $this->favourites->queryResults(
+			"SELECT type, COUNT(*) as count FROM {table} WHERE user_id = %d GROUP BY type",
+			[$user_id]
+		);
+
+		$counts = [];
+		foreach ($results as $row) {
+			$type = str_replace(BASE, '', $row->type);
+			$counts[$type] = (int) $row->count;
+		}
+		$defaults = array_fill_keys(
+			array_map(fn($t) => str_replace(BASE, '', $t), Registrar::withFeature('favouritable')),
+			0
+		);
+		return array_merge($defaults, $counts);
+	}
+
+	public function cleanupOrphanedFavourites(): bool
+	{
+		global $wpdb;
+
+		// Posts - no FK possible since target_id is generic
+		$this->favourites->query(
+			"DELETE f FROM {table} f
+         LEFT JOIN {$wpdb->posts} p ON f.target_id = p.ID
+         WHERE f.type IN (
+             SELECT CONCAT('" . BASE . "', post_type)
+             FROM {$wpdb->posts} GROUP BY post_type
+         )
+         AND p.ID IS NULL"
+		);
+
+		// Terms - same reason
+		$this->favourites->query(
+			"DELETE f FROM {table} f
+         LEFT JOIN {$wpdb->term_taxonomy} tt ON f.target_id = tt.term_id
+         AND f.type = CONCAT('" . BASE . "', tt.taxonomy)
+         WHERE tt.term_id IS NULL
+         AND f.type != CONCAT('" . BASE . "', 'user')"
+		);
+
+		$this->listItems->query(
+			"DELETE li FROM {table} li
+         LEFT JOIN {$wpdb->posts} p ON li.target_id = p.ID
+         LEFT JOIN {$wpdb->term_taxonomy} tt ON li.target_id = tt.term_id
+         WHERE p.ID IS NULL AND tt.term_id IS NULL"
+		);
+
+		return true;
+	}
 	/***************************************************************
 	 * UTILITY METHODS
 	 **************************************************************/
@@ -682,4 +746,50 @@
 		unset($notes[$index]);
 		return array_values($notes); //reindexed array
 	}
+
+	public function cleanupPostFavourites(int $post_id):void
+	{
+		try {
+			$type = get_post_type($post_id);
+			if (!$type) return;
+
+			$this->favourites->where([
+				'type'	=> $type,
+				'target_id'	=> $post_id
+			])->deleteResults();
+
+			$this->listItems->where([
+				'item_type'	=> $type,
+				'item_id'	=> $post_id
+			])->deleteResults();
+		} catch (Exception $e) {
+			JVB()->error()->log('cleanupPostFavourites', $e->getMessage(), [
+				'post_id' => $post_id
+			]);
+		}
+	}
+
+	public function cleanupTermFavourites(int $term_id, int $tt_id, string $taxonomy):void
+	{
+		try {
+			$registrar = Registrar::getInstance($taxonomy);
+			if (!$registrar || !$registrar->hasFeature('favouritable')) {
+				return;
+			}
+			$this->favourites->where([
+				'type'	=> $taxonomy,
+				'target_id'	=> $term_id,
+			])->deleteResults();
+
+			$this->listItems->where([
+				'item_type'	=> $taxonomy,
+				'item_id'	=> $term_id
+			])->deleteResults();
+		} catch (Exception $e) {
+			JVB()->error()->log('cleanupTermFavourites', $e->getMessage(), [
+				'term_id' => $term_id,
+				'taxonomy'=> $taxonomy
+			]);
+		}
+	}
 }
diff --git a/inc/managers/SEO/render/Traits/_Properties/openingHoursSpecificationTrait.php b/inc/managers/SEO/render/Traits/_Properties/openingHoursSpecificationTrait.php
index 594ae51..b21a54b 100644
--- a/inc/managers/SEO/render/Traits/_Properties/openingHoursSpecificationTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/openingHoursSpecificationTrait.php
@@ -47,11 +47,11 @@
 	{
 		$fields->addField('openingHours', [
 			'type'	=> 'repeater',
-			'label'	=> __('Opening Hours', 'jvb'),
+			'label'	=> 'Opening Hours',
 			'fields'	=> [
 				'dayOfWeek'	=> [
 					'type'		=> 'set',
-					'label'		=> __('Day(s) of Week', 'jvb'),
+					'label'		=> 'Day(s) of Week',
 					'options'	=> [
 						'Mo'	=> 'Monday',
 						'Tu'	=> 'Tuesday',
@@ -65,23 +65,23 @@
 				],
 				'opens'	=> [
 					'type'	=> 'time',
-					'label'	=> __('Opens at', 'jvb'),
+					'label'	=> 'Opens at',
 					'required'	=> true
 				],
 				'closes'	=> [
 					'type'	=> 'time',
-					'label'	=> __('Closes at', 'jvb'),
+					'label'	=> 'Closes at',
 					'required'	=> true
 				]
 			]
 		]);
 		$fields->addField('by_appointment', [
 			'type'	=> 'true_false',
-			'label'	=> __('By Appointment Only', 'jvb'),
+			'label'	=> 'By Appointment Only',
 		]);
 		$fields->addField('allow_walkins', [
 			'type'	=> 'true_false',
-			'label'	=> __('Walk Ins Welcome', 'jvb')
+			'label'	=> 'Walk Ins Welcome'
 		]);
 	}
 	public function formatOpeningHoursSpecificationField(Meta $meta):void
diff --git a/inc/meta/Form.php b/inc/meta/Form.php
index 8b7b414..868abf9 100644
--- a/inc/meta/Form.php
+++ b/inc/meta/Form.php
@@ -53,7 +53,7 @@
 	/**
 	 * Render complete form from Meta instance
 	 */
-	public static function renderFormFrom(Meta $meta, string $endpoint, array $options = []): string
+	public static function renderFormFrom(Meta $meta, string $endpoint, array $options = [], array $fields = []): string
 	{
 		$id = $options['form-id'] ?? $endpoint;
 		$classes = isset($options['classes']) ? ' class="' . implode(' ', $options['classes']) . '"' : '';
@@ -70,9 +70,10 @@
 				$output .= '<p>' . esc_html($d) . '</p>';
 			}
 		}
-
-		foreach ($meta->configs() as $name => $config) {
-			$output .= static::render($name, $meta->get($name), $config);
+		$allFields = $meta->getAll($fields);
+		foreach ($allFields as $name => $value) {
+			$config = $meta->config($name);
+			$output .= static::render($name, $value, $config);
 		}
 
 		if (!empty($options['submit'])) {
diff --git a/inc/meta/Meta.php b/inc/meta/Meta.php
index 35a8c3d..d870467 100644
--- a/inc/meta/Meta.php
+++ b/inc/meta/Meta.php
@@ -1,6 +1,7 @@
 <?php
 namespace JVBase\meta;
 
+use JVBase\base\Options;
 use JVBase\registrar\Registrar;
 use WP_Post;
 use WP_Term;
@@ -75,7 +76,7 @@
 	/**
 	 * Create Meta instance for options
 	 */
-	public static function forOptions(?string $baseKey = 'ajv'): self
+	public static function forOptions(?string $baseKey = BASE): self
 	{
 		if (array_key_exists($baseKey, self::$instances['options'])) {
 			return self::$instances['options'][$baseKey];
@@ -118,6 +119,10 @@
 
 		$registrar = !is_null($this->slug) ? Registrar::getInstance($this->slug) : false;
 		$fields = $registrar ? $registrar->getFields() : [];
+		if ($this->type == 'options') {
+			$options = Options::getInstance();
+			$fields = $options->getFields();
+		}
 		$meta = match($type) {
 			'post'	=> get_post_meta($id),
 			'term'	=> get_term_meta($id),
diff --git a/inc/registrar/Fields.php b/inc/registrar/Fields.php
index 8e37662..79d07bd 100644
--- a/inc/registrar/Fields.php
+++ b/inc/registrar/Fields.php
@@ -17,7 +17,7 @@
 
 class Fields {
 	protected array $fields;
-	private Registrar $registrar;
+	private ?Registrar $registrar = null;
 
 	public function __construct(?string $type = null, ?Registrar $registrar = null) {
 		$this->registrar = $registrar;
diff --git a/inc/registrar/Registrar.php b/inc/registrar/Registrar.php
index 8b4b57f..1ecadf3 100644
--- a/inc/registrar/Registrar.php
+++ b/inc/registrar/Registrar.php
@@ -14,6 +14,7 @@
 use JVBase\registrar\config\Directory;
 use JVBase\registrar\config\Feed;
 use JVBase\registrar\config\Integration;
+use JVBase\registrar\config\Register;
 use JVBase\registrar\config\Section;
 use JVBase\registrar\config\SEO;
 use JVBase\registrar\helpers\AddIntegrationFields;
@@ -212,6 +213,7 @@
 	protected Dashboard $dashboard;
 	protected Directory|false $directory;
 	protected Feed|false $feed;
+	protected Register|false $login;
 //	protected Management $management;
 //	protected Responses $responses;
 	protected ?SEO $seo = null;
@@ -653,7 +655,7 @@
 
 	public function config(string $config):mixed
 	{
-		$allowed = ['breadcrumbs','calendar','dashboard','directory','feed','management','has_responses','seo','trackchanges','verification'];
+		$allowed = ['breadcrumbs','calendar','register','login','dashboard','directory','feed','management','has_responses','seo','trackchanges','verification'];
 		if (!in_array(strtolower($config), $allowed)) {
 			error_log('Invalid config requested from Registrar: '.$config);
 			return [];
@@ -662,6 +664,7 @@
 			'breadcrumbs' 	=> $this->getBreadcrumbs(),
 			'dashboard'		=> $this->getDashboard(),
 			'directory'		=> $this->getDirectory(),
+            'register','login' => $this->getLogin(),
 			'feed'			=> $this->getFeed(),
 			'management'	=> $this->getManagement(),
 			'has_responses'	=> $this->getResponses(),
@@ -670,6 +673,13 @@
 			'verification'	=> $this->getVerification()
 		};
 	}
+        protected function getLogin():Register|false
+        {
+            if (!isset($this->login)) {
+                $this->login = new Register();
+            }
+            return $this->login;
+        }
 		protected function getBreadcrumbs():Breadcrumbs
 		{
 			if (!isset($this->breadcrumbs)) {
diff --git a/inc/registrar/config/Integration.php b/inc/registrar/config/Integration.php
index 8a829d5..3b9a6a3 100644
--- a/inc/registrar/config/Integration.php
+++ b/inc/registrar/config/Integration.php
@@ -13,7 +13,7 @@
 	 * @var string Must match as defined in the JVBase\integrations namespace
 	 */
 	protected string $service_name;
-	protected string $content_type;
+	protected ?string $content_type = null;
 	/**
 	 * @var bool Whether to send to the integration on publish
 	 */
@@ -45,11 +45,14 @@
 	}
 
 	/**
-	 * @param string $content must match what integration expects
+	 * @param ?string $content must match what integration expects
 	 * @return self
 	 */
-	public function setContentType(string $content):self
+	public function setContentType(?string $content):self
 	{
+		if (is_null($content)) {
+			return $this;
+		}
 		$connection = JVB()->connect($this->service_name);
 		if (!$connection){
 			error_log('[Integration]::setContentType Service is not setup. '.$this->service_name);
@@ -64,7 +67,7 @@
 		return $this;
 	}
 
-	public function getContentType():string
+	public function getContentType():?string
 	{
 		return $this->content_type;
 	}
diff --git a/inc/rest/routes/FavouritesRoutes.php b/inc/rest/routes/FavouritesRoutes.php
index 9f38917..953e4db 100644
--- a/inc/rest/routes/FavouritesRoutes.php
+++ b/inc/rest/routes/FavouritesRoutes.php
@@ -46,13 +46,6 @@
 		$this->lists = CustomTable::for('favourites_lists');
 		$this->listItems = CustomTable::for('favourites_list_items');
 		$this->listShares = CustomTable::for('favourites_list_shares');
-
-		// Register hooks
-		add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
-		add_action('before_delete_post', [$this, 'cleanupPostFavourites']);
-		add_action('delete_term', [$this, 'cleanupTermFavourites'], 10, 3);
-		add_action('jvbUserRegistered', [$this, 'maybeAcceptListInvite'], 10, 3);
-		add_action('jvb_cleanupOrphanedFavourites', [$this, 'cleanupOrphanedFavourites']);
 	}
 
 	public function registerRoutes(): void
@@ -215,44 +208,14 @@
 	 */
 	public function getFavouriteCounts(WP_REST_Request $request): WP_REST_Response
 	{
+
 		$user_id = absint($request->get_param('user'));
 
 		if (!$this->userCheck($user_id)) {
 			return $this->unauthorized();
 		}
 
-		$key = "counts_{$user_id}";
-
-		$counts = $this->cache->remember($key, function() use ($user_id) {
-			try {
-				// Get counts grouped by type using raw query
-				$results = $this->favourites->queryResults(
-					"SELECT type, COUNT(*) as count FROM {table} WHERE user_id = %d GROUP BY type",
-					[$user_id],
-					OBJECT_K
-				);
-
-				$all_counts = array_fill_keys(
-					array_map(fn($type) => str_replace(BASE, '', $type), array_keys($this->valid_types)),
-					0
-				);
-
-				foreach ($results as $type => $data) {
-					$type_key = str_replace(BASE, '', $type);
-					$all_counts[$type_key] = (int)$data->count;
-				}
-
-				return $all_counts;
-
-			} catch (Exception $e) {
-				$this->logError('getFavouriteCounts', [
-					'error' => $e->getMessage(),
-					'user_id' => $user_id
-				]);
-
-				return array_fill_keys(array_keys($this->valid_types), 0);
-			}
-		});
+		$counts = JVB()->favourites()->getFavouriteCounts($user_id);
 
 		return Response::success(['counts' => $counts]);
 	}
@@ -560,105 +523,6 @@
 	}
 
 	/**
-	 * Clean up favourites when a post is deleted
-	 */
-	public function cleanupPostFavourites(int $post_id): void
-	{
-		try {
-			$type = get_post_type($post_id);
-			if (!$type) return;
-
-			$type = BASE . $type;
-
-			// Delete using fluent interface
-			$this->favourites->where([
-				'type' => $type,
-				'target_id' => $post_id
-			])->deleteResults();
-
-			$this->listItems->where([
-				'item_type' => $type,
-				'item_id' => $post_id
-			])->deleteResults();
-
-		} catch (Exception $e) {
-			$this->logError('cleanupPostFavourites', [
-				'error' => $e->getMessage(),
-				'post_id' => $post_id
-			]);
-		}
-	}
-
-	/**
-	 * Clean up favourites when a term is deleted
-	 */
-	public function cleanupTermFavourites(int $term_id, int $tt_id, string $taxonomy): void
-	{
-		try {
-			if (!isset($this->valid_types[$taxonomy])) {
-				return;
-			}
-
-			// Delete using fluent interface
-			$this->favourites->where([
-				'type' => $taxonomy,
-				'target_id' => $term_id
-			])->deleteResults();
-
-			$this->listItems->where([
-				'item_type' => $taxonomy,
-				'item_id' => $term_id
-			])->deleteResults();
-
-		} catch (Exception $e) {
-			$this->logError('cleanupTermFavourites', [
-				'error' => $e->getMessage(),
-				'term_id' => $term_id,
-				'taxonomy' => $taxonomy
-			]);
-		}
-	}
-
-	/**
-	 * Cleanup orphaned favourites using CustomTable query method
-	 */
-	public function cleanupOrphanedFavourites(): bool
-	{
-		try {
-			// Delete favourites for non-existent users
-			$this->favourites->query(
-				"DELETE f FROM {table} f
-                 LEFT JOIN {$GLOBALS['wpdb']->users} u ON f.user_id = u.ID
-                 WHERE u.ID IS NULL"
-			);
-
-			// Delete favourites for non-existent posts
-			$post_types = array_filter(
-				array_keys($this->valid_types),
-				fn($type) => $this->valid_types[$type]['table'] === 'post'
-			);
-
-			foreach ($post_types as $type) {
-				$this->favourites->query(
-					"DELETE f FROM {table} f
-                     LEFT JOIN {$GLOBALS['wpdb']->posts} p ON f.target_id = p.ID
-                     WHERE f.type = %s AND p.ID IS NULL",
-					[$type]
-				);
-			}
-
-			return true;
-
-		} catch (Exception $e) {
-			$this->logError('cleanupOrphanedFavourites', [
-				'error' => $e->getMessage()
-			]);
-
-			return false;
-		}
-	}
-
-	/**
 	 * Helper methods
 	 */
 	protected function buildParams(WP_REST_Request $request): array
@@ -748,112 +612,10 @@
 		}
 	}
 
-	/**
-	 * Notify content owner of new favourite if configured
-	 *
-	 * @param string $type Content type
-	 * @param int $target_id Content ID
-	 * @param int $user_id User who favourited
-	 * @return void
-	 */
-	protected function maybeNotifyOwner(string $type, int $target_id, int $user_id): void
-	{
-		try {
-			$owner_id = $this->getContentOwner($type, $target_id);
-
-			if ($owner_id && $owner_id !== $user_id) {
-				JVB()->notification()->addNotification(
-					$owner_id,
-					'new_favourite',
-					[
-						'user_id' => $user_id,
-						'type' => $type,
-						'target_id' => $target_id
-					]
-				);
-			}
-		} catch (Exception $e) {
-			// Silent fail - notifications are non-critical
-		}
-	}
-
-	/**
-	 * Remove any existing notifications about a favorite action
-	 *
-	 * @param int $user_id User who removed the favorite
-	 * @param string $type Content type
-	 * @param int $target_id Content ID
-	 * @return void
-	 */
-	protected function removeRelatedNotifications(int $user_id, string $type, int $target_id):void
-	{
-		try {
-			// Get the content owner(s)
-			$owner_ids = $this->getContentOwner($type, $target_id);
-			if (!$owner_ids) {
-				return;
-			}
-
-			$owner_ids = (is_array($owner_ids)) ? $owner_ids : [$owner_ids];
-
-			foreach ($owner_ids as $owner_id) {
-				// Skip if owner is the same as the user who unfavorited
-				if ($owner_id === $user_id) {
-					continue;
-				}
-
-				global $wpdb;
-				$notifications_table = $wpdb->prefix . BASE . 'notifications';
-
-				// Find recent (within last 30 days) new_favourite notifications from this user for this content
-				$notifications = $wpdb->get_results($wpdb->prepare(
-					"SELECT id FROM {$notifications_table}
-                WHERE owner_id = %d
-                AND action_user_id = %d
-                AND type = 'new_favourite'
-                AND target_id = %d
-                AND target_type = %s
-                AND created_at > DATE_SUB(%s, INTERVAL 30 DAY)",
-					$owner_id,
-					$user_id,
-					$target_id,
-					$type,
-					current_time('mysql')
-				));
-
-				if (empty($notifications)) {
-					continue;
-				}
-
-				// Delete the notifications
-				foreach ($notifications as $notification) {
-					$wpdb->delete(
-						$notifications_table,
-						['id' => $notification->id],
-						['%d']
-					);
-				}
-
-				// Invalidate notification cache for this user
-//                if (method_exists(JVB()->notification(), 'clearNotificationCache')) {
-//                    JVB()->notification()->clearNotificationCache($owner_id);
-//                }
-			}
-		} catch (Exception $e) {
-			// Log but continue
-			JVB()->error()->log(
-				'favourites',
-				'Error removing related notifications: ' . $e->getMessage(),
-				['type' => $type, 'target_id' => $target_id, 'user_id' => $user_id],
-				'warning'
-			);
-		}
-	}
-
 	public function maybeAcceptListInvite(int $user_id, string $email, array $data):void
 	{
 		if (array_key_exists('list_token', $data) && !empty($data['list_token'])) {
-			$this->acceptListInvitation($data['list_token'], $email);
+			JVB()->favourites()->acceptListShare($data['list_token'], $user_id);
 		}
 	}
 
diff --git a/jvb.php b/jvb.php
index 16816ed..6cc5ff8 100644
--- a/jvb.php
+++ b/jvb.php
@@ -259,6 +259,7 @@
 add_action('plugins_loaded', 'jvb_site_definitions',1);
 add_action('plugins_loaded', 'jvb_registrar_definitions',2);
 add_action('plugins_loaded', 'jvb_field_definitions', 3);
+add_action('plugins_loaded', 'jvb_options_definitions',3);
 add_action('init', 'jvbLoadBase', 1);
 add_action('init', 'jvb_integration_definitions',3);
 add_action('init', 'jvb_field_section_definitions', 5);
@@ -279,6 +280,10 @@
 	do_action('jvb_define_fields');
 }
 
+function jvb_options_definitions():void
+{
+    do_action('jvb_define_options');
+}
 
 function jvbLoadBase():void
 {

--
Gitblit v1.10.0