From 3d25ad7de4fdbb3eba2cde2a62422e6be5d79a0a Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 25 Aug 2026 22:57:22 +0000
Subject: [PATCH] =It's been a minute. I went through the Routes and associated Managers and ensured they played together well. Reworked the Cache.php, and I think I caught all the references. Moving away from JVBase() root class, to individuall classes that are Instanced. Double checked for the ABSPATH check in every file, and added index.php to every directory that didn't already have it. Also still working on the MenuBlock to work with WooCommerce for now for Northeh

---
 inc/managers/queue/executors/ContentExecutor.php |  955 +++++++++++++++++++++++++++++++----------------------------
 1 files changed, 505 insertions(+), 450 deletions(-)

diff --git a/inc/managers/queue/executors/ContentExecutor.php b/inc/managers/queue/executors/ContentExecutor.php
index c53f4a7..2755eb9 100644
--- a/inc/managers/queue/executors/ContentExecutor.php
+++ b/inc/managers/queue/executors/ContentExecutor.php
@@ -1,10 +1,9 @@
 <?php
 namespace JVBase\managers\queue\executors;
 
-use JVBase\managers\CacheManager;
-use JVBase\managers\queue\{Executor, Operation, Progress, Result};
-use JVBase\meta\MetaManager;
-use JVBase\utility\Features;
+use JVBase\managers\queue\{Executor, Operation, Progress, Result, Storage};
+use JVBase\meta\Meta;
+use JVBase\registrar\Registrar;
 use Exception;
 
 if (!defined('ABSPATH')) {
@@ -19,34 +18,30 @@
 {
 	private const HANDLED_TYPES = [
 		'content_update',
-		'batch_creation',
+//		'batch_creation',
 	];
 
 	private int $userId;
 	private string $postType;
 	private array $fields = [];
-	private array $timelineSharedFields = [];
-	private array $timelineUniqueFields = [];
 
 	public function execute(Operation $operation, Progress $progress): Result
 	{
+		$this->userId = $operation->userId;
+
 		if (!in_array($operation->type, self::HANDLED_TYPES)) {
 			throw new Exception("ContentExecutor cannot handle type: {$operation->type}");
 		}
-
-		$this->userId = $operation->userId;
+		error_log('Executing ContentExecutor.php');
 
 		try {
 			$data = $operation->requestData;
 
-			$result = match($operation->type) {
+			return match($operation->type) {
 				'content_update'  => $this->processContentUpdate($operation, $data, $progress),
-				'batch_creation'  => $this->processBatchCreation($operation, $data, $progress),
 				default           => throw new Exception("Unknown type: {$operation->type}")
 			};
 
-			return $result;
-
 		} catch (Exception $e) {
 			JVB()->error()->log(
 				'[ContentExecutor]:execute',
@@ -72,84 +67,71 @@
 	private function processContentUpdate(Operation $operation, array $data, Progress $progress): Result
 	{
 		$posts = $data['posts'] ?? [];
-
+		error_log('Processing Content Update: '.print_r($posts, true));
 		if (empty($posts)) {
 			return new Result(
-				outcome: 'failed',
+				outcome: 'success',
 				result: ['message' => 'No posts to update']
 			);
 		}
 
-		$results = [];
+		$results = [
+			'errors'	=> [],
+			'success'	=> [],
+			'newPosts'	=> [],
+			'timelineParents'	=> [],
+			'timelineStatus'	=> [],
+			'timelineSharedFields'	=> [],
+		];
 		$errors = [];
 
 		foreach ($posts as $id => $postData) {
 			try {
-				$content = $postData['content'] ?? '';
-
-				// Timeline posts
-				if (Features::forContent($content)->has('is_timeline') && isset($postData['timeline'])) {
-					$parentId = (int)$id;
-					if ($parentId === 0) {
-						$progress->failItem($id, 'Invalid parent post ID for timeline');
-						continue;
-					}
-					$results[$id] = $this->processTimelinePost($parentId, $postData);
-					$progress->advance(1);
-					continue;
+				$content = $postData['content'] ?? false;
+				if (!$content) continue;
+				$registrar = Registrar::getInstance($content);
+				switch ($registrar->getType()) {
+					case 'post':
+						$results = $this->handlePost($id, $postData, $registrar, $results, $progress);
+						break;
+					case 'term':
+						$results = $this->handleTerm($id, $postData, $registrar, $results, $progress);
+						break;
+					case 'user':
+						$results = $this->handleUser($id, $postData, $registrar, $results, $progress);
+						break;
 				}
-
-				// New post creation
-				if (str_starts_with((string)$id, 'new')) {
-					$newId = wp_insert_post([
-						'post_author' => $this->userId,
-						'post_type'   => jvbCheckBase($content),
-						'post_title'  => $postData['post_title'] ?? '',
-						'post_status' => $postData['status'] ?? 'draft',
-					]);
-
-					if (!$newId || is_wp_error($newId)) {
-						$progress->failItem($id, 'Could not create post');
-						continue;
-					}
-
-					$this->savePostFields($newId, $postData);
-					$results[$id] = ['success' => true, 'new_id' => $newId];
-					$progress->advance(1);
-					continue;
-				}
-
-				// Existing post update
-				if (!$this->verifyOwnership((int)$id)) {
-					$progress->failItem($id, 'No permission to modify this post');
-					continue;
-				}
-
-				$this->processPostUpdate((int)$id, $postData);
-				$results[$id] = ['success' => true];
-				$progress->advance(1);
-
-				// Clear caches
-				CacheManager::for($content)->clear();
-				if (jvbSiteUsesFeedBlock()) {
-					CacheManager::for('feed')->clear();
-				}
-
 			} catch (Exception $e) {
 				$progress->failItem($id, $e->getMessage());
-				$errors[$id] = $e->getMessage();
+				$results['errors'][$id] = $e->getMessage();
 			}
 		}
+		error_log('Final Results: '.print_r($results, true));
+
+		try {
+			if (!empty($results['timelineSharedFields'])) {
+				$this->checkSharedFields($results['timelineSharedFields']);
+			}
+			if (!empty($results['timelineStatus'])) {
+				$this->handleTimelineStatusChange($results['timelineStatus']);
+			}
+			if (!empty($results['timelineParents'])) {
+				$this->maybeReorderTimelines($results['timelineParents']);
+			}
+		} catch (Exception $e) {
+			$results['errors'][] = $e->getMessage();
+		}
+
 
 		// Send notification
-		if (jvbSiteHasNotifications()) {
-			JVB()->notification()->addNotification(
-				$this->userId,
-				'content_update_complete',
-				null,
-				'Content updates completed!'
-			);
-		}
+//		if (jvbSiteHasNotifications()) {
+//			NotificationManager::getInstance()->addNotification(
+//				$this->userId,
+//				'content_update_complete',
+//				null,
+//				'Content updates completed!'
+//			);
+//		}
 
 		$outcome = 'success';
 		if (!empty($errors)) {
@@ -158,423 +140,496 @@
 
 		return new Result(
 			outcome: $outcome,
-			result: $results
+			result: $results,
 		);
 	}
 
-	private function processPostUpdate(int $postId, array $postData): void
-	{
-		$content = $postData['content'] ?? '';
-
-		// Handle status changes
-		if (isset($postData['post_status'])) {
-			switch ($postData['post_status']) {
-				case 'publish':
-					if (user_can($this->userId, 'manage_options') || user_can($this->userId, 'skip_moderation')) {
-						wp_update_post(['ID' => $postId, 'post_status' => 'publish']);
-					}
-					unset($postData['post_status']);
-					break;
-				case 'draft':
-					wp_update_post(['ID' => $postId, 'post_status' => 'draft']);
-					break;
-				case 'trash':
-					wp_trash_post($postId);
-					return;
-				case 'delete':
-					wp_delete_post($postId, true);
-					return;
-			}
-		}
-
-		$this->savePostFields($postId, $postData);
-	}
-
 	private function savePostFields(int $postId, array $postData): bool
 	{
 		$content = $postData['content'] ?? '';
-		$fields = jvbGetFields($content);
+		$fields = Registrar::getFieldsFor($content);
 
 		$allowedFields = array_filter($postData, function ($key) use ($fields) {
 			return array_key_exists($key, $fields);
 		}, ARRAY_FILTER_USE_KEY);
 
+		//Remove values that are already saved
+		$check = Meta::forPost($postId)->getAll(array_keys($allowedFields));
+		error_log('Stored values: '.print_r($check, true));
+		$allowedFields = array_filter($allowedFields, function ($key) use ($allowedFields, $check) {
+			return $allowedFields[$key] !== $check[$key];
+		}, ARRAY_FILTER_USE_KEY);
+
 		if (empty($allowedFields)) {
 			return true;
 		}
 
-		$meta = new MetaManager($postId, 'post');
-		return $meta->setAll($allowedFields);
+		return Meta::forPost($postId)
+			->setAll($allowedFields);
 	}
-
-	// ─────────────────────────────────────────────────────────────
-	// Batch Creation
-	// ─────────────────────────────────────────────────────────────
-
-	private function processBatchCreation(Operation $operation, array $data, Progress $progress): Result
+	private function saveTermFields(int $termId, array $data): bool
 	{
-		$this->postType = BASE . $data['content'];
+		$content = $data['content'] ?? '';
+		error_log('Saving term fields: '.print_r($data, true));
+		$fields = Registrar::getFieldsFor($content);
 
-		// Get upload results from dependency
-		$uploadOpId = $operation->id . '_upload';
-		$images = JVB()->queue()->get($uploadOpId)?->result ?? null;
-
-		if (!$images) {
-			return new Result(
-				outcome: 'failed',
-				result: ['message' => 'No upload results found']
-			);
-		}
-
-		$results = [];
-
-		if ($data['mode'] === 'selection') {
-			$results = $this->createFromSelection($operation, $data, $images, $progress);
-		} else {
-			$results = $this->createFromDirect($operation, $data, $images, $progress);
-		}
-
-		// Clear caches
-		CacheManager::for($data['content'])->clear();
-		CacheManager::for('feed')->clear();
-
-		return new Result(
-			outcome: !empty($results) ? 'success' : 'failed',
-			result: $results
-		);
-	}
-
-	private function createFromSelection(Operation $operation, array $data, array $images, Progress $progress): array
-	{
-		$results = [];
-
-		foreach ($images as $group => $files) {
-			$settings = json_decode($data['files_data'][$group] ?? '{}');
-
-			if (($settings->type ?? '') === 'group') {
-				$postId = $this->createGroupPost($operation, $data, $files, $settings);
-			} else {
-				$postId = $this->createIndividualPosts($operation, $data, $files);
-			}
-
-			if ($postId) {
-				$results = array_merge($results, (array)$postId);
-			}
-
-			$progress->advance(1);
-		}
-
-		return $results;
-	}
-
-	private function createFromDirect(Operation $operation, array $data, array $images, Progress $progress): array
-	{
-		$results = [];
-
-		foreach ($images as $img) {
-			$postId = wp_insert_post([
-				'post_type'   => $this->postType,
-				'post_title'  => $this->generatePostTitle($data['content']),
-				'post_status' => 'draft',
-				'post_author' => $operation->userId,
-			]);
-
-			if ($postId && !is_wp_error($postId)) {
-				set_post_thumbnail($postId, $img['attachment_id']);
-				$results[] = $postId;
-			}
-
-			$progress->advance(1);
-		}
-
-		return $results;
-	}
-
-	private function createGroupPost(Operation $operation, array $data, array $files, object $settings): ?int
-	{
-		$featuredIndex = $settings->metadata->featuredFile ?? 0;
-		$title = $settings->metadata->title ?? $this->generatePostTitle($data['content']);
-
-		$postId = wp_insert_post([
-			'post_type'   => $this->postType,
-			'post_title'  => $title,
-			'post_status' => 'draft',
-			'post_author' => $operation->userId,
-		]);
-
-		if (!$postId || is_wp_error($postId)) {
-			return null;
-		}
-
-		// Set featured image
-		set_post_thumbnail($postId, $files[$featuredIndex]['attachment_id']);
-
-		// Remaining files go to gallery
-		unset($files[$featuredIndex]);
-		if (!empty($files)) {
-			$meta = new MetaManager($postId, 'post');
-			$ids = array_column($files, 'attachment_id');
-			$meta->updateValue('gallery', implode(',', $ids));
-		}
-
-		return $postId;
-	}
-
-	private function createIndividualPosts(Operation $operation, array $data, array $files): array
-	{
-		$results = [];
-
-		foreach ($files as $img) {
-			$postId = wp_insert_post([
-				'post_type'   => $this->postType,
-				'post_title'  => $this->generatePostTitle($data['content']),
-				'post_status' => 'draft',
-				'post_author' => $operation->userId,
-			]);
-
-			if ($postId && !is_wp_error($postId)) {
-				set_post_thumbnail($postId, $img['attachment_id']);
-				$results[] = $postId;
-			}
-		}
-
-		return $results;
-	}
-
-	// ─────────────────────────────────────────────────────────────
-	// Timeline Processing
-	// ─────────────────────────────────────────────────────────────
-
-	private function processTimelinePost(int $parentId, array $postData): array
-	{
-		if (!$this->verifyOwnership($parentId)) {
-			return ['success' => false, 'message' => 'No permission'];
-		}
-
-		$content = $postData['content'];
-		$this->initTimelineFields($content);
-
-		$parentPost = get_post($parentId);
-		$parentIsPublished = ($parentPost->post_status === 'publish');
-
-		// Extract shared data (excluding post_thumbnail)
-		$sharedData = array_filter($postData, function ($key) {
-			return in_array($key, $this->timelineSharedFields)
-				&& !in_array($key, ['content', 'user'])
-				&& $key !== 'post_thumbnail';
+		$allowedFields = array_filter($data, function ($key) use ($fields) {
+			return array_key_exists($key, $fields);
 		}, ARRAY_FILTER_USE_KEY);
 
-		if (!isset($sharedData['post_title']) && isset($postData['timeline'][0]['post_title'])) {
-			$sharedData['post_title'] = $postData['timeline'][0]['post_title'];
+
+		//Remove values that are already saved
+		$check = Meta::forTerm($termId)->getAll(array_keys($allowedFields));
+		error_log('Stored values: '.print_r($check, true));
+		$allowedFields = array_filter($allowedFields, function ($value, $key) use ($check) {
+			error_log('Sent value: '.print_r($value, true));
+			error_log('Stored Value: '.print_r($check[$key], true));
+			return $value !== $check[$key];
+		}, ARRAY_FILTER_USE_BOTH);
+
+		if (empty($allowedFields)) {
+			return true;
 		}
 
-		if (!isset($postData['timeline']) || !is_array($postData['timeline'])) {
-			return ['success' => false, 'message' => 'No timeline data'];
-		}
+		error_log('Allowed fields: '.print_r($allowedFields, true));
 
-		// Validate parent is in timeline
-		$index = array_search((string)$parentId, array_column($postData['timeline'], 'id'));
-		if ($index === false) {
-			return ['success' => false, 'message' => 'Missing parent id'];
-		}
-
-		// Handle parent reordering if needed
-		if ($index !== 0) {
-			$parentId = $this->reorderTimelineParent($parentId, $postData['timeline'], $index);
-			$parentPost = get_post($parentId);
-			$parentIsPublished = ($parentPost->post_status === 'publish');
-		}
-
-		// Shared taxonomies (excluding title and thumbnail)
-		$sharedTaxonomies = array_filter($sharedData, function ($key) {
-			return !in_array($key, ['post_title', 'post_thumbnail']);
-		}, ARRAY_FILTER_USE_KEY);
-
-		$existingChildren = get_children([
-			'post_parent'  => $parentId,
-			'orderby'      => 'menu_order',
-			'post_status'  => ['publish', 'draft'],
-			'fields'       => 'ids',
-		]);
-
-		$errors = [];
-		$success = [];
-
-		foreach ($postData['timeline'] as $order => $timeline) {
-			$result = $this->processTimelineEntry(
-				$timeline,
-				$order,
-				$parentId,
-				$parentIsPublished,
-				$sharedTaxonomies,
-				$existingChildren,
-				$content
-			);
-
-			if ($result['success']) {
-				$success[] = $result;
-				if (isset($result['child_id']) && in_array($result['child_id'], $existingChildren)) {
-					unset($existingChildren[array_search($result['child_id'], $existingChildren)]);
-				}
-			} else {
-				$errors[] = $result;
-			}
-		}
-
-		// Trash orphaned children
-		foreach ($existingChildren as $orphanId) {
-			wp_trash_post($orphanId);
-		}
-
-		return [
-			'success' => empty($errors),
-			'updated' => count($success),
-			'errors'  => $errors,
-		];
+		return Meta::forTerm($termId)
+			->setAll($allowedFields);
 	}
-
-	private function processTimelineEntry(
-		array $timeline,
-		int $order,
-		int $parentId,
-		bool $parentIsPublished,
-		array $sharedTaxonomies,
-		array &$existingChildren,
-		string $content
-	): array {
-		$isParent = ((int)($timeline['id'] ?? 0) === $parentId);
-
-		// Get unique fields for this entry
-		$allowedFields = array_filter($timeline, function ($key) {
-			return in_array($key, $this->timelineUniqueFields) && !in_array($key, ['content', 'user']);
-		}, ARRAY_FILTER_USE_KEY);
-
-		// Determine title
-		$providedTitle = $timeline['post_title'] ?? '';
-		$autoPattern = '/^.+Treatment #?\d+$/';
-
-		if ($isParent) {
-			$allowedFields['post_title'] = $providedTitle ?: ($sharedTaxonomies['post_title'] ?? get_post($parentId)->post_title);
-		} else {
-			if (empty($providedTitle) || preg_match($autoPattern, $providedTitle)) {
-				$allowedFields['post_title'] = 'Treatment ' . $order;
-			} else {
-				$allowedFields['post_title'] = $providedTitle;
-			}
-		}
-
-		$allowedFields = array_merge($sharedTaxonomies, $allowedFields);
-
-		// Create child if needed
-		$childId = $timeline['id'] ?? null;
-		if (!$childId || !is_numeric($childId)) {
-			$childId = wp_insert_post([
-				'post_author' => $this->userId,
-				'post_type'   => jvbCheckBase($content),
-				'post_title'  => $allowedFields['post_title'],
-				'post_parent' => $parentId,
-				'menu_order'  => $order,
-				'post_status' => $parentIsPublished ? 'publish' : 'draft',
-			]);
-
-			if (!$childId || is_wp_error($childId)) {
-				return ['success' => false, 'message' => 'Could not create child post'];
-			}
-		}
-
-		// Update post
-		$postUpdates = ['ID' => $childId];
-		if (!$isParent) {
-			$postUpdates['menu_order'] = $order;
-			if ($parentIsPublished) {
-				$currentPost = get_post($childId);
-				if ($currentPost && $currentPost->post_status !== 'publish') {
-					$postUpdates['post_status'] = 'publish';
-				}
-			}
-		}
-
-		if (isset($allowedFields['post_title'])) {
-			$postUpdates['post_title'] = $allowedFields['post_title'];
-			unset($allowedFields['post_title']);
-		}
-
-		wp_update_post($postUpdates);
-
-		// Save meta fields
-		if (!empty($allowedFields)) {
-			$meta = new MetaManager($childId, 'post');
-			$meta->setAll($allowedFields);
-		}
-
-		return ['success' => true, 'child_id' => $childId];
-	}
-
-	private function reorderTimelineParent(int $currentParentId, array $timeline, int $currentIndex): int
+	private function saveUserFields(int $userId, array $data): bool
 	{
-		$newParentId = $timeline[0]['id'] ?? null;
+		$content = $data['content'] ?? '';
+		$fields = Registrar::getFieldsFor($content);
 
-		if (!is_numeric($newParentId) || (int)$newParentId <= 0) {
-			return $currentParentId;
+		$allowedFields = array_filter($data, function ($key) use ($fields) {
+			return array_key_exists($key, $fields);
+		}, ARRAY_FILTER_USE_KEY);
+
+		//Remove values that are already saved
+		$check = Meta::forUser($userId)->getAll(array_keys($allowedFields));
+		$allowedFields = array_filter($allowedFields, function ($key) use ($allowedFields, $check) {
+			return $allowedFields[$key] !== $check[$key];
+		}, ARRAY_FILTER_USE_KEY);
+
+		if (empty($allowedFields)) {
+			return true;
 		}
 
-		$newParentId = (int)$newParentId;
-
-		// Make new parent a top-level post
-		wp_update_post(['ID' => $newParentId, 'post_parent' => 0]);
-
-		// Make old parent a child
-		wp_update_post(['ID' => $currentParentId, 'post_parent' => $newParentId]);
-
-		// Move existing children to new parent
-		$existingChildren = get_children(['post_parent' => $currentParentId, 'fields' => 'ids']);
-		foreach ($existingChildren as $childId) {
-			if ($childId !== $newParentId) {
-				wp_update_post(['ID' => $childId, 'post_parent' => $newParentId]);
-			}
-		}
-
-		return $newParentId;
+		return Meta::forUser($userId)
+			->setAll($allowedFields);
 	}
 
+
 	// ─────────────────────────────────────────────────────────────
 	// Helpers
 	// ─────────────────────────────────────────────────────────────
 
-	private function initTimelineFields(string $content): void
-	{
-		$content = jvbNoBase($content);
-		if (!Features::forContent($content)->has('is_timeline')) {
-			return;
-		}
-
-		$config = Features::getConfig($content);
-		$this->fields = $config['fields'] ?? [];
-
-		// Shared fields (apply to all posts)
-		$this->timelineSharedFields = array_keys(array_filter($this->fields, function ($field) {
-			return !isset($field['for_all']) || $field['for_all'] === false;
-		}));
-		array_unshift($this->timelineSharedFields, 'post_thumbnail', 'post_title', 'post_status');
-
-		// Unique fields (per-entry)
-		$this->timelineUniqueFields = array_keys(array_filter($this->fields, function ($field) {
-			return isset($field['for_all']) && $field['for_all'] === true;
-		}));
-	}
-
 	private function verifyOwnership(int $postId): bool
 	{
 		$post = get_post($postId);
 		return $post && (int)$post->post_author === $this->userId;
 	}
 
-	private function generatePostTitle(string $content): string
+	/*************************************************************
+	 * TIMELINE HELPERS
+	 *************************************************************/
+	protected function maybeReorderTimelines(array $parentIDs):void {
+		foreach ($parentIDs as $parentId) {
+			try {
+				$this->maybeReorderTimeline((int)$parentId);
+			} catch (Exception $e) {
+				error_log("Timeline reorder failed for parent {$parentId}: " . $e->getMessage());
+			}
+		}
+	}
+	protected function getTimelinePosts(int $parentID):array
 	{
-		$username = get_user_meta($this->userId, 'first_name', true);
-		$link = get_user_meta($this->userId, BASE . 'link', true);
-		$city = function_exists('jvbArtistCity') ? jvbArtistCity($link) : '';
+		$parent = get_post($parentID);
+		if (!$parent) {
+			return [];
+		}
 
-		return ucfirst($content) . ' by ' . ($city ? "$city artist " : '') . $username;
+		$children = jvbTimelinePoints($parentID, $parent->post_type, ['publish', 'draft']);
+
+		if (count($children) === 0) {
+			return [];
+		}
+
+		$allPosts = array_merge([$parent], $children);
+
+		// Sort by post_date
+		usort($allPosts, function($a, $b) {
+			return strtotime($a->post_date) <=> strtotime($b->post_date);
+		});
+
+		return $allPosts;
+	}
+	protected function maybeReorderTimeline(int $parentID):void
+	{
+//		clean_post_cache($parentID);
+		$parent = get_post($parentID);
+		if (!$parent) {
+			return;
+		}
+
+		$allPosts = $this->getTimelinePosts($parentID);
+		if (empty($allPosts)) {
+			return;
+		}
+
+
+
+		// Check if order changed
+		$needsReorder = false;
+		foreach ($allPosts as $index => $post) {
+			if ($index === 0 && $post->ID !== $parent->ID) {
+				$needsReorder = true;
+				break;
+			}
+			if ($index > 0 && (int)$post->menu_order !== $index) {
+				$needsReorder = true;
+				break;
+			}
+		}
+
+		if (!$needsReorder) {
+			// Just recalculate timelines without reordering
+			$this->recalculateTimelines($allPosts);
+			return;
+		}
+
+		// Handle parent swap if needed
+		$newParent = $allPosts[0];
+		if ($newParent->ID !== $parent->ID) {
+			$this->swapTimelineParent($parent, $newParent, $allPosts);
+		} else {
+			// Just update menu orders and timelines
+			foreach ($allPosts as $index => $post) {
+				if ($index === 0) continue; // Skip parent
+
+				$success = jvb_update_post([
+					'ID' => $post->ID,
+					'post_parent'	=> $newParent->ID,
+					'menu_order' => $index
+				]);
+			}
+
+			$this->recalculateTimelines($allPosts);
+		}
+	}
+
+	private function recalculateTimelines(array $posts): void
+	{
+		$previousPost = null;
+		$latestTimestamp = 0;
+
+
+		$lastKey = array_key_last($posts);
+		foreach ($posts as $index => $post) {
+			$meta = Meta::forPost($post->ID);
+			if ($index === 0) {
+				$meta->set('timeline', '');
+				$previousPost = $post;
+				continue; // Parent has no timeline
+			}
+
+			// Calculate timeline from previous post
+			if ($previousPost) {
+				$timeline = $this->calculateTimeline($previousPost, $post);
+				if ($timeline) {
+					$termId = $this->getOrCreateTerm($timeline, 'timeline');
+					if ($termId) {
+						$success = $meta->set('timeline', $termId);
+					}
+				}
+			}
+			$isUpdate = $meta->get('is_update');
+			if (!(bool) $isUpdate) {
+				$meta->set('number', $index);
+			}
+
+			if ($lastKey === $index) {
+				$latestTimestamp = strtotime($post->post_date);
+			}
+			$previousPost = $post;
+		}
+
+		// Update parent's latest_date
+		if ($latestTimestamp > 0) {
+			$success = update_post_meta($posts[0]->ID, BASE . 'latest_date', $latestTimestamp);
+		}
+	}
+
+	private function calculateTimeline(\WP_POST $previous, \WP_POST $current): ?string
+	{
+		$previousDate = strtotime($previous->post_date);
+		$currentDate = strtotime($current->post_date);
+
+		if (!$previousDate || !$currentDate || $currentDate <= $previousDate) {
+			return null;
+		}
+
+		$daysDiff = floor(($currentDate - $previousDate) / (60*60*24));
+		$weeks = floor($daysDiff / 7);
+		if ($weeks === 0) {
+			return 'Less than 1 Week';
+		}
+		if ($weeks < 16) {
+			return $weeks === 1 ? '1 Week' : $weeks . ' Weeks';
+		}
+
+		$previousDateTime = new \DateTime($previous->post_date);
+		$currentDateTime = new \DateTime($current->post_date);
+
+		$interval = $previousDateTime->diff($currentDateTime);
+		$months = ($interval->y * 12) + $interval->m;
+
+		if ($months === 0) {
+			return $weeks . ' Weeks';
+		}
+
+		return ($months === 1) ? '1 Month' : $months . ' Months';
+	}
+
+	private function swapTimelineParent(\WP_Post $oldParent, \WP_Post $newParent, array $allPosts): void
+	{
+		// Swap titles and content
+		$originalTitle = $oldParent->post_title;
+		$originalSlug = $oldParent->post_name;
+		$originalContent = $oldParent->post_content;
+
+		$updateParent = jvb_update_post([
+			'ID' => $oldParent->ID,
+			'post_title' => 'Treatment',
+			'post_name' => sanitize_title('Treatment ' . $newParent->ID),
+			'post_content' => '',
+		]);
+
+		$updateNewParent = jvb_update_post([
+			'ID' => $newParent->ID,
+			'post_title' => $originalTitle,
+			'post_name' => $originalSlug,
+			'post_content' => $originalContent,
+			'post_parent' => 0,
+			'menu_order' => 0
+		]);
+
+		// Clear timeline taxonomy from new parent
+		wp_set_object_terms($newParent->ID, [], BASE . 'timeline', false);
+
+		// Update all other posts to new parent
+		foreach ($allPosts as $index => $post) {
+			if ($index === 0) continue; // Skip new parent
+
+			$title = $post->post_title;
+			if (str_starts_with($title, 'Treatment #')) {
+				$title = 'Treatment #' . $index;
+			}
+
+			$childUpdate = jvb_update_post([
+				'ID' => $post->ID,
+				'post_title' => $title,
+				'post_parent' => $newParent->ID,
+				'menu_order' => $index,
+			]);
+		}
+
+		// Recalculate timelines for all posts
+		$this->recalculateTimelines($allPosts);
+	}
+
+	private function getOrCreateTerm(string $termName, string $taxonomy): ?int
+	{
+		$taxonomy = jvbCheckBase($taxonomy);
+		$term = get_term_by('name', $termName, $taxonomy);
+
+		if (!$term) {
+			$result = wp_insert_term($termName, $taxonomy);
+			if (is_wp_error($result)) {
+				return null;
+			}
+			return $result['term_id'];
+		}
+
+		return $term->term_id;
+	}
+
+	protected function checkSharedFields(array $fields): void
+	{
+		foreach ($fields as $parentID => $shared) {
+			$meta = Meta::forPost($parentID);
+			$values = $meta->getAll($shared);
+
+			$children = jvbTimelinePoints($parentID, get_post_type($parentID), ['any']);
+
+			if (empty($children)) {
+				continue;
+			}
+
+			foreach ($children as $child) {
+				Meta::forPost($child)->setAll($values);
+			}
+		}
+	}
+
+	protected function handleTimelineStatusChange(array $updates):void
+	{
+		$updates = array_filter($updates, function ($status) {
+			return in_array($status, ['trash', 'delete', 'publish', 'draft']);
+		});
+
+		foreach ($updates as $parentID => $status) {
+			$children = jvbTimelinePoints($parentID, get_post_type($parentID), ['any']);
+
+			if (!empty($children)) {
+				foreach($children as $child) {
+					if ($status === 'trash') {
+						wp_trash_post($child);
+					} elseif ($status === 'delete') {
+						wp_delete_post($child, true);
+					}else {
+						jvb_update_post([
+							'ID'	=> $child,
+							'post_status'	=> $status
+						]);
+					}
+
+				}
+			}
+		}
+	}
+
+	protected function handlePost(string|int $ID, array $data, Registrar $registrar, array $results, Progress $progress):array
+	{
+		// New post creation
+		if (str_starts_with((string)$ID, 'new')) {
+
+			$newId = wp_insert_post([
+				'post_author' => $this->userId,
+				'post_type'   => $registrar->getBased(),
+				'post_title'  => $data['post_title'] ?? apply_filters('jvbDefaultTitle', '', $registrar->getSlug()),
+				'post_status' => $data['status'] ?? 'draft',
+			]);
+			error_log('Created new post: '.print_r($newId, true));
+
+			if (!$newId || is_wp_error($newId)) {
+				$results['errors'][$ID] = 'Could not create post';
+				$progress->failItem($ID, 'Could not create post');
+				return $results;
+			}
+
+			$results['newPosts'][$ID] = $newId;
+			$this->savePostFields($newId, $data);
+			unset($data['content']);
+			$results['success'][$newId] = $data;
+			$progress->advance();
+			return $results;
+		}
+
+		//Existing post update
+		if (!$this->verifyOwnership((int)$ID)) {
+			$progress->failItem($ID, 'No permission to modify this post');
+			$results['errors'][$ID] = 'No permission';
+			return $results;
+		}
+
+		$result = $this->savePostFields((int)$ID, $data);
+		unset($data['content']);
+		if ($result) {
+			$results['success'][$ID] = $data;
+		} else {
+			$results['errors'][$ID] = 'Could not update post data';
+		}
+		if ($registrar && $registrar->hasFeature('is_timeline')) {
+			$post = get_post((int)$ID);
+			$parentId = $post->post_parent > 0 ? $post->post_parent : $post->ID;
+			$fields = $registrar->getFields();
+			$sharedFields = array_keys(array_filter($fields, function ($field) {
+				return !array_key_exists('for_all', $field) || !$field['for_all'];
+			}));
+
+
+			if (array_key_exists('timeline_gallery', $data)) {
+				//This should only happen if we delete an image from the gallery
+				$changes = explode(',', $data['timeline_gallery']);
+				$timelinePosts = $this->getTimelinePosts($parentId);
+				if (!empty($timelinePosts)) {
+					$posts = array_map(function($item) { return $item->ID; }, $timelinePosts);
+					$changed = false;
+					foreach ($posts as $tID) {
+						if (!in_array($tID, $changes)) {
+							$changed = true;
+							wp_delete_post($tID, true);
+						}
+					}
+					if ($changed) {
+						$results['timelineParents'][] = $parentId;
+					}
+				}
+
+			}
+
+
+			if (array_key_exists('post_date', $data) && !in_array($parentId, $results['timelineParents'])) {
+				$results['timelineParents'][] = $parentId;
+			}
+			if ($parentId === $ID) {
+				if (array_key_exists('post_status', $data) && !array_key_exists($parentId, $results['timelineStatus'])) {
+					$results['timelineStatus'][$parentId] = $data['post_status'];
+				}
+
+				if (count(array_intersect($sharedFields, array_keys($data))) > 0) {
+					if (!array_key_exists($parentId, $results['timelineSharedFields'])) {
+						$results['timelineSharedFields'][$parentId] = [];
+					}
+					$temp = array_intersect($sharedFields, array_keys($data));
+					$results['timelineSharedFields'][$parentId] = array_unique(array_merge($results['timelineSharedFields'][$parentId], $temp));
+				}
+			}
+		}
+		$progress->advance();
+		return $results;
+	}
+
+
+	protected function handleTerm(int $ID, array $data, Registrar $registrar, array $results, Progress $progress):array
+	{
+		error_log('Handling term '.$ID.' with data: '.print_r($data, true));
+		//Existing term update
+		if ($registrar->hasFeature('is_ownable') && (!Roles::getInstance()()->isOwner($this->userId, $ID) && !Roles::getInstance()()->isManager($this->userId, $ID))) {
+			error_log('Term is ownable. User does not own this term.');
+			$progress->failItem($ID, 'No permission to modify this term');
+			$results['errors'][$ID] = 'No permission';
+			return $results;
+		}
+
+		$result = $this->saveTermFields($ID, $data);
+		unset($data['content']);
+		if ($result) {
+			$results['success'][$ID] = $data;
+		} else {
+			$results['errors'][$ID] = 'Could not update term data';
+		}
+		$progress->advance();
+		return $results;
+	}
+	protected function handleUser(int $ID, array $data, Registrar $registrar, array $results, Progress $progress):array
+	{
+		//Existing term update
+		if ($ID !== $this->userId || !user_can($this->userId, 'manage_options')) {
+			$progress->failItem($ID, 'No permission to modify this term');
+			$results['errors'][$ID] = 'No permission';
+			return $results;
+		}
+
+		$result = $this->saveUserFields($ID, $data);
+		unset($data['content']);
+		if ($result) {
+			$results['success'][$ID] = $data;
+		} else {
+			$results['errors'][$ID] = 'Could not update post data';
+		}
+		$progress->advance();
+		return $results;
 	}
 }

--
Gitblit v1.10.0