From ac444cba221832c012c0435fdc8339fe9f37febb Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Mon, 11 May 2026 18:35:04 +0000
Subject: [PATCH] =Some changes to the CRUD.js editing, timeline post configuration

---
 inc/rest/routes/ContentRoutes.php | 1155 +++++++++++----------------------------------------------
 1 files changed, 232 insertions(+), 923 deletions(-)

diff --git a/inc/rest/routes/ContentRoutes.php b/inc/rest/routes/ContentRoutes.php
index 46c7d45..dd25f8e 100644
--- a/inc/rest/routes/ContentRoutes.php
+++ b/inc/rest/routes/ContentRoutes.php
@@ -2,46 +2,47 @@
 
 namespace JVBase\rest\routes;
 
-use JVBase\JVB;
 use JVBase\managers\queue\executors\ContentExecutor;
 use JVBase\managers\queue\TypeConfig;
-use JVBase\rest\RestRouteManager;
-use JVBase\managers\Cache;
-use JVBase\meta\MetaManager;
-use JVBase\utility\Features;
+use JVBase\meta\Meta;
+use JVBase\registrar\Registrar;
+use JVBase\rest\PermissionHandler;
+use JVBase\rest\Response;
+use JVBase\rest\Rest;
+use JVBase\rest\Route;
 use WP_Post;
 use WP_Query;
-use WP_Error;
 use WP_REST_Request;
 use WP_REST_Response;
-use Exception;
+use WP_Term;
+use WP_Term_Query;
 
 if (!defined('ABSPATH')) {
 	exit; // Exit if accessed directly
 }
 
-class ContentRoutes extends RestRouteManager
+class ContentRoutes extends Rest
 {
 	protected array $fields = [];
 	protected array $taxonomies = [];
-	protected MetaManager $meta;
 	protected string $post_type = '';
 	protected string $user_id = '';
 
 	//For Timeline-specific posts
 	protected array $timelineSharedFields = [];
 	protected array $timelineUniqueFields = [];
+	protected static ?string $action = 'dash-';
+	protected Meta $meta;
 
 	public function __construct()
 	{
-		$this->cache_name = 'user_content_' . get_current_user_id();
+		$this->cacheName = 'user_content_' . get_current_user_id();
 		parent::__construct();
 		if (JVB_TESTING) {
 			$this->cache->flush();
 		}
-
-		$this->action = 'dash-';
-		$this->operation_type = 'content_update';
+		$this->cache->connect('post', true);
+		$this->cache->connect('term', true);
 		add_action('init', [$this, 'registerContentExecutors'], 5);
 	}
 
@@ -60,10 +61,10 @@
 			chunkSize: 10
 		));
 
-		// Batch creation (from uploads)
-		$registry->register('batch_creation', new TypeConfig(
-			executor: $executor
-		));
+		// Batch creation (from uploads) TODO: I believe this is all handled by UploadExecutor
+//		$registry->register('batch_creation', new TypeConfig(
+//			executor: $executor
+//		));
 	}
 
 	/**
@@ -72,45 +73,42 @@
 	 */
 	public function registerRoutes(): void
 	{
-		// Base content endpoint
-		register_rest_route($this->namespace, "/content", [
-			[
-				'methods' => 'GET',
-				'callback' => [$this, 'handleContentRequest'],
-				'permission_callback' => [$this, 'checkPermission'],
-			],
-			[
-				'methods' => 'POST',
-				'callback' => [$this, 'handleContentUpdate'],
-				'permission_callback' => [$this, 'checkPermission']
-			]
-		]);
-
-		//TODO: consolidate create/batch in with create? I don't think we are ever creating a single item
-		register_rest_route($this->namespace, "/create", [
-			[
-				'methods' => 'POST',
-				'callback' => [$this, 'handleContentCreate'],
-				'permission_callback' => [$this, 'checkPermission']
-			]
-		]);
-		register_rest_route($this->namespace, "/create/batch", [
-			[
-				'methods' => 'POST',
-				'callback' => [$this, 'handleBatchCreation'],
-				'permission_callback' => [$this, 'checkPermission']
-			]
-		]);
+		Route::for('content')
+			->get([$this, 'getContent'])
+			->auth(PermissionHandler::combine(['user', 'nonce', ['actionNonce'=>'dash-']]))
+			->args([
+				'content' => 'string|required',
+				'status' => 'string|default:all',
+				'page' => 'integer|default:1|min:1',
+				'per_page' => 'integer|default:30|min:1|max:100',
+				'orderby' => 'string|enum:date,alphabetical|default:date',
+				'order' => 'string|enum:asc,desc|default:desc',
+				'search' => 'string',
+				'date-filter' => 'string',
+				'dateFrom' => 'string',
+				'dateTo' => 'string',
+			])
+			->rateLimit(20)
+			->post([$this, 'postContent'])
+			->auth(PermissionHandler::combine(['user', 'nonce', ['actionNonce'=>'dash-']]))
+			->rateLimit(30)
+			->args([
+				'user'	=> 'int|required',
+				'posts' => 'required',
+				'content' => 'string',
+			])
+			->register();
 	}
 
 	protected function initTimelineFields(string $content): void
 	{
 		$content = jvbNoBase($content);
-		if (!Features::forContent($content)->has('is_timeline')) {
+
+		$config = Registrar::getInstance($content);
+		if (!$config || !$config->hasFeature('is_timeline')) {
 			return;
 		}
-		$config = Features::getConfig($content);
-		$this->fields = $config['fields'];
+		$this->fields = $config->getFields();
 
 		$this->timelineSharedFields = $this->getTimelineSharedFields($content);
 		array_unshift($this->timelineSharedFields, 'post_thumbnail');
@@ -123,11 +121,12 @@
 	public function getTimelineUniqueFields(string $content): array
 	{
 		$content = jvbNoBase($content);
-		if (!Features::forContent($content)->has('is_timeline')) {
+		$registrar = Registrar::getInstance($content);
+		if (!$registrar || !$registrar->hasFeature('is_timeline')) {
 			return [];
 		}
-		$config = Features::getConfig($content);
-		$allFields = $config['fields'];
+
+		$allFields = $registrar->getFields();
 
 		return array_keys(array_filter($allFields, function ($field) {
 			if (array_key_exists('for_all', $field) && $field['for_all'] === true) {
@@ -140,17 +139,15 @@
 	public function getTimelineSharedFields(string $content): array
 	{
 		$content = jvbNoBase($content);
-		if (!Features::forContent($content)->has('is_timeline')) {
+		$registrar = Registrar::getInstance($content);
+		if (!$registrar || !$registrar->hasFeature('is_timeline')) {
 			return [];
 		}
-		$config = Features::getConfig($content);
-		if (!$config || empty($config)) {
-			return [];
-		}
-		$allFields = $config['fields'] ?? [];
+
+		$allFields = $registrar->getFields()??[];
 
 		return array_keys(array_filter($allFields, function ($field) {
-			if (!array_key_exists('for_all', $field) || $field['for_all'] === false) {
+			if (!array_key_exists('for_all', $field) || is_null($field['for_all']) || $field['for_all'] === false) {
 				return true;
 			}
 			return false;
@@ -163,86 +160,33 @@
 	 *
 	 * @return WP_REST_Response
 	 */
-	public function handleContentUpdate(WP_REST_Request $request): WP_REST_Response
+	public function postContent(WP_REST_Request $request): WP_REST_Response
 	{
 		$data = $request->get_params();
 		$user_id = $data['user'];
 
-		if (!$this->userCheck($user_id)) {
-			return new WP_REST_Response([
-				'success' => true,
-				'message' => 'You for real?'
-			]);
+		if (!array_key_exists('posts', $data) || !is_array($data['posts'])) {
+			return Response::success(['message'=>'No posts found in request']);
 		}
 
-		if (!array_key_exists('posts', $data) || !is_array($data['posts'])) {
-			return new WP_REST_Response([
-				'success' => true,
-				'message' => 'No posts found'
-			]);
-		}
 
 		$count = count($data['posts']);
 		$operationId = $data['id'];
 		unset($data['user']);
 		unset($data['id']);
 
+		error_log('[CONTENT]:'.print_r($data, true));
 		$queue = JVB()->queue();
 		$queue->queueOperation(
 			'content_update',
 			$user_id,
 			$data,
 			[
-				'count' => $count,
-				'chunk_key' => 'posts',
-				'chunk_size' => 10,
 				'operation_id' => $operationId
 			]
 		);
-		return new WP_REST_Response([
-			'success' => true,
-			'message' => 'Queued for processing',
-			'operation' => $operationId
-		]);
-	}
 
-	/**
-	 * Handle content creation
-	 * @param WP_REST_Request $request
-	 *
-	 * @return WP_REST_Response
-	 */
-	public function handleContentCreate(WP_REST_Request $request): WP_REST_Response
-	{
-		$data = $request->get_json_params();
-		$user_id = $data['user'];
-
-		if (!isset($data['posts']) || !is_array($data['posts'])) {
-			return new WP_REST_Response([
-				'success' => false,
-				'message' => 'Invalid request format'
-			]);
-		}
-
-		$count = count($data['posts']);
-		$operationId = $data['id'];
-		unset($data['user']);
-		unset($data['id']);
-		JVB()->queue()->queueOperation(
-			'batch_creation',
-			$user_id,
-			$data,
-			[
-				'count' => $count,
-				'operation_id' => $operationId,
-			]
-		);
-
-		return new WP_REST_Response([
-			'success' => true,
-			'message' => 'Queued for processing',
-			'operation' => $operationId
-		]);
+		return Response::queued($operationId);
 	}
 
 
@@ -252,18 +196,29 @@
 	 *
 	 * @return WP_REST_Response
 	 */
-	public function handleContentRequest(WP_REST_Request $request): WP_REST_Response
+	public function getContent(WP_REST_Request $request): WP_REST_Response
 	{
 		$params = $request->get_params();
-		$user_id = $params['user'];
+		error_log('getContent::params '.print_r($params, true));
 
-		if (!$this->userCheck($user_id)) {
-			return new WP_REST_Response([
-				'success' => false,
-				'message' => 'User does not match up. Are you a bot?',
-			]);
+		$registrar = Registrar::getInstance($params['content']);
+		switch ($registrar->getType()) {
+			case 'term':
+				return $this->getTerms($request, $params, $registrar);
+			case 'user':
+				//TODO maybe do something?
+				break;
+			case 'post':
+				return $this->getPosts($request, $params, $registrar);
 		}
 
+		return $this->error('Something went wrong, this does not appear to have a proper content type');
+	}
+
+	public function getPosts(WP_REST_Request $request, array $params, Registrar $registrar):WP_REST_Response
+	{
+		$user_id = $params['user'];
+
 		$post_status = $params['status'];
 		if ($post_status === 'all') {
 			$post_status = ['publish', 'draft'];
@@ -272,7 +227,6 @@
 		}
 		$post_type = str_replace('-', '_', jvbCheckBase($params['content']));
 
-
 		// Build query args
 		$args = [
 			'post_type' => $post_type,
@@ -283,13 +237,15 @@
 			'author' => $user_id,
 			'post_status' => $post_status
 		];
+
+
 		//Only top level posts for timeline types
-		if (Features::forContent($post_type)->has('is_timeline')) {
+		if ($registrar?->hasFeature('is_timeline')) {
 			$args['post_parent'] = 0;
 		}
 
 		//Calendar filters
-		if (Features::forContent($post_type)->has('is_calendar')) {
+		if ($registrar?->hasFeature('is_calendar')) {
 			$args = $this->applyCalendarFilters($args, $params);
 		}
 		$taxonomies = array_filter($params, function ($param) {
@@ -316,6 +272,70 @@
 			$args['s'] = sanitize_text_field($params['search']);
 		}
 
+		$key = $this->cache->generateKey($args);
+		$cached = $this->checkCache($key, $request);
+		if ($cached) {
+			return $cached;
+		}
+
+		$this->post_type = jvbCheckBase($params['content'] ?? $params['type']);
+
+		if (array_key_exists('s', $args)) {
+			$args = $this->applySearchFilters($args, $params);
+		}
+
+		// Run query
+		$query = new WP_Query($args);
+
+		$registrar = Registrar::getInstance($this->post_type);
+		$this->fields = $registrar->getFields()??[];
+		$this->taxonomies = $this->getTaxonomies($this->post_type);
+
+		$posts = array_map([$this, 'preparePost'], $query->posts);
+
+		$data = [
+			'items' => $posts,
+			'total' => $query->found_posts,
+			'total_pages' => $query->max_num_pages,
+			'has_more'	=> $args['paged']??1 < $query->max_num_pages,
+		];
+
+
+		$this->cache->set($key, $data);
+
+		$response = Response::success($data);
+		return $this->addCacheHeaders($response);
+	}
+	public function getTerms(WP_REST_Request $request, array $params, Registrar $registrar):WP_REST_Response
+	{
+		// Build query args
+		$args = [
+			'taxonomy' 		=> jvbCheckBase($params['content']),
+			'number'		=> $params['per_page'] ?? 30,
+			'orderby' 		=> 'name',
+			'order' 		=> 'DESC',
+			'hide_empty'	=> false,
+		];
+		$paged = $params['page']??1;
+		$args['page'] = $paged;
+		if ($paged > 1) {
+			$args['offset'] = ($paged-1) * $args['number'];
+		}
+
+		//TODO
+//		if (array_key_exists('taxonomies', $params)) {
+//			$args = $this->applyTaxonomyFilters($args, $params);
+//		}
+//		if (array_key_exists('date-filter', $params) || array_key_exists('dateFrom', $params)) {
+//			$args = $this->applyDateFilters($args, $params);
+//		}
+		if (array_key_exists('orderby', $params) || array_key_exists('order', $params)) {
+			$args = $this->applyOrderFilters($args, $params);
+		}
+
+		if (array_key_exists('search', $params)) {
+			$args['s'] = sanitize_text_field($params['search']);
+		}
 
 		$key = $this->cache->generateKey($args);
 		// Check HTTP cache headers with the specific content type
@@ -324,13 +344,11 @@
 			return $cache_check;
 		}
 
-
 		$cache = $this->cache->get($key);
 		if ($cache) {
-			$response = new WP_REST_Response($cache);
+			$response = Response::success($cache);
 			return $this->addCacheHeaders($response);
 		}
-
 		$this->post_type = jvbCheckBase($params['content'] ?? $params['type']);
 		// Only expand search to taxonomies if we're actually going to query
 		if (array_key_exists('s', $args)) {
@@ -338,23 +356,37 @@
 		}
 
 		// Run query
-		$query = new WP_Query($args);
+		$query = new WP_Term_Query($args);
 
-
-		$this->fields = jvbGetFields(str_replace('-', '_', $this->post_type));
-		$this->taxonomies = $this->getTaxonomies($this->post_type);
-		$posts = array_map([$this, 'prepareItem'], $query->posts);
-
+		$terms = $query->get_terms();
 		$data = [
-			'items' => $posts,
-			'total' => $query->found_posts,
-			'total_pages' => $query->max_num_pages
+			'total'			=> 0,
+			'total_pages' 	=> 0,
+			'has_more' 		=> false
 		];
 
+		if (!is_wp_error($terms) && !empty($terms))
+		{
+			$total = get_terms([
+				'taxonomy'		=> $args['taxonomy'],
+				'hide_empty'	=> false,
+				'fields'		=> 'count'
+			]);
+			$data['total'] = $total;
+			$data['total_pages'] = max($total/$args['number'], 1);
+			$data['has_more'] = ($args['page'] * $args['number']) < $total;
+		} else {
+			$terms = [];
+		}
+
+		$this->fields = $registrar->getFields()??[];
+
+		$this->taxonomies = [];
+		$data['items'] =array_map([$this, 'prepareTerm'], $terms);
 
 		$this->cache->set($key, $data);
 
-		$response = new WP_REST_Response($data);
+		$response = Response::success($data);
 		return $this->addCacheHeaders($response);
 	}
 
@@ -447,465 +479,23 @@
 	 */
 	protected function getTaxonomies(string $content): array
 	{
-		$taxonomy_for = jvbGlobalTaxonomyFor();
-		$out = [];
-		foreach ($taxonomy_for as $tax => $postTypes) {
-			if (in_array($content, $postTypes)) {
-				$out[BASE . $tax] = [
-					'label' => JVB_CONTENT[$content]['plural'],
-					'icon' => $tax,
-				];
-			}
+		$registrar = Registrar::getInstance($content);
+		if (!$registrar || $registrar->getType()!== 'post') {
+			return [];
 		}
-		return $out;
-	}
-
-	/**
-	 * Processes operation from queue
-	 * @param object $operation
-	 * @param array $data
-	 *
-	 * @return array
-	 */
-	protected function processBatches(object $operation, array $data): array
-	{
-		$this->user_id = $operation->user_id;
-		$posts = $data['posts'];
-
-		if (empty($posts)) {
-			return [
-				'success' => false,
-				'message' => 'No posts to update'
+		$out = [];
+		foreach ($registrar->registrar->taxonomies as $tax) {
+			$taxReg = Registrar::getInstance($tax);
+			$out[jvbCheckBase($tax)] = [
+				'label'	=> $taxReg->getPlural(),
+				'icon'	=> $taxReg->getIcon()??jvbDefaultIcon()
 			];
 		}
 
-		$results = [];
-
-		foreach ($posts as $ID => $post_data) {
-			if (Features::forContent($post_data['content'])->has('is_timeline') && array_key_exists('timeline', $post_data)) {
-				// Handle timeline posts - ensure we have a valid integer ID
-				$parent_id = (int)$ID;
-
-				// Skip if ID is invalid (0, 'null', etc would become 0)
-				if ($parent_id === 0) {
-					error_log('Invalid timeline parent ID: ' . $ID);
-					$results[$ID] = [
-						'success' => false,
-						'message' => 'Invalid parent post ID for timeline'
-					];
-					continue;
-				}
-
-				$results[$ID] = $this->processTimelinePost($parent_id, $post_data);
-				continue;
-			}
-			if (str_starts_with($ID, 'new')) {
-
-				error_log('New post detected. Creating... with: ' . print_r([
-						'post_author' => $this->user_id,
-						'post_type' => jvbCheckBase($post_data['content']),
-						'post_title' => $post_data['post_title'] ?? '',
-						'post_status' => $post_data['status'] ?? 'draft',
-					], true));
-				error_log('Recieved Data: ' . print_r($post_data, true));
-				$ID = wp_insert_post([
-					'post_author' => $this->user_id,
-					'post_type' => jvbCheckBase($post_data['content']),
-					'post_title' => $post_data['post_title'] ?? '',
-					'post_status' => $post_data['status'] ?? 'draft',
-				]);
-				if (!$ID || is_wp_error($ID)) {
-					$results[$ID] = [
-						'success' => false,
-						'message' => 'Couldn\'t Create Post'
-					];
-					continue;
-				}
-				$fields = jvbGetFields($post_data['content']);
-				$allowedFields = array_filter($post_data, function ($key) use ($fields) {
-					return array_key_exists($key, $fields);
-				}, ARRAY_FILTER_USE_KEY);
-
-				$meta = new MetaManager($ID, 'post');
-				$success = $meta->setAll($allowedFields);
-				$results[$ID] = [
-					'success' => $success
-				];
-			} else {
-				if (!$this->verifyOwnership($ID)) {
-					$results[$ID] = [
-						'success' => false,
-						'message' => 'No permission to modify this post'
-					];
-					continue;
-				}
-				error_log('Saving post data: ' . print_r($post_data, true));
-
-				if (array_key_exists('post_status', $post_data)) {
-					switch ($post_data['post_status']) {
-						case 'publish':
-							unset($post_data['post_status']);
-							if (user_can($this->user_id, 'manage_options') || user_can($this->user_id, 'skip_moderation')) {
-								$result = wp_update_post(['ID' => $ID, 'post_status' => 'publish']);
-							}
-							break;
-						case 'draft':
-							$result = wp_update_post([
-								'ID' => $ID,
-								'post_status' => 'draft'
-							]);
-							break;
-						case 'trash':
-							$result = wp_trash_post($ID);
-							break;
-						case 'delete':
-							$result = wp_delete_post($ID, true);
-							return ['success' => (bool)$result];
-					}
-				}
-				error_log('Updating data: ' . print_r($post_data, true));
-				$fields = jvbGetFields($post_data['content']);
-				$allowedFields = array_filter($post_data, function ($key) use ($fields) {
-					return array_key_exists($key, $fields);
-				}, ARRAY_FILTER_USE_KEY);
-
-				error_log('Allowed Fields: ' . print_r($allowedFields, true));
-				$meta = new MetaManager($ID, 'post');
-				$success = $meta->setAll($allowedFields);
-				$results[$ID] = [
-					'success' => $success
-				];
-
-			}
-		}
-
-		if (jvbSiteHasNotifications()) {
-			$this->notifications = JVB()->notification();
-			$this->notifications->addNotification(
-				$this->user_id,
-				'content_update_complete',
-				null,
-				'Content updates completed!'
-			);
-		}
-
-
-		return [
-			'success' => true,
-			'result' => $results
-		];
+		return $out;
 	}
 
-	/**
-	 * Extracts the postdata for timeline post child posts from the pseudo-repeater element
-	 * @param int $parent_id
-	 * @param array $post_data
-	 * @return array|true[]
-	 */
-	protected function processTimelinePost(int $parent_id, array $post_data): array
-	{
-		if (!$this->verifyOwnership($parent_id)) {
-			return ['success' => false, 'message' => 'No permission'];
-		}
 
-		error_log('[Processing Timeline Post...');
-
-		$ignore = ['content', 'user'];
-		$this->fields = jvbGetFields($post_data['content']);
-		$this->initTimelineFields($post_data['content']);
-
-		// Get parent post details
-		$parent_post = get_post($parent_id);
-		$parent_title = $parent_post->post_title;
-		$parent_is_published = ($parent_post->post_status === 'publish');
-
-		// Extract shared data from top level (excluding post_thumbnail which is unique per post)
-		$sharedData = array_filter($post_data, function ($key) use ($ignore) {
-			return in_array($key, $this->timelineSharedFields)
-				&& !in_array($key, $ignore)
-				&& $key !== 'post_thumbnail';
-		}, ARRAY_FILTER_USE_KEY);
-
-		// If no shared post_title at top level, extract from first timeline entry
-		if (!isset($sharedData['post_title']) && isset($post_data['timeline'][0]['post_title'])) {
-			$sharedData['post_title'] = $post_data['timeline'][0]['post_title'];
-		}
-		$clearParent = false;
-		if (array_key_exists('timeline', $post_data) && is_array($post_data['timeline'])) {
-			// Remove post_title and post_thumbnail from shared taxonomies
-			$sharedTaxonomies = array_filter($sharedData, function ($key) {
-				return $key !== 'post_title' && $key !== 'post_thumbnail';
-			}, ARRAY_FILTER_USE_KEY);
-
-			// Ensure the parent post exists and is still first in the array
-			$index = array_search((string)$parent_id, array_column($post_data['timeline'], 'id'));
-
-			if ($index === false) {
-				return [
-					'success' => false,
-					'message' => 'Missing parent id. This should not have happened'
-				];
-			}
-
-			if ($index !== 0) {
-				$new_parent_id = $post_data['timeline'][0]['id'];
-
-				if (is_numeric($new_parent_id) && (int)$new_parent_id > 0) {
-					$new_parent_id = (int)$new_parent_id;
-					wp_update_post([
-						'ID' => $new_parent_id,
-						'post_parent' => 0
-					]);
-
-					wp_update_post([
-						'ID' => $parent_id,
-						'post_parent' => $new_parent_id
-					]);
-
-					$existing_children = get_children([
-						'post_parent' => $parent_id,
-						'fields' => 'ids'
-					]);
-
-					foreach ($existing_children as $child_id) {
-						if ($child_id !== $new_parent_id) {
-							wp_update_post([
-								'ID' => $child_id,
-								'post_parent' => $new_parent_id
-							]);
-						}
-					}
-
-					// Update parent references
-					$parent_id = $new_parent_id;
-					$parent_post = get_post($parent_id);
-					$parent_title = $parent_post->post_title;
-					$parent_is_published = ($parent_post->post_status === 'publish');
-				} else {
-					$item = $post_data['timeline'][$index];
-					unset($post_data['timeline'][$index]);
-					array_unshift($post_data['timeline'], $item);
-				}
-			}
-
-			$errors = [];
-			$success = [];
-			$existing_children = get_children([
-				'post_parent' => $parent_id,
-				'orderby' => 'menu_order',
-				'post_status' => ['publish', 'draft'],
-				'fields' => 'ids'
-			]);
-
-			$prevDate = null;
-			$latest_date = null;
-			$earliest_date = null;
-			foreach ($post_data['timeline'] as $order => $timeline) {
-				// Get unique fields for this specific timeline entry
-				$allowedFields = array_filter($timeline, function ($key) use ($ignore) {
-					return in_array($key, $this->timelineUniqueFields) && !in_array($key, $ignore);
-				}, ARRAY_FILTER_USE_KEY);
-
-				// Determine the post title
-				$is_parent = ((int)$timeline['id'] === $parent_id);
-				$provided_title = $timeline['post_title'] ?? '';
-				$auto_generated_pattern = '/^.+Treatment #?\d+$/'; // Matches "Title - Treatment #1" or "Title - Treatment 1"
-
-				if ($is_parent) {
-					// Parent keeps its own title or uses shared title
-					$allowedFields['post_title'] = $provided_title ?: ($sharedData['post_title'] ?? $parent_title);
-				} else {
-					// For child posts, auto-generate if:
-					// 1. No title provided, OR
-					// 2. Title matches auto-generated pattern (meaning it wasn't customized)
-					if (empty($provided_title) || preg_match($auto_generated_pattern, $provided_title)) {
-						$allowedFields['post_title'] = 'Treatment ' . $order;
-					} else {
-						// Keep custom title
-						$allowedFields['post_title'] = $provided_title;
-					}
-				}
-
-				// Merge with shared taxonomies AFTER setting unique fields
-				$allowedFields = array_merge($sharedTaxonomies, $allowedFields);
-
-				// Handle post creation if needed
-				if (!array_key_exists('id', $timeline) || !is_numeric($timeline['id'])) {
-					$newChild = wp_insert_post([
-						'post_author' => $this->user_id,
-						'post_type' => jvbCheckBase($post_data['content']),
-						'post_title' => $allowedFields['post_title'],
-						'post_parent' => $parent_id,
-						'menu_order' => $order,
-						'post_status' => $parent_is_published ? 'publish' : 'draft'
-					]);
-					if (!$newChild || is_wp_error($newChild)) {
-						$errors[] = [
-							'message' => 'Could not create child post',
-							'data' => $timeline
-						];
-						continue;
-					}
-					$timeline['id'] = $newChild;
-				}
-
-				if (in_array((int)$timeline['id'], $existing_children)) {
-					unset($existing_children[array_search((int)$timeline['id'], $existing_children)]);
-				}
-
-				// Update post status and menu order
-				$post_updates = ['ID' => $timeline['id']];
-
-				if (!$is_parent) {
-					$post_updates['menu_order'] = $order;
-
-					// Auto-publish child if parent is published
-					if ($parent_is_published) {
-						$current_post = get_post($timeline['id']);
-						if ($current_post && $current_post->post_status !== 'publish') {
-							$post_updates['post_status'] = 'publish';
-						}
-					}
-				}
-
-				if (count($post_updates) > 1) {
-					$result = wp_update_post($post_updates);
-					error_log('Updated post ' . $timeline['id'] . ' with: ' . print_r($post_updates, true) . ' Result: ' . $result);
-					$clearParent = true;
-				}
-
-				// Update metadata
-				$meta = new MetaManager($timeline['id'], 'post');
-				$oldValues = $meta->getAll(array_keys($allowedFields));
-
-//				// Set number taxonomy to menu_order (always update for reordering)
-//				if (!$is_parent) {
-//					$number_value = $order;
-//					$term = get_term_by('name', (string)$number_value, BASE . 'number');
-//					if (!$term) {
-//						$result = wp_insert_term((string)$number_value, BASE . 'number');
-//						if ($result && !is_wp_error($result)) {
-//							$term = $result['term_id'];
-//						}
-//					} else {
-//						$term = $term->term_id;
-//					}
-//					$allowedFields['number'] = $term;
-//				}
-
-				// Auto-timeline logic
-				if ($prevDate) {
-					$newDate = array_key_exists('date', $oldValues) ? $oldValues['date'] : ((array_key_exists('date', $allowedFields)) ? $allowedFields['date'] : null);
-					if ($newDate) {
-						$date1 = new \DateTime($prevDate);
-						$date2 = new \DateTime($newDate);
-						$weeks = floor($date1->diff($date2)->days / 7);
-						if ($weeks > 0) {
-							$termToCheck = $weeks . ' Weeks';
-							$term = get_term_by('name', $termToCheck, BASE . 'timeline');
-							if (!$term) {
-								$result = wp_insert_term($termToCheck, BASE . 'timeline');
-								if ($result && !is_wp_error($result)) {
-									$term = $result['term_id'];
-								}
-							} else {
-								$term = $term->term_id;
-							}
-							$allowedFields['timeline'] = $term;
-						}
-					}
-				}
-				$prevDate = array_key_exists('date', $oldValues) ? $oldValues['date'] : ((array_key_exists('date', $allowedFields)) ? $allowedFields['date'] : $prevDate);
-
-				$updateValues = array_filter($allowedFields, function ($value, $key) use ($oldValues) {
-					return (!array_key_exists($key, $oldValues) || $value !== $oldValues[$key]);
-				}, ARRAY_FILTER_USE_BOTH);
-
-
-				$meta->setAll($updateValues);
-				$timeline['id'] = (int)$timeline['id'];
-
-				$success[] = $timeline['id'];
-			}
-		}
-
-		// Delete any remaining children that no longer exist
-		if (!empty($existing_children)) {
-			foreach ($existing_children as $ID) {
-				wp_delete_post($ID);
-			}
-		}
-
-		if ($clearParent) {
-			$this->cache->flush();
-			Cache::onPostChange($parent_id, $parent_post);
-		}
-
-
-		return ['success' => true, 'data' => [
-			'success' => $success,
-			'errors' => $errors
-		]];
-	}
-
-	/**
-	 * Handle batch content creation from uploads
-	 * @param WP_REST_Request $request
-	 *
-	 * @return WP_REST_Response
-	 */
-	public function handleBatchCreation(WP_REST_Request $request): WP_REST_Response
-	{
-		//Operation has two parts
-		//First, queue image processing
-		//Then queue post creation from the stored IDs, depending on mode
-		//if direct, each image becomes a new post
-		//if selection, each group becomes its own post,
-		// and ungrouped items each become their own post
-		if (!isset($_FILES['files'])) {
-			return new WP_REST_Response([
-				'success' => false,
-				'message' => 'No files uploaded...',
-			]);
-		}
-
-		$data = $request->get_params();
-
-
-		$user_id = $data['user'];
-		if (!$this->userCheck($user_id)) {
-			return new WP_REST_Response([
-				'success' => 'false',
-				'message' => 'Invalid user match... are you a bot?'
-			]);
-		}
-		$operation_id = $data['id'];
-		$response = new WP_REST_Response([
-			'success' => true,
-			'message' => 'Successfully sent to server. Added to queue.',
-			'operation_id' => $operation_id,
-			'status' => 'pending'
-		]);
-		$this->queue = JVB()->queue();
-		JVB()->routes('uploads')->handleUploadRequest($request, false);
-		$this->queue->queueOperation(
-			'batch_creation',
-			$user_id,
-			[
-				'content' => $request->get_param('content'),
-				'mode' => $request->get_param('mode') ?: 'direct',
-				'files_data' => $request->get_param('files_data')
-			],
-			[
-				'operation_id' => $operation_id,
-				'priority' => 'high',
-				'notification' => true,
-				'depends_on' => $operation_id . '_upload'
-			]
-		);
-
-		return $response;
-	}
 
 	/**
 	 * Generates a post title, based on content type
@@ -926,13 +516,15 @@
 	 *
 	 * @return array
 	 */
-	protected function prepareItem(WP_Post $post, bool $skip = false, bool $fields = true): array
+	protected function preparePost(WP_Post $post, bool $skip = false, bool $fields = true): array
 	{
-		if (!$skip && Features::forContent($post->post_type)->has('is_timeline')) {
+		$registrar = Registrar::getInstance($post->post_type);
+		if (!$skip && $registrar && $registrar->hasFeature('is_timeline')) {
 			$this->initTimelineFields($post->post_type);
 			return $this->formatTimeline($post);
 		}
-		$this->meta = new MetaManager($post->ID, 'post');
+		$this->meta = Meta::forPost($post->ID);
+		$fields = ($fields) ? $this->meta->getAll() : [];
 		$data = [
 			'id' => $post->ID,
 			'title' => $post->post_title,
@@ -941,68 +533,68 @@
 			'modified' => $post->post_modified,
 			'thumbnail' => get_the_post_thumbnail_url($post->ID),
 			'alt' => get_post_meta(get_post_thumbnail_id(), '_wp_attachment_image_alt', true),
-			'icon' => $this->post_type,
+			'icon' => $registrar->getIcon(),
 			'taxonomies' => [],
-			'fields' => ($fields) ? $this->meta->getAll() : [],
+			'fields' => $fields,
 			'images' => [],
 		];
 
-		// Add taxonomy terms
-		foreach ($this->taxonomies as $taxonomy => $options) {
-			$tax = str_replace(BASE, '', $taxonomy);
-			$terms = wp_get_object_terms(
-				$post->ID,
-				$taxonomy,
-				['fields' => 'id=>name']
-			);
-			$data['taxonomies'][$tax] = [
-				'terms' => (is_wp_error($terms)) ? [] : $terms,
-				'name' => $options['label'],
-				'icon' => $tax
-			];
-		}
-
-		$images = $this->extractImages();
-
-
+		$images = $this->extractImages($fields, $this->meta);
 		if (!empty($images)) {
 			$data['images'] = $images;
 		}
 
+
+		$taxonomies = $this->extractTerms($fields, $this->meta);
+		if (!empty($taxonomies)) {
+			$data['taxonomies'] = $taxonomies;
+		}
 		return $data;
 	}
 
-	protected function extractImages(array $fields = []): array
+	/**
+	 * @param WP_Term $post the post object
+	 *
+	 * @return array
+	 */
+	protected function prepareTerm(WP_Term $post, bool $fields = true): array
 	{
-		//Extract images
-		$images = [];
-		$get = [];
-		$fields = (empty($fields)) ? $this->fields : $fields;
-		foreach ($fields as $field => $config) {
-			if ($config['type'] === 'gallery' || $config['type'] === 'image' || $field === 'post_thumbnail') {
-				$get[] = $field;
-			}
+		$registrar = Registrar::getInstance($post->taxonomy);
+
+		$this->meta = Meta::forTerm($post->term_id);
+		$fields = ($fields) ? $this->meta->getAll() : [];
+		$data = [
+			'id' => $post->term_id,
+			'title' => $post->name,
+			'date' => $fields['created_date']??'',
+			'modified' => $fields['modified_date']??'',
+			'thumbnail' => '',
+			'icon' => $registrar->getIcon(),
+			'taxonomies' => [],
+			'fields' => $fields,
+			'images' => [],
+		];
+
+		$images = $this->extractImages($fields, $this->meta);
+		if (!empty($images)) {
+			$data['images'] = $images;
 		}
 
-		if (!empty($get)) {
-			$allImages = $this->meta->getAll($get);
-			foreach ($allImages as $k => $imgs) {
-				$temp = explode(',', $imgs);
-				foreach ($temp as $img) {
-					if (is_numeric($img) && !array_key_exists($img, $images)) {
-						$images[$img] = jvbImageData((int)$img);
-					}
-				}
-			}
+		$taxonomies = $this->extractTerms($fields, $this->meta);
+		if (!empty($taxonomies)) {
+			$data['taxonomies'] = $taxonomies;
 		}
-		return $images;
+
+		error_log('Term data: '.print_r($data, true));
+		return $data;
 	}
 
+
 	public function formatTimeline(WP_Post $post): array
 	{
-		$item = $this->prepareItem($post, true, false);
+		$item = $this->preparePost($post, true, false);
 		//Step 1: Get the fields that apply to all posts
-		$mainMeta = new MetaManager($post->ID, 'post');
+		$mainMeta = Meta::forPost($post->ID);
 		$item['fields'] = $mainMeta->getAll($this->timelineSharedFields);
 
 		//Step 2: Get the fields for each individual posts
@@ -1012,300 +604,17 @@
 		$subFields = [];
 		$images = [];
 		foreach ($children as $child) {
-			$meta = new MetaManager($child, 'post');
+			$meta = Meta::forPost($child);
 			$f = $meta->getAll($this->timelineUniqueFields);
 			$f = ['id' => $child] + $f;
 			$subFields[] = $f;
 
 			$images[$f['post_thumbnail']] = jvbImageData((int)$f['post_thumbnail']);
 		}
-		$item['fields']['timeline'] = $subFields;
+		$item['fields']['timeline_gallery'] = $subFields;
 		$item['images'] = $item['images'] + $images;
-		$item['number'] = $mainMeta->getValue('number');
+		$item['number'] = $mainMeta->get('number');
 
 		return $item;
 	}
-
-	/**
-	 * Builds the taxonomy query
-	 * @param array $taxonomies
-	 *
-	 * @return array|string[]
-	 */
-	protected function buildTaxQuery(array $taxonomies): array
-	{
-		$tax_query = [];
-		error_log('Taxonomies in query: ' . print_r($taxonomies, true));
-
-		foreach ($taxonomies as $taxonomy => $terms) {
-			if (!empty($terms)) {
-				$tax_query[] = [
-					'taxonomy' => jvbCheckBase($taxonomy),
-					'field' => 'term_id',
-					'terms' => array_map('absint', (array)$terms)
-				];
-			}
-		}
-
-
-		return count($tax_query) > 1
-			? array_merge(['relation' => 'AND'], $tax_query)
-			: $tax_query;
-	}
-
-	/**
-	 * Builds the date query
-	 * @param array $date_params
-	 *
-	 * @return array
-	 */
-	protected function buildDateQuery(array $date_params): array
-	{
-		$query = [];
-
-		if (!empty($date_params['after'])) {
-			$query['after'] = sanitize_text_field($date_params['after']);
-		}
-
-		if (!empty($date_params['before'])) {
-			$query['before'] = sanitize_text_field($date_params['before']);
-		}
-
-		if (isset($date_params['inclusive'])) {
-			$query['inclusive'] = (bool)$date_params['inclusive'];
-		}
-
-		return empty($query) ? [] : [$query];
-	}
-
-	/**
-	 * @param int $post_id
-	 *
-	 * @return bool
-	 */
-	protected function verifyOwnership(int $post_id): bool
-	{
-		$post = get_post($post_id);
-		return $post && $post->post_author == $this->user_id;
-	}
-
-	/**
-	 * Processes operation from Operation Queue
-	 * @param WP_Error|array $result
-	 * @param object $operation
-	 * @param array $data
-	 *
-	 * @return array|WP_Error
-	 */
-	public function processOperation(WP_Error|array $result, object $operation, array $data): array|WP_Error
-	{
-		if ($operation->type === 'batch_creation') {
-			$JVB = JVB();
-			$queue = $JVB->queue();
-
-			$images = $queue->getOperationValue($operation->id . '_upload', 'result') ?? false;
-
-			$this->user_id = $operation->user_id;
-			$this->post_type = BASE . $data['content'];
-			try {
-				$results = [];
-				if ($images) {
-					if ($data['mode'] == 'selection') {
-						$total = count($images);
-						foreach ($images as $group => $files) {
-							$settings = json_decode($data['files_data'][$group]);
-
-							switch ($settings->type) {
-								case 'group':
-									$featuredIndex = $settings->metadata->featuredFile ?? 0;
-									$title = $settings->metadata->title ?? $this->generatePostTitle($data['content']);
-									$new = wp_insert_post([
-										'post_type' => BASE . $data['content'],
-										'post_title' => $title,
-										'post_status' => 'draft',
-										'post_author' => $operation->user_id
-									]);
-									if ($new && !is_wp_error($new)) {
-										set_post_thumbnail($new, $files[$featuredIndex]['attachment_id']);
-										unset($files[$featuredIndex]);
-										if (!empty($files)) {
-											$meta = new MetaManager($new, 'post');
-											$IDs = array_column($files, 'attachment_id');
-											$meta->updateValue('gallery', implode(',', $IDs));
-										}
-										$results[] = $new;
-//                                        $queue->updateOperationProgress($operation->id, $group + 1, $total);
-									}
-									break;
-								default:
-									foreach ($files as $img) {
-										$new = wp_insert_post([
-											'post_type' => BASE . $data['content'],
-											'post_title' => $this->generatePostTitle($data['content']),
-											'post_status' => 'draft',
-											'post_author' => $operation->user_id
-										]);
-
-										if ($new && !is_wp_error($new)) {
-											set_post_thumbnail($new, $img['attachment_id']);
-											$results[] = $new;
-//                                            $queue->updateOperationProgress($operation->id, $group + 1, $total);
-										}
-									}
-									break;
-							}
-						}
-					} else {
-						$total = count($images);
-						foreach ($images as $key => $img) {
-							$new = wp_insert_post([
-								'post_type' => BASE . $data['content'],
-								'post_title' => $this->generatePostTitle($data['content']),
-								'post_status' => 'draft',
-								'post_author' => $operation->user_id
-							]);
-							if ($new && !is_wp_error($new)) {
-								set_post_thumbnail($new, $img['attachment_id']);
-							}
-							$results[] = $new;
-//                            $queue->updateOperationProgress($operation->id, $key + 1, $total);
-						}
-					}
-				}
-
-				return [
-					'success' => true,
-					'result' => $results
-				];
-			} catch (Exception $e) {
-				$JVB->error()->log(
-					'[ContentRoutes]:processOperation',
-					$e->getMessage()
-				);
-			}
-
-			return $results;
-		} elseif ($operation->type == 'content_update') {
-			$result = $this->processBatches($operation, $data);
-		}
-
-		return $result;
-	}
-	// Add to ContentRoutes.php
-
-	/**
-	 * One-time migration: Set latest_date meta for all timeline posts
-	 * Call this once via WP-CLI or a temporary admin page
-	 *
-	 * Usage: add_action('admin_init', function() {
-	 *     if (current_user_can('manage_options')) {
-	 *         JVB()->routes('content')->migrateTimelineLatestDates();
-	 *     }
-	 * });
-	 */
-	public function migrateTimelineLatestDates(): array
-	{
-		global $wpdb;
-
-		$results = [
-			'processed' => 0,
-			'updated' => 0,
-			'skipped' => 0,
-			'errors' => []
-		];
-
-		// Get all timeline post types
-		$timeline_types = [];
-		foreach (JVB_CONTENT as $type => $config) {
-			if (Features::forContent($type)->has('is_timeline')) {
-				$timeline_types[] = BASE . $type;
-			}
-		}
-
-		if (empty($timeline_types)) {
-			return $results;
-		}
-
-		// Get all parent timeline posts
-		$args = [
-			'post_type' => $timeline_types,
-			'post_status' => ['publish', 'draft'],
-			'post_parent' => 0,
-			'posts_per_page' => -1,
-			'fields' => 'ids'
-		];
-
-		$parent_ids = get_posts($args);
-
-		foreach ($parent_ids as $parent_id) {
-			$results['processed']++;
-
-			try {
-				// Get all children including the parent
-				$children = get_children([
-					'post_parent' => $parent_id,
-					'post_status' => ['publish', 'draft'],
-					'orderby' => 'menu_order',
-					'order' => 'ASC',
-					'fields' => 'ids'
-				]);
-
-				// Add parent to the list
-				array_unshift($children, $parent_id);
-
-				// Find latest date among all posts
-				$latest_timestamp = 0;
-
-				foreach ($children as $post_id) {
-					$date = get_post_meta($post_id, BASE . 'date', true);
-
-					if ($date) {
-						$timestamp = strtotime($date);
-						if ($timestamp > $latest_timestamp) {
-							$latest_timestamp = $timestamp;
-						}
-					}
-				}
-
-				// Update parent with latest date
-				if ($latest_timestamp > 0) {
-					update_post_meta($parent_id, BASE . 'latest_date', $latest_timestamp);
-					$results['updated']++;
-					error_log("Updated post {$parent_id} with latest_date: {$latest_timestamp}");
-				} else {
-					// Fallback to parent post's post_date
-					$parent_post = get_post($parent_id);
-					$fallback_timestamp = strtotime($parent_post->post_date);
-
-					if ($fallback_timestamp > 0) {
-						update_post_meta($parent_id, BASE . 'latest_date', $fallback_timestamp);
-						$results['updated']++;
-						error_log("Updated post {$parent_id} with fallback latest_date: {$fallback_timestamp} (from post_date)");
-					} else {
-						$results['skipped']++;
-						error_log("No dates found for post {$parent_id}");
-					}
-				}
-
-			} catch (Exception $e) {
-				$results['errors'][] = [
-					'post_id' => $parent_id,
-					'error' => $e->getMessage()
-				];
-			}
-		}
-
-		error_log('Timeline migration complete: ' . print_r($results, true));
-		return $results;
-	}
 }
-
-
-//add_action('init', function() {
-////	delete_option('jvb_timeline_migrated');
-//	if (get_option('jvb_timeline_migrated')) {
-//		return;
-//	}
-//	JVB()->routes('content')->migrateTimelineLatestDates();
-//	update_option('jvb_timeline_migrated', true);
-//});

--
Gitblit v1.10.0