From e9967fa22781d922ba4eb8fb44fe72d200ac4b14 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Mon, 10 Nov 2025 21:04:10 +0000
Subject: [PATCH] =IconsManager.php update

---
 inc/rest/routes/ContentRoutes.php |  264 +++++++++++++++++++++++++++++++++++++++++++++-------
 1 files changed, 226 insertions(+), 38 deletions(-)

diff --git a/inc/rest/routes/ContentRoutes.php b/inc/rest/routes/ContentRoutes.php
index 6916093..dfa9cb6 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->action = 'dash-';
         $this->operation_type = 'content_update';
         add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
@@ -73,6 +79,32 @@
         ]);
     }
 
+	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 = array_keys(array_filter($this->fields, function ($field) {
+			if (!array_key_exists('for_all', $field) || $field['for_all'] === false){
+				return true;
+			}
+			return false;
+		}));
+		array_unshift($this->timelineSharedFields, 'post_thumbnail');
+		array_unshift($this->timelineSharedFields, 'post_title');
+
+		$this->timelineUniqueFields = array_keys(array_filter($this->fields, function ($field) {
+			if (array_key_exists('for_all', $field) && $field['for_all'] === true) {
+				return true;
+			}
+			return false;
+		}));
+	}
+
     /**
      * Handle content update/creation
      * @param WP_REST_Request $request
@@ -185,9 +217,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,9 +228,13 @@
             '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);
 		}
 		if (array_key_exists('taxonomies', $params)) {
@@ -218,27 +251,26 @@
 			$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 +292,8 @@
 
         $this->cache->set($key, $data);
 
-        return new WP_REST_Response($data);
+        $response = new WP_REST_Response($data);
+		return $this->addCacheHeaders($response);
     }
 
     /**
@@ -306,6 +339,10 @@
         $results = [];
 
         foreach ($posts as $ID => $post_data) {
+			if (Features::forContent($post_data['content'])->has('is_timeline')) {
+				$results[$ID] =$this->processTimelinePost($ID, $post_data);
+				continue;
+			}
 			if (str_starts_with($ID, 'new')) {
 
 				error_log('New post detected. Creating... with: '.print_r([
@@ -386,14 +423,12 @@
 
 			}
 
-            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 +446,123 @@
         ];
     }
 
+	/**
+	 * 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'];
+		}
+
+		$this->fields = jvbGetFields($post_data['content']);
+		$this->initTimelineFields($post_data['content']);
+		error_log('Received Data: '.print_r($post_data, true));
+
+		// First, process the main fields that will apply to all posts
+		$sharedData = array_filter($post_data, function ($key) {
+			return in_array($key, $this->timelineSharedFields);
+		}, ARRAY_FILTER_USE_KEY);
+
+		//Next, process any individual posts, including any menu order changes
+		if (array_key_exists('timeline', $post_data) && is_array($post_data['timeline'])) {
+			$sharedTaxonomies = $sharedData;
+			unset($sharedTaxonomies['post_title']);
+
+			//Ensure the parent post exists and is still first in the array
+			$index = array_search((string)$parent_id, array_column($post_data['timeline'], 'id'));
+			error_log('Found index: '.print_r($index, true));
+			if ($index === false) {
+				return [
+					'success' => false,
+					'message'	=> 'Missing parent id. This should not have happened'
+				];
+			} elseif ($index !== 0) {
+				// Move that element to the start of the array
+				$item = $post_data['timeline'][$index];
+				unset($post_data['timeline'][$index]);
+				array_unshift($post_data['timeline'], $item);
+			}
+			$errors = [];
+			$success = [];
+			// Get existing children to track deletions
+			$existing_children = get_children([
+				'post_parent' => $parent_id,
+				'post_type' => jvbCheckBase($post_data['content']),
+				'fields' => 'ids'
+			]);
+			//Iterate through the timeline posts
+			foreach($post_data['timeline'] as $order => $timeline) {
+				$allowedFields = array_filter($timeline, function($key) {
+					return in_array($key, $this->timelineUniqueFields);
+				}, ARRAY_FILTER_USE_KEY);
+
+				$allowedFields['post_title'] = $allowedFields['post_title'] ?? $sharedData['post_title'].' - Treatment #'.$order;
+				$allowedFields = array_merge($allowedFields, $sharedTaxonomies);
+				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
+					]);
+					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)]);
+				}
+
+				//Determine which fields to update
+				$meta = new MetaManager($timeline['id'], 'post');
+				$oldValues = $meta->getAll(array_keys($allowedFields));
+				$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);
+				//Update Menu Order, if applicable
+				if ((int) $timeline['id'] !== $parent_id) {
+					$post = get_post((int) $timeline['id']);
+					if ($post && $post->menu_order !== $order) {
+						$updated = wp_update_post([
+							'ID'			=> $post->ID,
+							'menu_order'	=> $order,
+						]);
+						if (!$updated || is_wp_error($updated)) {
+							$errors[] = [
+								'message'	=> 'Could not update timeline order for post',
+								'data'		=> $timeline
+							];
+						}
+					}
+				}
+				$success[] = $timeline['id'];
+			}
+		}
+		//Delete any remaining children that no longer exist
+		if (!empty($existing_children)) {
+			foreach ($existing_children as $ID) {
+				wp_delete_post($ID);
+			}
+		}
+
+		return ['success' => true, 'data' => [
+			'success'	=> $success,
+			'errors'	=> $errors
+		]];
+	}
+
     /**
      * Handle batch content creation from uploads
      * @param WP_REST_Request $request
@@ -485,12 +637,16 @@
     }
 
     /**
-     * @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,
@@ -501,7 +657,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 +676,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 +708,35 @@
 				}
 			}
 		}
+		return $images;
+	}
 
-        if (!empty($images)) {
-            $data['images'] = $images;
-        }
+	protected 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' => 'menu_order', '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['post_thumbnail']] = $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 +885,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