From 58e8ae0759ccfa97c478ccae4e0778bdce70966f Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Thu, 22 Jan 2026 22:40:02 +0000
Subject: [PATCH] =DirectoryManager.php updates, some javascript tweaks for CRUD.js, and minor style changes

---
 inc/managers/queue/executors/ContentExecutor.php |  452 ++++++++++++++++++++++++++++++--------------------------
 1 files changed, 243 insertions(+), 209 deletions(-)

diff --git a/inc/managers/queue/executors/ContentExecutor.php b/inc/managers/queue/executors/ContentExecutor.php
index c53f4a7..cf3936d 100644
--- a/inc/managers/queue/executors/ContentExecutor.php
+++ b/inc/managers/queue/executors/ContentExecutor.php
@@ -83,22 +83,12 @@
 		$results = [];
 		$errors = [];
 
+		$updateTimelineOrder = [];
+
 		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;
-				}
-
 				// New post creation
 				if (str_starts_with((string)$id, 'new')) {
 					$newId = wp_insert_post([
@@ -115,6 +105,11 @@
 
 					$this->savePostFields($newId, $postData);
 					$results[$id] = ['success' => true, 'new_id' => $newId];
+
+					if (Features::forContent($content)->has('is_timeline')) {
+						$this->updateTimelineLatestDate($newId);
+					}
+
 					$progress->advance(1);
 					continue;
 				}
@@ -124,8 +119,43 @@
 					$progress->failItem($id, 'No permission to modify this post');
 					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];
 				$progress->advance(1);
 
@@ -140,6 +170,11 @@
 				$errors[$id] = $e->getMessage();
 			}
 		}
+		if (!empty($updateTimelineOrder)) {
+			foreach ($updateTimelineOrder as $parentID) {
+				$this->reorderTimelineByDate($parentID);
+			}
+		}
 
 		// Send notification
 		if (jvbSiteHasNotifications()) {
@@ -177,6 +212,7 @@
 					break;
 				case 'draft':
 					wp_update_post(['ID' => $postId, 'post_status' => 'draft']);
+					unset($postData['post_status']);
 					break;
 				case 'trash':
 					wp_trash_post($postId);
@@ -187,9 +223,201 @@
 			}
 		}
 
+		// 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): void
+	{
+		$parent = get_post($parentId);
+		if (!$parent) return;
+
+		// 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];
+
+		// 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;
+		}
+
+		$this->updateTimelineLatestDate($newParent->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'
+		]);
+
+		$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);
+
+		// Store as UNIX timestamp
+		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'] ?? '';
@@ -325,9 +553,11 @@
 		$results = [];
 
 		foreach ($files as $img) {
+			$title = $this->generatePostTitle($data['content']);
 			$postId = wp_insert_post([
 				'post_type'   => $this->postType,
-				'post_title'  => $this->generatePostTitle($data['content']),
+				'post_title'  => $title,
+				'post_slug'		=> sanitize_title($title),
 				'post_status' => 'draft',
 				'post_author' => $operation->userId,
 			]);
@@ -342,202 +572,6 @@
 	}
 
 	// ─────────────────────────────────────────────────────────────
-	// 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';
-		}, ARRAY_FILTER_USE_KEY);
-
-		if (!isset($sharedData['post_title']) && isset($postData['timeline'][0]['post_title'])) {
-			$sharedData['post_title'] = $postData['timeline'][0]['post_title'];
-		}
-
-		if (!isset($postData['timeline']) || !is_array($postData['timeline'])) {
-			return ['success' => false, 'message' => 'No timeline data'];
-		}
-
-		// 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,
-		];
-	}
-
-	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
-	{
-		$newParentId = $timeline[0]['id'] ?? null;
-
-		if (!is_numeric($newParentId) || (int)$newParentId <= 0) {
-			return $currentParentId;
-		}
-
-		$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;
-	}
-
-	// ─────────────────────────────────────────────────────────────
 	// Helpers
 	// ─────────────────────────────────────────────────────────────
 

--
Gitblit v1.10.0