From 94de71140be2d0c80bf6a2e03cb9381b37736ed5 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Fri, 06 Feb 2026 17:03:02 +0000
Subject: [PATCH] =Some minor CRUD.js and UploadManager.js tweaks

---
 inc/rest/routes/ContentTermsRoutes.php |  551 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 550 insertions(+), 1 deletions(-)

diff --git a/inc/rest/routes/ContentTermsRoutes.php b/inc/rest/routes/ContentTermsRoutes.php
index 695f192..aa863e0 100644
--- a/inc/rest/routes/ContentTermsRoutes.php
+++ b/inc/rest/routes/ContentTermsRoutes.php
@@ -1,17 +1,566 @@
 <?php
 namespace JVBase\rest\routes;
 
+use JVBase\managers\queue\executors\ContentTermExecutor;
+use JVBase\managers\queue\TypeConfig;
 use JVBase\rest\Rest;
+use JVBase\rest\Route;
+use JVBase\rest\PermissionHandler;
+use JVBase\utility\Features;
+use JVBase\managers\CustomTable;
+use WP_REST_Request;
+use WP_REST_Response;
+use Exception;
 
 if (!defined('ABSPATH')) {
 	exit;
 }
 
+/**
+ * Generic routes for managing content taxonomies (shops, studios, etc.)
+ *
+ * Responsibilities:
+ * - Update term settings/metadata
+ * - Manage members (add/remove via history table)
+ * - Manage ownership/management permissions
+ * - Process membership requests (if verify_entry enabled)
+ *
+ * Note: Invitations handled by InvitationRoutes
+ * Note: Approvals handled by ApprovalRoutes
+ */
 class ContentTermsRoutes extends Rest
 {
+	protected string $taxonomy;
+	protected array $config;
+	protected ?CustomTable $historyTable = null;
+	protected ?CustomTable $requestsTable = null;
+
+	public function __construct(string $taxonomy = '')
+	{
+		$this->taxonomy = jvbNoBase($taxonomy);
+
+		if ($taxonomy && isset(JVB_TAXONOMY[$this->taxonomy])) {
+			$this->config = JVB_TAXONOMY[$this->taxonomy];
+			$this->cacheName = $this->taxonomy;
+			parent::__construct();
+			$this->setupTables();
+			$this->setupCacheConnections();
+		}
+
+		add_action('init', [$this, 'registerContentTermsExecutors'], 5);
+	}
+
+	public function registerContentTermsExecutors():void
+	{
+		$registry = JVB()->queue()->registry();
+		$executor = new ContentTermExecutor();
+		$taxonomies = Features::getTypesWithFeature('is_content', 'taxonomy');
+
+		foreach($taxonomies as $taxonomy) {
+			$registry->register("{$taxonomy}_update", new TypeConfig(
+				executor: $executor,
+			));
+
+			if (Features::forTaxonomy($taxonomy)->has('track_changes')) {
+				$registry->register("{$taxonomy}_member_add", new TypeConfig(
+					executor: $executor
+				));
+
+				$registry->register("{$taxonomy}_member_remove", new TypeConfig(
+					executor: $executor
+				));
+			}
+
+		}
+	}
+
+	protected function setupTables(): void
+	{
+		$content = $this->config['for_content'] ?? [];
+
+		if (Features::forTaxonomy($this->taxonomy)->has('track_changes') && !empty($content)) {
+			foreach ($content as $contentType) {
+				$tableName = "history_{$contentType}_{$this->taxonomy}";
+				$this->historyTable = CustomTable::for($tableName);
+				break; // Only need one table
+			}
+		}
+
+		if (Features::forTaxonomy($this->taxonomy)->has('verify_entry') && !empty($content)) {
+			foreach ($content as $contentType) {
+				$tableName = "{$contentType}_{$this->taxonomy}_requests";
+				$this->requestsTable = CustomTable::for($tableName);
+				break;
+			}
+		}
+	}
+
+	protected function setupCacheConnections(): void
+	{
+		$this->cache
+			->connect('taxonomy')
+			->connect('user');
+	}
 
 	public function registerRoutes(): void
 	{
-		// TODO: Implement registerRoutes() method.
+		if (!Features::forTaxonomy($this->taxonomy)->has('is_content')) {
+			return;
+		}
+
+		$base = $this->taxonomy;
+
+		// Update term settings
+		Route::for("{$base}/:term_id/settings")
+			->post([$this, 'updateSettings'])
+			->args([
+				'user' => 'int|required',
+				'term_id' => 'int|required'
+			])
+			->auth(PermissionHandler::custom([$this, 'checkTermPermission']))
+			->rateLimit(10);
+
+		// Member management (if track_changes enabled)
+		if (Features::forTaxonomy($this->taxonomy)->has('track_changes')) {
+			Route::for("{$base}/:term_id/members")
+				->get([$this, 'getMembers'])
+				->args([
+					'term_id' => 'int|required',
+					'status' => 'string|enum:active,inactive,all|default:active',
+					'page' => 'int|default:1|min:1'
+				])
+				->auth('logged_in')
+				->rateLimit(30)
+
+				->post([$this, 'manageMember'])
+				->args([
+					'user' => 'int|required',
+					'term_id' => 'int|required',
+					'target_user' => 'int|required',
+					'action' => 'string|enum:add,remove|required'
+				])
+				->auth(PermissionHandler::custom([$this, 'checkTermPermission']))
+				->rateLimit(5);
+		}
+
+		// Membership requests (if verify_entry enabled)
+		if (Features::forTaxonomy($this->taxonomy)->has('verify_entry')) {
+			Route::for("{$base}/:term_id/requests")
+				->get([$this, 'getRequests'])
+				->args([
+					'user' => 'int|required',
+					'term_id' => 'int|required',
+					'status' => 'string|enum:requested,accepted,rejected,all|default:requested',
+					'page' => 'int|default:1|min:1'
+				])
+				->auth(PermissionHandler::custom([$this, 'checkTermPermission']))
+				->rateLimit(20);
+
+			Route::for("{$base}/request")
+				->post([$this, 'handleRequest'])
+				->args([
+					'user' => 'int|required',
+					'term_id' => 'int|required',
+					'action' => 'string|enum:create,accept,reject|required',
+					'request_id' => 'int',
+					'notes' => 'string'
+				])
+				->auth('verified')
+				->rateLimit(5);
+		}
+
+		// Ownership/management (if is_ownable enabled)
+		if (Features::forTaxonomy($this->taxonomy)->has('is_ownable')) {
+			Route::for("{$base}/:term_id/permissions")
+				->post([$this, 'updatePermissions'])
+				->args([
+					'user' => 'int|required',
+					'term_id' => 'int|required',
+					'target_user' => 'int|required',
+					'role' => 'string|enum:owner,manager|required',
+					'grant' => 'bool|required'
+				])
+				->auth(PermissionHandler::custom([$this, 'checkOwnerPermission']))
+				->rateLimit(5);
+		}
+	}
+
+	/**
+	 * Permission Callbacks
+	 */
+	public function checkTermPermission(WP_REST_Request $request): bool
+	{
+		$userID = $request->get_param('user') ?? get_current_user_id();
+		$termID = (int)$request->get_param('term_id');
+
+		if (!$this->checkUser($userID) || !term_exists($termID, jvbCheckBase($this->taxonomy))) {
+			return false;
+		}
+
+		return user_can($userID, 'manage_options') ||
+			JVB()->roles()->isManager($userID, $termID);
+	}
+
+	public function checkOwnerPermission(WP_REST_Request $request): bool
+	{
+		$userID = $request->get_param('user') ?? get_current_user_id();
+		$termID = (int)$request->get_param('term_id');
+
+		if (!$this->checkUser($userID) || !term_exists($termID, jvbCheckBase($this->taxonomy))) {
+			return false;
+		}
+
+		return user_can($userID, 'manage_options') ||
+			JVB()->roles()->isOwner($userID, $termID);
+	}
+
+	/**
+	 * Update term settings
+	 */
+	public function updateSettings(WP_REST_Request $request): WP_REST_Response
+	{
+		$termID = (int)$request->get_param('term_id');
+		$userID = $request->get_param('user');
+
+		$data = $request->get_params();
+		unset($data['user'], $data['term_id']);
+
+		// Queue the update
+		$op = JVB()->queue()->add(
+			"{$this->taxonomy}_update",
+			$userID,
+			array_merge(['term_id' => $termID], $data),
+			[
+				'priority' => 'high',
+				'notification' => true
+			]
+		);
+
+		return $this->queued($op['operation_id']);
+	}
+
+	/**
+	 * Get term members
+	 */
+	public function getMembers(WP_REST_Request $request): WP_REST_Response
+	{
+		$termID = (int)$request->get_param('term_id');
+		$status = $request->get_param('status');
+		$page = (int)$request->get_param('page');
+
+		$cacheKey = $this->cache->generateKey(compact('termID', 'status', 'page'));
+
+		return $this->cache->remember($cacheKey, function() use ($termID, $status, $page) {
+			$perPage = 20;
+			$offset = ($page - 1) * $perPage;
+
+			$query = $this->historyTable->where(['term_id' => $termID]);
+
+			if ($status === 'active') {
+				$query = $query->where(['end_date' => null]);
+			} elseif ($status === 'inactive') {
+				$query = $query->whereNotNull('end_date');
+			}
+
+			$total = $query->countResults();
+
+			$members = $query
+				->orderBy('is_primary', 'DESC')
+				->orderBy('created_at', 'ASC')
+				->limit($perPage, $offset)
+				->getResults(ARRAY_A);
+
+			// Enrich with user data
+			$members = array_map(function($member) {
+				$user = get_userdata($member['user_id']);
+				$member['display_name'] = $user ? $user->display_name : 'Unknown';
+				$member['user_email'] = $user ? $user->user_email : '';
+				return $member;
+			}, $members);
+
+			return $this->success([
+				'members' => $members,
+				'total' => $total,
+				'pages' => ceil($total / $perPage),
+				'page' => $page,
+				'per_page' => $perPage
+			]);
+		});
+	}
+
+	/**
+	 * Manage member (add/remove)
+	 */
+	public function manageMember(WP_REST_Request $request): WP_REST_Response
+	{
+		$action = $request->get_param('action');
+		$userID = $request->get_param('user');
+		$termID = (int)$request->get_param('term_id');
+		$targetUserID = (int)$request->get_param('target_user');
+
+		if (!$this->checkUser($targetUserID)) {
+			return $this->error('Invalid target user');
+		}
+
+		// Queue the operation
+		$op = JVB()->queue()->add(
+			"{$this->taxonomy}_member_{$action}",
+			$userID,
+			[
+				'term_id' => $termID,
+				'target_user' => $targetUserID
+			],
+			['priority' => 'high']
+		);
+
+		return $this->queued($op['operation_id']);
+	}
+
+	/**
+	 * Get membership requests
+	 */
+	public function getRequests(WP_REST_Request $request): WP_REST_Response
+	{
+		$termID = (int)$request->get_param('term_id');
+		$status = $request->get_param('status');
+		$page = (int)$request->get_param('page');
+
+		$cacheKey = $this->cache->generateKey(compact('termID', 'status', 'page'));
+
+		return $this->cache->remember($cacheKey, function() use ($termID, $status, $page) {
+			$perPage = 20;
+			$offset = ($page - 1) * $perPage;
+
+			$query = $this->requestsTable->where(['term_id' => $termID]);
+
+			if ($status !== 'all') {
+				$query = $query->where(['status' => $status]);
+			}
+
+			$total = $query->countResults();
+
+			$requests = $query
+				->orderBy('created_date', 'DESC')
+				->limit($perPage, $offset)
+				->getResults(ARRAY_A);
+
+			return $this->success([
+				'requests' => $requests,
+				'total' => $total,
+				'pages' => ceil($total / $perPage),
+				'page' => $page,
+				'per_page' => $perPage
+			]);
+		});
+	}
+
+	/**
+	 * Handle membership request (create/accept/reject)
+	 */
+	public function handleRequest(WP_REST_Request $request): WP_REST_Response
+	{
+		$action = $request->get_param('action');
+		$userID = $request->get_param('user');
+		$termID = (int)$request->get_param('term_id');
+
+		return match($action) {
+			'create' => $this->createRequest($userID, $termID),
+			'accept', 'reject' => $this->processRequest($request),
+			default => $this->error('Invalid action')
+		};
+	}
+
+	protected function createRequest(int $userID, int $termID): WP_REST_Response
+	{
+		if (!term_exists($termID, jvbCheckBase($this->taxonomy))) {
+			return $this->error('Invalid ' . $this->taxonomy);
+		}
+
+		// Check if request already exists
+		$existing = $this->requestsTable
+			->where([
+				'user_id' => $userID,
+				'term_id' => $termID
+			])
+			->first();
+
+		if ($existing) {
+			return $this->error('Request already exists');
+		}
+
+		$contentID = get_user_meta($userID, BASE . 'link', true);
+		if (!$contentID) {
+			return $this->error('User profile not found');
+		}
+
+		try {
+			$requestID = $this->requestsTable->create([
+				'user_id' => $userID,
+				'content_id' => $contentID,
+				'term_id' => $termID,
+				'status' => 'requested',
+				'created_date' => current_time('mysql')
+			]);
+
+			if ($requestID) {
+				$this->notifyTermManagers($termID, $userID, 'membership_request');
+				$this->cache->flush();
+
+				return $this->success([
+					'message' => 'Request submitted successfully',
+					'request_id' => $requestID
+				]);
+			}
+
+			return $this->error('Failed to create request');
+		} catch (Exception $e) {
+			$this->logError('createRequest', [
+				'error' => $e->getMessage(),
+				'user_id' => $userID,
+				'term_id' => $termID
+			]);
+
+			return $this->error('An error occurred');
+		}
+	}
+
+	protected function processRequest(WP_REST_Request $request): WP_REST_Response
+	{
+		$action = $request->get_param('action');
+		$requestID = (int)$request->get_param('request_id');
+		$notes = $request->get_param('notes') ?? '';
+
+		if (!$requestID) {
+			return $this->error('Request ID required');
+		}
+
+		$requestData = $this->requestsTable
+			->where(['id' => $requestID])
+			->first(ARRAY_A);
+
+		if (!$requestData) {
+			return $this->error('Request not found');
+		}
+
+		$status = $action === 'accept' ? 'accepted' : 'rejected';
+
+		try {
+			$this->requestsTable->transaction(function($table) use ($requestID, $status, $notes, $requestData) {
+				// Update request
+				$table->where(['id' => $requestID])->updateResults([
+					'status' => $status,
+					'notes' => $notes,
+					'updated_date' => current_time('mysql')
+				]);
+
+				// If accepted, add member via action
+				if ($status === 'accepted') {
+					do_action(
+						BASE . 'add_user_to_term',
+						$requestData['user_id'],
+						$requestData['term_id'],
+						$this->taxonomy,
+						'member'
+					);
+				}
+			});
+
+			// Notify user
+			JVB()->notification()->addNotification(
+				$requestData['user_id'],
+				'request_' . $status,
+				[
+					'term_id' => $requestData['term_id'],
+					'taxonomy' => $this->taxonomy,
+					'notes' => $notes
+				]
+			);
+
+			$this->cache->flush();
+
+			return $this->success(['message' => 'Request ' . $status]);
+
+		} catch (Exception $e) {
+			$this->logError('processRequest', [
+				'error' => $e->getMessage(),
+				'request_id' => $requestID,
+				'action' => $action
+			]);
+
+			return $this->error('Failed to process request');
+		}
+	}
+
+	/**
+	 * Update ownership/management permissions
+	 */
+	public function updatePermissions(WP_REST_Request $request): WP_REST_Response
+	{
+		$termID = (int)$request->get_param('term_id');
+		$targetUserID = (int)$request->get_param('target_user');
+		$role = $request->get_param('role');
+		$grant = (bool)$request->get_param('grant');
+
+		if (!$this->checkUser($targetUserID)) {
+			return $this->error('Invalid target user');
+		}
+
+		$roleManager = JVB()->roles();
+
+		try {
+			$result = match($role) {
+				'owner' => $grant
+					? $roleManager->grantOwnership($targetUserID, $termID, $this->taxonomy)
+					: $roleManager->revokeOwnership($targetUserID, $termID, $this->taxonomy),
+				'manager' => $grant
+					? $roleManager->grantManagement($targetUserID, $termID, $this->taxonomy)
+					: $roleManager->revokeManagement($targetUserID, $termID, $this->taxonomy),
+				default => false
+			};
+
+			if ($result) {
+				$this->cache->flush();
+
+				return $this->success([
+					'message' => ucfirst($role) . ' permissions ' . ($grant ? 'granted' : 'revoked')
+				]);
+			}
+
+			return $this->error('Failed to update permissions');
+
+		} catch (Exception $e) {
+			$this->logError('updatePermissions', [
+				'error' => $e->getMessage(),
+				'term_id' => $termID,
+				'target_user' => $targetUserID,
+				'role' => $role
+			]);
+
+			return $this->error('An error occurred');
+		}
+	}
+
+	/**
+	 * Notify term managers
+	 */
+	protected function notifyTermManagers(int $termID, int $actingUserID, string $notificationType): void
+	{
+		$roleManager = JVB()->roles();
+		$managers = $roleManager->getManagedTerms($actingUserID, $this->taxonomy);
+
+		$term = get_term($termID, jvbCheckBase($this->taxonomy));
+
+		foreach ($managers as $managerID) {
+			JVB()->notification()->addNotification(
+				$managerID,
+				$notificationType,
+				[
+					'user_id' => $actingUserID,
+					'term_id' => $termID,
+					'term_name' => $term->name ?? 'Unknown',
+					'taxonomy' => $this->taxonomy
+				]
+			);
+		}
 	}
 }

--
Gitblit v1.10.0