From 235ce5716edc2f7cbe80fdccf26eac7269587839 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Mon, 08 Jun 2026 04:38:18 +0000
Subject: [PATCH] =FavouritesManager.php and FavouritesRoutes.php fixes. Moving all logic to FavouritesManager.php. Still some left to do

---
 inc/managers/queue/executors/ContentExecutor.php | 1003 +++++++++++++++++++++++++++++-----------------------------
 1 files changed, 500 insertions(+), 503 deletions(-)

diff --git a/inc/managers/queue/executors/ContentExecutor.php b/inc/managers/queue/executors/ContentExecutor.php
index dd4c80c..6b6aa1a 100644
--- a/inc/managers/queue/executors/ContentExecutor.php
+++ b/inc/managers/queue/executors/ContentExecutor.php
@@ -1,9 +1,9 @@
 <?php
 namespace JVBase\managers\queue\executors;
 
-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')) {
@@ -18,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',
@@ -71,135 +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 = [];
 
-		$updateTimelineOrder = [];
-
 		foreach ($posts as $id => $postData) {
 			try {
-				$content = $postData['content'] ?? '';
-
-				// 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,
-						'processed_fields' => array_keys($postData)
-					];
-
-					if (Features::forContent($content)->has('is_timeline')) {
-						$this->updateTimelineLatestDate($newId);
-					}
-
-					$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;
 				}
-
-				// Existing post update
-				if (!$this->verifyOwnership((int)$id)) {
-					$progress->failItem($id, 'No permission to modify this post');
-					$errors[$id] = 'No permission';
-					continue;
-				}
-				// Check if this is a timeline post
-				$isTimeline = Features::forContent($content)->has('is_timeline');
-
-				if ($isTimeline) {
-					$post = get_post((int)$id);
-					$parentId = $post->post_parent;
-					$isParent = ($parentId === 0);
-
-					// Track timeline reordering only if date changed
-					if (array_key_exists('post_date', $postData)) {
-						$timelineRoot = $isParent ? (int)$id : $parentId;
-						if (!in_array($timelineRoot, $updateTimelineOrder)) {
-							$updateTimelineOrder[] = $timelineRoot;
-						}
-					}
-
-					// Update shared fields if this is the parent
-					if ($isParent) {
-						$this->initTimelineFields($content);
-						$sharedFieldsUpdated = array_filter($postData, function($key) {
-							return in_array($key, $this->timelineSharedFields);
-						}, ARRAY_FILTER_USE_KEY);
-
-						if (!empty($sharedFieldsUpdated)) {
-							$this->updateSharedFields((int)$id, $sharedFieldsUpdated);
-						}
-					}
-				}
-				$this->processPostUpdate((int)$id, $postData);
-
-				if (Features::forContent($content)->has('is_timeline')
-					&& array_key_exists('post_date', $postData)) {
-					$post = get_post((int)$id);
-					$parentId = $post->post_parent === 0 ? (int)$id : $post->post_parent;
-					$this->updateTimelineLatestDate($parentId);
-				}
-
-				$results[$id] = [
-					'success' => true,
-					'processed_fields' => array_keys($postData)
-				];
-				$progress->advance(1);
-
 			} catch (Exception $e) {
 				$progress->failItem($id, $e->getMessage());
-				$errors[$id] = $e->getMessage();
-				$results[$id] = [
-					'success' => false,
-					'error' => $e->getMessage()
-				];
+				$results['errors'][$id] = $e->getMessage();
 			}
 		}
-		if (!empty($updateTimelineOrder)) {
-			$processedParents = []; // Track to avoid duplicate processing
+		error_log('Final Results: '.print_r($results, true));
 
-			foreach ($updateTimelineOrder as $oldParentID) {
-				if (in_array($oldParentID, $processedParents)) continue;
-
-				$actualParentId = $this->reorderTimelineByDate($oldParentID);
-				$processedParents[] = $actualParentId;
-
-				// If parent changed, mark the new parent as processed too
-				if ($actualParentId !== $oldParentID) {
-					$processedParents[] = $oldParentID;
-				}
+		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()) {
+//			JVB()->notification()->addNotification(
+//				$this->userId,
+//				'content_update_complete',
+//				null,
+//				'Content updates completed!'
+//			);
+//		}
 
 		$outcome = 'success';
 		if (!empty($errors)) {
@@ -208,431 +140,496 @@
 
 		return new Result(
 			outcome: $outcome,
-			result: [
-				'posts' => $results,
-				'errors' => $errors,
-				'updated_count' => count(array_filter($results, fn($r) => $r['success'] ?? false)),
-				'failed_count' => count($errors)
-			]
+			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']);
-					unset($postData['post_status']);
-					break;
-				case 'trash':
-					wp_trash_post($postId);
-					return;
-				case 'delete':
-					wp_delete_post($postId, true);
-					return;
-			}
-		}
-
-		// Save all fields via MetaManager (handles post fields too)
-		$this->savePostFields($postId, $postData);
-	}
-
-	private function updateSharedFields(int $parentId, array $sharedFields): void
-	{
-		// Get all posts in timeline
-		$children = get_posts([
-			'post_type' => get_post_type($parentId),
-			'post_parent' => $parentId,
-			'post_status' => ['publish', 'draft'],
-			'posts_per_page' => -1,
-			'fields' => 'ids'
-		]);
-
-		$allPostIds = array_merge([$parentId], $children);
-
-		// Apply shared fields to all posts
-		foreach ($allPostIds as $timelinePostId) {
-			$meta = new MetaManager($timelinePostId, 'post');
-			$meta->setAll($sharedFields);
-		}
-	}
-
-	private function reorderTimelineByDate(int $parentId): int
-	{
-		$parent = get_post($parentId);
-		if (!$parent) return $parentId;
-
-		// Get all posts in this timeline (parent + children)
-		$children = get_posts([
-			'post_type' => get_post_type($parentId),
-			'post_parent' => $parentId,
-			'post_status' => ['publish', 'draft'],
-			'posts_per_page' => -1,
-			'orderby' => 'date',
-			'order' => 'ASC'
-		]);
-
-		// Combine and sort by post_date
-		$allPosts = array_merge([$parent], $children);
-		usort($allPosts, function($a, $b) {
-			return strtotime($a->post_date) <=> strtotime($b->post_date);
-		});
-
-		$newParent = $allPosts[0];
-		$actualParentId = $newParent->ID; // Track the actual parent
-
-		// If parent changed, restructure
-		if ($newParent->ID !== $parentId) {
-			wp_update_post([
-				'ID' => $newParent->ID,
-				'post_parent' => 0,
-				'menu_order' => 0
-			]);
-
-			wp_update_post([
-				'ID' => $parentId,
-				'post_parent' => $newParent->ID
-			]);
-
-			foreach ($allPosts as $index => $post) {
-				if ($index === 0) continue;
-
-				wp_update_post([
-					'ID' => $post->ID,
-					'post_parent' => $newParent->ID,
-					'menu_order' => $index
-				]);
-
-				$this->getOrCreateTerm($post->ID, (string)$index, 'number');
-			}
-		} else {
-			// Just update menu_order
-			foreach ($allPosts as $index => $post) {
-				if ($index === 0) continue;
-
-				wp_update_post([
-					'ID' => $post->ID,
-					'menu_order' => $index
-				]);
-
-				$this->getOrCreateTerm($post->ID, (string)$index, 'number');
-			}
-		}
-
-		// Calculate and set timeline taxonomy (time since previous post)
-		$previousPost = null;
-		foreach ($allPosts as $index => $post) {
-			if ($index === 0) {
-				// Parent post - no timeline term (it's the baseline)
-				wp_set_object_terms($post->ID, [], BASE . 'timeline', false);
-				$previousPost = $post;
-				continue;
-			}
-
-			$timelineTerm = $this->calculateTimelineTerm($previousPost, $post);
-			if ($timelineTerm) {
-				$this->getOrCreateTerm($post->ID, $timelineTerm, 'timeline');
-			}
-
-			$previousPost = $post;
-		}
-
-		// Update latest_date AFTER reordering with the actual parent
-		$this->updateTimelineLatestDate($actualParentId);
-
-		return $actualParentId; // Return the actual parent ID
-	}
-
-	private function updateTimelineLatestDate(int $parentId): void
-	{
-		$parent = get_post($parentId);
-		if (!$parent) return;
-
-		// Get all posts in timeline
-		$children = get_posts([
-			'post_type' => get_post_type($parentId),
-			'post_parent' => $parentId,
-			'post_status' => ['publish', 'draft'],
-			'posts_per_page' => -1,
-			'orderby' => 'date',
-			'order' => 'DESC', // Get newest first
-			'fields' => 'ids'
-		]);
-
-		// Count: parent + children
-		$number = count($children) + 1;
-
-		$allPostIds = array_merge([$parentId], $children);
-
-		// Get all timestamps
-		$timestamps = array_map(function($id) {
-			$post = get_post($id);
-			return $post ? strtotime($post->post_date) : 0;
-		}, $allPostIds);
-
-		$latestTimestamp = max($timestamps);
-
-		// Update both meta fields
-		update_post_meta($parentId, BASE . 'number', $number);
-		update_post_meta($parentId, BASE . 'latest_date', $latestTimestamp);
-	}
-
-	private function calculateTimelineTerm(\WP_Post $previousPost, \WP_Post $currentPost): ?string
-	{
-		$previousDate = strtotime($previousPost->post_date);
-		$currentDate = strtotime($currentPost->post_date);
-
-		if (!$previousDate || !$currentDate || $currentDate <= $previousDate) {
-			return null;
-		}
-
-		// Calculate difference in days
-		$daysDiff = floor(($currentDate - $previousDate) / (60 * 60 * 24));
-
-		// Convert to weeks
-		$weeks = floor($daysDiff / 7);
-
-		// If less than 16 weeks, use weeks
-		if ($weeks < 16) {
-			if ($weeks === 0) {
-				return null; // Same week, no term
-			}
-			return $weeks === 1 ? '1 Week' : $weeks . ' Weeks';
-		}
-
-		// 16+ weeks, calculate months
-		// Using actual month calculation rather than weeks/4
-		$previousDateTime = new \DateTime($previousPost->post_date);
-		$currentDateTime = new \DateTime($currentPost->post_date);
-		$interval = $previousDateTime->diff($currentDateTime);
-
-		$months = ($interval->y * 12) + $interval->m;
-
-		if ($months === 0) {
-			// Edge case: technically less than a full month but 16+ weeks
-			return $weeks . ' Weeks';
-		}
-
-		return $months === 1 ? '1 Month' : $months . ' Months';
-	}
-
-	private function getOrCreateTerm(int $postID, string $termName, string $taxonomy): void
-	{
-		$taxonomy = jvbCheckBase($taxonomy);
-		$term = get_term_by('name', $termName, $taxonomy);
-
-		if (!$term) {
-			$result = wp_insert_term($termName, $taxonomy);
-			if (is_wp_error($result)) {
-				return;
-			}
-			$termID = $result['term_id'];
-		} else {
-			$termID = $term->term_id;
-		}
-
-		if ($termID) {
-			wp_set_object_terms($postID, [$termID], $taxonomy, false);
-		}
-	}
-
 	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;
+		$allowedFields = array_filter($data, function ($key) use ($fields) {
+			return array_key_exists($key, $fields);
+		}, ARRAY_FILTER_USE_KEY);
 
-		if (!$images) {
-			return new Result(
-				outcome: 'failed',
-				result: ['message' => 'No upload results found']
-			);
+
+		//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;
 		}
 
-		$results = [];
+		error_log('Allowed fields: '.print_r($allowedFields, true));
 
-		if ($data['mode'] === 'selection') {
-			$results = $this->createFromSelection($operation, $data, $images, $progress);
-		} else {
-			$results = $this->createFromDirect($operation, $data, $images, $progress);
-		}
-
-		return new Result(
-			outcome: !empty($results) ? 'success' : 'failed',
-			result: $results
-		);
+		return Meta::forTerm($termId)
+			->setAll($allowedFields);
 	}
-
-	private function createFromSelection(Operation $operation, array $data, array $images, Progress $progress): array
+	private function saveUserFields(int $userId, array $data): bool
 	{
-		$results = [];
+		$content = $data['content'] ?? '';
+		$fields = Registrar::getFieldsFor($content);
 
-		foreach ($images as $group => $files) {
-			$settings = json_decode($data['files_data'][$group] ?? '{}');
+		$allowedFields = array_filter($data, function ($key) use ($fields) {
+			return array_key_exists($key, $fields);
+		}, ARRAY_FILTER_USE_KEY);
 
-			if (($settings->type ?? '') === 'group') {
-				$postId = $this->createGroupPost($operation, $data, $files, $settings);
-			} else {
-				$postId = $this->createIndividualPosts($operation, $data, $files);
-			}
+		//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 ($postId) {
-				$results = array_merge($results, (array)$postId);
-			}
-
-			$progress->advance(1);
+		if (empty($allowedFields)) {
+			return true;
 		}
 
-		return $results;
+		return Meta::forUser($userId)
+			->setAll($allowedFields);
 	}
 
-	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) {
-			$title = $this->generatePostTitle($data['content']);
-			$postId = wp_insert_post([
-				'post_type'   => $this->postType,
-				'post_title'  => $title,
-				'post_slug'		=> sanitize_title($title),
-				'post_status' => 'draft',
-				'post_author' => $operation->userId,
-			]);
-
-			if ($postId && !is_wp_error($postId)) {
-				set_post_thumbnail($postId, $img['attachment_id']);
-				$results[] = $postId;
-			}
-		}
-
-		return $results;
-	}
 
 	// ─────────────────────────────────────────────────────────────
 	// 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') && (!JVB()->roles()->isOwner($this->userId, $ID) && !JVB()->roles()->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