From d38d825e3484d822ea3c1f0fb1df37ecf386b18a Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 04 Jan 2026 19:54:16 +0000
Subject: [PATCH] =TaxonomyCreator.js debugging

---
 inc/rest/routes/ContentRoutes.php |  452 ++++++++++++++++++++++++++++++++++++++++++++++++++-----
 1 files changed, 407 insertions(+), 45 deletions(-)

diff --git a/inc/rest/routes/ContentRoutes.php b/inc/rest/routes/ContentRoutes.php
index 6916093..3d455d5 100644
--- a/inc/rest/routes/ContentRoutes.php
+++ b/inc/rest/routes/ContentRoutes.php
@@ -5,6 +5,8 @@
 use JVBase\rest\RestRouteManager;
 use JVBase\managers\CacheManager;
 use JVBase\meta\MetaManager;
+use JVBase\utility\Features;
+use WP_Post;
 use WP_Query;
 use WP_Error;
 use WP_REST_Request;
@@ -23,14 +25,18 @@
     protected string $post_type = '';
     protected string $user_id = '';
 
+	//For Timeline-specific posts
+	protected array $timelineSharedFields = [];
+	protected array $timelineUniqueFields = [];
+
     //TODO: Ensure we are handling the bulk operations for all processes
-    //TODO: be sure to clear cache ($this->>cache->invalidateGroup($this->>cache_name)) on content update/create
     //TODO: Also invalidate feed caches on updates!!
 
     public function __construct()
     {
         $this->cache_name = 'user_content_'.get_current_user_id();
         parent::__construct();
+		$this->cache->clear();
         $this->action = 'dash-';
         $this->operation_type = 'content_update';
         add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
@@ -73,6 +79,59 @@
         ]);
     }
 
+	protected function initTimelineFields(string $content):void
+	{
+		$content = jvbNoBase($content);
+		if (!Features::forContent($content)->has('is_timeline')){
+			return;
+		}
+		$config = Features::getConfig($content);
+		$this->fields = $config['fields'];
+
+		$this->timelineSharedFields = $this->getTimelineSharedFields($content);
+		array_unshift($this->timelineSharedFields, 'post_thumbnail');
+		array_unshift($this->timelineSharedFields, 'post_title');
+		array_unshift($this->timelineSharedFields, 'post_status');
+
+		$this->timelineUniqueFields = $this->getTimelineUniqueFields($content);
+	}
+	public function getTimelineUniqueFields(string $content):array
+	{
+		$content = jvbNoBase($content);
+		if (!Features::forContent($content)->has('is_timeline')){
+			return [];
+		}
+		$config = Features::getConfig($content);
+		$allFields = $config['fields'];
+
+		return array_keys(array_filter($allFields, function ($field) {
+			if (array_key_exists('for_all', $field) && $field['for_all'] === true) {
+				return true;
+			}
+			return false;
+		}));
+	}
+
+	public function getTimelineSharedFields(string $content):array
+	{
+		$content = jvbNoBase($content);
+		if (!Features::forContent($content)->has('is_timeline')){
+			return [];
+		}
+		$config = Features::getConfig($content);
+		if (!$config || empty($config)) {
+			return [];
+		}
+		$allFields = $config['fields']??[];
+
+		return array_keys(array_filter($allFields, function ($field) {
+			if (!array_key_exists('for_all', $field) || $field['for_all'] === false){
+				return true;
+			}
+			return false;
+		}));
+	}
+
     /**
      * Handle content update/creation
      * @param WP_REST_Request $request
@@ -166,9 +225,6 @@
     public function handleContentRequest(WP_REST_Request $request):WP_REST_Response
     {
         $params = $request->get_params();
-		error_log('handleContentRequest params: '.print_r($params, true));
-
-		error_log('Fetching content. Params: '.print_r($params, true));
         $user_id = $params['user'];
         if (!$this->userCheck($user_id)) {
             return new WP_REST_Response([
@@ -185,9 +241,6 @@
         }
         $post_type = str_replace('-', '_',jvbCheckBase($params['content']));
 
-		$config = (array_key_exists($params['content'], JVB_CONTENT) && !empty(JVB_CONTENT[$params['content']])) ? JVB_CONTENT[$params['content']] : [];
-
-
 
         // Build query args
         $args = [
@@ -199,11 +252,25 @@
             'author' => $user_id,
             'post_status' => $post_status
         ];
+		//Only top level posts for timeline types
+		if (Features::forContent($post_type)->has('is_timeline')) {
+			$args['post_parent'] = 0;
+		}
 
 		//Calendar filters
-		if (jvbCheck('is_calendar', $config))  {
+		if (Features::forContent($post_type)->has('is_calendar'))  {
 			$args = $this->applyCalendarFilters($args, $params);
 		}
+		$taxonomies = array_filter($params, function($param) {
+			return str_starts_with($param, 'tax_');
+		}, ARRAY_FILTER_USE_KEY);
+		if (!empty($taxonomies)) {
+			$params['taxonomies'] = [];
+			foreach ($taxonomies as $taxonomy => $terms) {
+				$taxonomy = str_replace('tax_', '', $taxonomy);
+				$params['taxonomies'][$taxonomy] = $terms;
+			}
+		}
 		if (array_key_exists('taxonomies', $params)) {
 			$args = $this->applyTaxonomyFilters($args, $params);
 		}
@@ -218,27 +285,21 @@
 			$args['s'] = sanitize_text_field($params['search']);
 		}
 
-		error_log('Content Routes final args: '.print_r($args, true));
-
         $key = $this->cache->generateKey($args);
-		$lastModified = $this->cache->getTimestamp($key);
-		if ($lastModified !== false) {
-			$headerCheck = $this->ifModifiedSince($lastModified, $args, $request);
-			if (!is_null($headerCheck)) {
-				return $headerCheck;
-			}
-		} else {
-			// No timestamp yet, but we can still set ETag
-			$etag = '"' . md5(serialize($args)) . '"';
-			header('ETag: ' . $etag);
-			header('Cache-Control: private, max-age=30');
+		// Check HTTP cache headers with the specific content type
+		$content_type = $params['content'] ?? $params['type'];
+		$cache_check = $this->checkHeaders($request, $content_type, [
+			'filter_hash' => $key,
+		]);
+		if ($cache_check) {
+			return $cache_check;
 		}
 
 
         $cache = $this->cache->get($key);
-		$cache = false;
         if ($cache) {
-            return new WP_REST_Response($cache);
+            $response = new WP_REST_Response($cache);
+			return $this->addCacheHeaders($response);
         }
 
         // Run query
@@ -260,7 +321,8 @@
 
         $this->cache->set($key, $data);
 
-        return new WP_REST_Response($data);
+        $response = new WP_REST_Response($data);
+		return $this->addCacheHeaders($response);
     }
 
     /**
@@ -306,6 +368,23 @@
         $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([
@@ -379,21 +458,18 @@
 				error_log('Allowed Fields: '.print_r($allowedFields, true));
 				$meta = new MetaManager($ID, 'post');
 				$success = $meta->setAll($allowedFields);
-				error_log('Should be set?');
 				$results[$ID] = [
 					'success'	=> $success
 				];
 
 			}
 
-            CacheManager::invalidateGroup($post_data['content']);
+            CacheManager::for($post_data['content'])->clear();
 			if (jvbSiteUsesFeedBlock()) {
-				CacheManager::invalidateGroup($post_data['feed']);
+				CacheManager::for('feed')->clear();
 			}
         }
 
-
-        CacheManager::invalidateGroup('user_content');
 		if (jvbSiteHasNotifications()) {
 			$this->notifications = JVB()->notification();
 			$this->notifications->addNotification(
@@ -411,6 +487,255 @@
         ];
     }
 
+	/**
+	 * 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'];
+		}
+
+		$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;
+
+			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);
+				error_log('Setting values for '.$timeline['id'].': '.print_r($updateValues, true));
+
+				$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->clear();
+			CacheManager::onPostSave($parent_id, $parent_post);
+		}
+
+
+		return ['success' => true, 'data' => [
+			'success' => $success,
+			'errors' => $errors
+		]];
+	}
+
     /**
      * Handle batch content creation from uploads
      * @param WP_REST_Request $request
@@ -485,15 +810,20 @@
     }
 
     /**
-     * @param object $post the wordpress post object
+     * @param WP_Post $post the wordpress post object
      *
      * @return array
      */
-    protected function prepareItem(object $post):array
+    protected function prepareItem(WP_Post $post, bool $skip = false, bool $fields = true):array
     {
+		if (!$skip && Features::forContent($post->post_type)->has('is_timeline')) {
+			$this->initTimelineFields($post->post_type);
+			return $this->formatTimeline($post);
+		}
         $this->meta = new MetaManager($post->ID, 'post');
         $data = [
             'id'        => $post->ID,
+			'title'		=> $post->post_title,
             'status'    => $post->post_status,
             'date'      => $post->post_date,
             'modified'  => $post->post_modified,
@@ -501,7 +831,7 @@
             'alt'       => get_post_meta(get_post_thumbnail_id(), '_wp_attachment_image_alt', true),
             'icon'      => $this->post_type,
             'taxonomies'=> [],
-            'fields'    => $this->meta->getAll(),
+            'fields'    => ($fields) ? $this->meta->getAll() : [],
 			'images'	=> [],
         ];
 
@@ -520,15 +850,26 @@
             ];
         }
 
+		$images = $this->extractImages();
 
-        //Extract images
+
+        if (!empty($images)) {
+            $data['images'] = $images;
+        }
+
+        return $data;
+    }
+	protected function extractImages(array $fields = []):array
+	{
+		//Extract images
 		$images = [];
 		$get = [];
-        foreach ($this->fields as $field => $config) {
-            if ($config['type'] === 'gallery' || $config['type'] === 'image' || $field === 'post_thumbnail') {
+		$fields = (empty($fields)) ? $this->fields : $fields;
+		foreach ($fields as $field => $config) {
+			if ($config['type'] === 'gallery' || $config['type'] === 'image' || $field === 'post_thumbnail') {
 				$get[] = $field;
-            }
-        }
+			}
+		}
 
 		if (!empty($get)) {
 			$allImages = $this->meta->getAll($get);
@@ -541,13 +882,35 @@
 				}
 			}
 		}
+		return $images;
+	}
 
-        if (!empty($images)) {
-            $data['images'] = $images;
-        }
+	public function formatTimeline(WP_Post $post):array
+	{
+		$item = $this->prepareItem($post, true, false);
+		//Step 1: Get the fields that apply to all posts
+		$mainMeta = new MetaManager($post->ID, 'post');
+		$item['fields'] = $mainMeta->getAll($this->timelineSharedFields);
 
-        return $data;
-    }
+		//Step 2: Get the fields for each individual posts
+		$children = get_children(['post_parent' => $post->ID, 'orderby' => 'date', 'order' => 'ASC', 'post_status' => ['publish', 'draft'], 'fields'=> 'ids']);
+		array_unshift($children, $post->ID);
+
+		$subFields = [];
+		$images = [];
+		foreach ($children as $child) {
+			$meta = new MetaManager($child, 'post');
+			$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['images'] = $item['images'] + $images;
+
+		return $item;
+	}
 
     /**
      * Builds the taxonomy query
@@ -696,9 +1059,8 @@
                     }
 
                     //Clear cache
-                    CacheManager::invalidateGroup($data['content']);
-                    CacheManager::invalidateGroup('feed');
-                    CacheManager::invalidateGroup('user_content');
+					CacheManager::for($data['content'])->clear();
+                    CacheManager::for('feed')->clear();
                 }
 
 				return [

--
Gitblit v1.10.0