| | |
| | | $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([ |
| | |
| | | |
| | | $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; |
| | | } |
| | |
| | | $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); |
| | | |
| | |
| | | $errors[$id] = $e->getMessage(); |
| | | } |
| | | } |
| | | if (!empty($updateTimelineOrder)) { |
| | | foreach ($updateTimelineOrder as $parentID) { |
| | | $this->reorderTimelineByDate($parentID); |
| | | } |
| | | } |
| | | |
| | | // Send notification |
| | | if (jvbSiteHasNotifications()) { |
| | |
| | | break; |
| | | case 'draft': |
| | | wp_update_post(['ID' => $postId, 'post_status' => 'draft']); |
| | | unset($postData['post_status']); |
| | | break; |
| | | case 'trash': |
| | | wp_trash_post($postId); |
| | |
| | | } |
| | | } |
| | | |
| | | // 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'] ?? ''; |
| | |
| | | $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, |
| | | ]); |
| | |
| | | } |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | | // 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 |
| | | // ───────────────────────────────────────────────────────────── |
| | | |