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.

---
 inc/managers/FavouritesManager.php |  223 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 221 insertions(+), 2 deletions(-)

diff --git a/inc/managers/FavouritesManager.php b/inc/managers/FavouritesManager.php
index a2cfb88..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();
@@ -29,7 +38,7 @@
 	}
 		private function defineFavouriteTable():void
 		{
-			$table = CustomTable::for('favourites');
+			$table = CustomTable::for('favourites', true);
 
 			$table->setColumns([
 				'id'		=> 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
@@ -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',
@@ -135,7 +145,7 @@
 			$table->setKeys([
 				['key' => 'PRIMARY', 'value' => '(`id`)'],
 				['key'	=> 'UNIQUE', 'value' => '`unique_share_user` (`list_id`, `user_id`)'],
-				['key'=> 'UNIQUE', 'value' => '`unique_share_email` (`list_id`, `email`'],
+				['key'=> 'UNIQUE', 'value' => '`unique_share_email` (`list_id`, `email`)'],
 				'`list_shares` (`list_id`)',
 				'`list_user` (`list_id`, `user_id`)',
 				'`status_index` (`status`)'
@@ -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
 			}
@@ -461,6 +472,168 @@
 				'status'	=> $accept ? 'accepted' : 'rejected',
 			], $args);
 		}
+
+		public function getFavourites(array $args = []):array
+		{
+			return $this->favourites->getMany($args, false);
+		}
+		public function getLists(array $args = []):array
+		{
+			return $this->lists->getMany($args, false);
+		}
+			public function getAvailableLists(array $args = [], bool $include_shared = true):array
+			{
+				$lists = [];
+				$owned = $this->lists->getMany($args, false);
+				foreach ($owned['items'] as &$list) {
+					$list['item_count'] = $this->listItems->count(['list_id' => $list['id']]);
+				}
+
+				$lists['owned'] = $owned;
+
+				if ($include_shared) {
+					$args['where']['status'] = 'accepted';
+					$sharedLists = $this->listShares->getMany($args);
+					$shared = [];
+
+					foreach ($sharedLists as $share) {
+						$sharedList = $this->lists->get(['id' => $share->list_id]);
+						if ($sharedList) {
+							$sharedList['owner_name'] = jvbGetUsername($sharedList['user_id']);
+							$sharedList['item_count'] = $this->listItems->count(['list_id' => $sharedList['id']]);
+							$sharedList['permission_type'] = $sharedList->permission_type;
+							$shared[] = $sharedList;
+						}
+					}
+					$lists['shared'] = $shared;
+				}
+				return $lists;
+			}
+
+			public function userOwnsList(int $list_id, int $user_id):bool
+			{
+				return $this->lists->count(['id' => $list_id, 'user_id' => $user_id]) > 0;
+			}
+
+			public function getListDetails(int $list_id, int $user_id, int $page = 1):array
+			{
+				$list = JVB()->favourites()->getLists(['id' => $list_id, 'user_id' => $user_id]);
+				if (!$list) {
+					return [];
+				}
+				$items = $this->listItems->getMany([
+					'where' 	=> ['list_id' => $list_id],
+					'order_by'	=> 'added_at',
+					'order'		=> 'DESC',
+					'per_page'	=> 25,
+					'page'		=> $page,
+				]);
+
+				$formatted = [];
+
+				foreach($items as $item) {
+					//See if we can get the actual favourite record first
+					if ($item->favourite_id) {
+						$fav = $this->favourites->get(['id' => $item->favourite_id]);
+						if ($fav) {
+							$formatted[] = $fav;
+							continue;
+						}
+					}
+
+					//Create a dummy favourite object otherwise
+					$formatted[] = (object) [
+						'type'		=> $item->item_type,
+						'target_id'	=> $item->item_id,
+						'created_at'=> $item->added_at
+					];
+				}
+
+				$sharedWith = [];
+				$is_owner = $this->userOwnsList($list_id, $user_id);
+				if ($is_owner) {
+					$shares = $this->listShares->getMany([
+						'where' => ['list_id' => $list_id],
+						'order_by' => 'created_at',
+						'order'	=> 'DESC'
+					]);
+					foreach ($shares as $share_item) {
+						$user = [
+							'email'		=> $share_item->email,
+							'status'	=> $share_item->status,
+							'date_added'=> $share_item->created_at,
+						];
+						if ($share_item->status === 'accepted' && $share_item->user_id) {
+							$user['name'] = jvbGetUsername($share_item->user_id);
+							$user['permission_type'] = $share_item->permission_type;
+						}
+						$sharedWith[] = $user;
+					}
+				}
+
+				return [
+					'id'	=> (int)$list['id'],
+					'name'	=> $list['name'],
+					'description'	=> $list['description']??'',
+					'created_at'	=> $list['created_at'],
+					'is_owner'		=> $is_owner,
+					'items'			=> $formatted,
+					'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
 	 **************************************************************/
@@ -573,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
+			]);
+		}
+	}
 }

--
Gitblit v1.10.0