Jake Vanderwerf
2025-11-23 d7dbe7fee362d587dfc334135d9581b6216a4295
inc/rest/routes/ContentRoutes.php
@@ -88,17 +88,44 @@
      $config = Features::getConfig($content);
      $this->fields = $config['fields'];
      $this->timelineSharedFields = array_keys(array_filter($this->fields, function ($field) {
         if (!array_key_exists('for_all', $field) || $field['for_all'] === false){
      $this->timelineSharedFields = $this->getTimelineSharedFields($content);
      array_unshift($this->timelineSharedFields, 'post_thumbnail');
      array_unshift($this->timelineSharedFields, 'post_title');
      array_unshift($this->timelineSharedFields, 'post_status');
      $this->timelineUniqueFields = $this->getTimelineUniqueFields($content);
   }
   public function getTimelineUniqueFields(string $content):array
   {
      $content = jvbNoBase($content);
      if (!Features::forContent($content)->has('is_timeline')){
         return [];
      }
      $config = Features::getConfig($content);
      $allFields = $config['fields'];
      return array_keys(array_filter($allFields, function ($field) {
         if (array_key_exists('for_all', $field) && $field['for_all'] === true) {
            return true;
         }
         return false;
      }));
      array_unshift($this->timelineSharedFields, 'post_thumbnail');
      array_unshift($this->timelineSharedFields, 'post_title');
   }
      $this->timelineUniqueFields = array_keys(array_filter($this->fields, function ($field) {
         if (array_key_exists('for_all', $field) && $field['for_all'] === true) {
   public function getTimelineSharedFields(string $content):array
   {
      $content = jvbNoBase($content);
      if (!Features::forContent($content)->has('is_timeline')){
         return [];
      }
      $config = Features::getConfig($content);
      if (!$config || empty($config)) {
         return [];
      }
      $allFields = $config['fields']??[];
      return array_keys(array_filter($allFields, function ($field) {
         if (!array_key_exists('for_all', $field) || $field['for_all'] === false){
            return true;
         }
         return false;
@@ -198,9 +225,6 @@
    public function handleContentRequest(WP_REST_Request $request):WP_REST_Response
    {
        $params = $request->get_params();
      error_log('handleContentRequest params: '.print_r($params, true));
      error_log('Fetching content. Params: '.print_r($params, true));
        $user_id = $params['user'];
        if (!$this->userCheck($user_id)) {
            return new WP_REST_Response([
@@ -251,10 +275,6 @@
         $args['s'] = sanitize_text_field($params['search']);
      }
      error_log('Content Routes final args: '.print_r($args, true));
        $key = $this->cache->generateKey($args);
      // Check HTTP cache headers with the specific content type
      $content_type = $params['content'] ?? $params['type'];
@@ -339,7 +359,7 @@
        $results = [];
        foreach ($posts as $ID => $post_data) {
         if (Features::forContent($post_data['content'])->has('is_timeline')) {
         if (Features::forContent($post_data['content'])->has('is_timeline') && array_key_exists('timeline', $post_data)) {
            $results[$ID] =$this->processTimelinePost($ID, $post_data);
            continue;
         }
@@ -416,7 +436,6 @@
            error_log('Allowed Fields: '.print_r($allowedFields, true));
            $meta = new MetaManager($ID, 'post');
            $success = $meta->setAll($allowedFields);
            error_log('Should be set?');
            $results[$ID] = [
               'success'   => $success
            ];
@@ -458,57 +477,133 @@
         return ['success' => false, 'message' => 'No permission'];
      }
      $ignore = ['content', 'user'];
      $this->fields = jvbGetFields($post_data['content']);
      $this->initTimelineFields($post_data['content']);
      error_log('Received Data: '.print_r($post_data, true));
      // First, process the main fields that will apply to all posts
      $sharedData = array_filter($post_data, function ($key) {
         return in_array($key, $this->timelineSharedFields);
      // Get parent post details
      $parent_post = get_post($parent_id);
      $parent_title = $parent_post->post_title;
      $parent_is_published = ($parent_post->post_status === 'publish');
      // Extract shared data from top level (excluding post_thumbnail which is unique per post)
      $sharedData = array_filter($post_data, function ($key) use ($ignore) {
         return in_array($key, $this->timelineSharedFields)
            && !in_array($key, $ignore)
            && $key !== 'post_thumbnail';
      }, ARRAY_FILTER_USE_KEY);
      //Next, process any individual posts, including any menu order changes
      // If no shared post_title at top level, extract from first timeline entry
      if (!isset($sharedData['post_title']) && isset($post_data['timeline'][0]['post_title'])) {
         $sharedData['post_title'] = $post_data['timeline'][0]['post_title'];
      }
      $clearParent = false;
      if (array_key_exists('timeline', $post_data) && is_array($post_data['timeline'])) {
         $sharedTaxonomies = $sharedData;
         unset($sharedTaxonomies['post_title']);
         // Remove post_title and post_thumbnail from shared taxonomies
         $sharedTaxonomies = array_filter($sharedData, function($key) {
            return $key !== 'post_title' && $key !== 'post_thumbnail';
         }, ARRAY_FILTER_USE_KEY);
         //Ensure the parent post exists and is still first in the array
         $index = array_search((string)$parent_id, array_column($post_data['timeline'], 'id'));
         error_log('Found index: '.print_r($index, true));
         if ($index === false) {
            return [
               'success' => false,
               'message'   => 'Missing parent id. This should not have happened'
            ];
         } elseif ($index !== 0) {
            // Move that element to the start of the array
         }
         if ($index !== 0) {
            $new_parent_id = $post_data['timeline'][0]['id'];
            if (is_numeric($new_parent_id) && (int)$new_parent_id > 0) {
               $new_parent_id = (int)$new_parent_id;
               wp_update_post([
                  'ID' => $new_parent_id,
                  'post_parent' => 0
               ]);
               wp_update_post([
                  'ID' => $parent_id,
                  'post_parent' => $new_parent_id
               ]);
               $existing_children = get_children([
                  'post_parent' => $parent_id,
                  'fields' => 'ids'
               ]);
               foreach ($existing_children as $child_id) {
                  if ($child_id !== $new_parent_id) {
                     wp_update_post([
                        'ID' => $child_id,
                        'post_parent' => $new_parent_id
                     ]);
                  }
               }
               // Update parent references
               $parent_id = $new_parent_id;
               $parent_post = get_post($parent_id);
               $parent_title = $parent_post->post_title;
               $parent_is_published = ($parent_post->post_status === 'publish');
            } else {
            $item = $post_data['timeline'][$index];
            unset($post_data['timeline'][$index]);
            array_unshift($post_data['timeline'], $item);
         }
         }
         $errors = [];
         $success = [];
         // Get existing children to track deletions
         $existing_children = get_children([
            'post_parent' => $parent_id,
            'post_type' => jvbCheckBase($post_data['content']),
            'orderby' => 'menu_order',
            'post_status' => ['publish', 'draft'],
            'fields' => 'ids'
         ]);
         //Iterate through the timeline posts
         $prevDate = null;
         foreach($post_data['timeline'] as $order => $timeline) {
            $allowedFields = array_filter($timeline, function($key) {
               return in_array($key, $this->timelineUniqueFields);
            // Get unique fields for this specific timeline entry
            $allowedFields = array_filter($timeline, function($key) use ($ignore) {
               return in_array($key, $this->timelineUniqueFields) && !in_array($key, $ignore);
            }, ARRAY_FILTER_USE_KEY);
            $allowedFields['post_title'] = $allowedFields['post_title'] ?? $sharedData['post_title'].' - Treatment #'.$order;
            $allowedFields = array_merge($allowedFields, $sharedTaxonomies);
            // Determine the post title
            $is_parent = ((int)$timeline['id'] === $parent_id);
            $provided_title = $timeline['post_title'] ?? '';
            $auto_generated_pattern = '/^.+Treatment #?\d+$/'; // Matches "Title - Treatment #1" or "Title - Treatment 1"
            if ($is_parent) {
               // Parent keeps its own title or uses shared title
               $allowedFields['post_title'] = $provided_title ?: ($sharedData['post_title'] ?? $parent_title);
            } else {
               // For child posts, auto-generate if:
               // 1. No title provided, OR
               // 2. Title matches auto-generated pattern (meaning it wasn't customized)
               if (empty($provided_title) || preg_match($auto_generated_pattern, $provided_title)) {
                  $allowedFields['post_title'] = 'Treatment ' . $order;
               } else {
                  // Keep custom title
                  $allowedFields['post_title'] = $provided_title;
               }
            }
            // Merge with shared taxonomies AFTER setting unique fields
            $allowedFields = array_merge($sharedTaxonomies, $allowedFields);
            // Handle post creation if needed
            if (!array_key_exists('id', $timeline) || !is_numeric($timeline['id'])) {
               $newChild = wp_insert_post([
                  'post_author'  => $this->user_id,
                  'post_type'    => jvbCheckBase($post_data['content']),
                  'post_title'   => $allowedFields['post_title'],
                  'post_parent'  => $parent_id,
                  'menu_order'   => $order
                  'menu_order' => $order,
                  'post_status' => $parent_is_published ? 'publish' : 'draft'
               ]);
               if (!$newChild || is_wp_error($newChild)) {
                  $errors[] = [
@@ -524,32 +619,82 @@
               unset($existing_children[array_search((int)$timeline['id'], $existing_children)]);
            }
            //Determine which fields to update
            // Update post status and menu order
            $post_updates = ['ID' => $timeline['id']];
            if (!$is_parent) {
               $post_updates['menu_order'] = $order;
               // Auto-publish child if parent is published
               if ($parent_is_published) {
                  $current_post = get_post($timeline['id']);
                  if ($current_post && $current_post->post_status !== 'publish') {
                     $post_updates['post_status'] = 'publish';
                  }
               }
            }
            if (count($post_updates) > 1) {
               $result = wp_update_post($post_updates);
               error_log('Updated post '.$timeline['id'].' with: '.print_r($post_updates, true).' Result: '.$result);
               $clearParent = true;
            }
            // Update metadata
            $meta = new MetaManager($timeline['id'], 'post');
            $oldValues = $meta->getAll(array_keys($allowedFields));
            // Set number taxonomy to menu_order (always update for reordering)
            if (!$is_parent) {
               $number_value = $order;
               $term = get_term_by('name', (string)$number_value, BASE.'number');
               if (!$term) {
                  $result = wp_insert_term((string)$number_value, BASE.'number');
                  if ($result && !is_wp_error($result)) {
                     $term = $result['term_id'];
                  }
               } else {
                  $term = $term->term_id;
               }
               $allowedFields['number'] = $term;
            }
            // Auto-timeline logic
            if ($prevDate) {
               $newDate = array_key_exists('date', $oldValues) ? $oldValues['date'] : ((array_key_exists('date', $allowedFields)) ? $allowedFields['date'] : null);
               if ($newDate) {
                  $date1 = new \DateTime($prevDate);
                  $date2 = new \DateTime($newDate);
                  $weeks = floor($date1->diff($date2)->days / 7);
                  if ($weeks > 0) {
                     $termToCheck = $weeks.' Weeks';
                     $term = get_term_by('name', $termToCheck, BASE.'timeline');
                     if (!$term) {
                        $result = wp_insert_term($termToCheck, BASE.'timeline');
                        if ($result && !is_wp_error($result)) {
                           $term = $result['term_id'];
                        }
                     } else {
                        $term = $term->term_id;
                     }
                     $allowedFields['timeline'] = $term;
                  }
               }
            }
            $prevDate = array_key_exists('date', $oldValues) ? $oldValues['date'] : ((array_key_exists('date', $allowedFields)) ? $allowedFields['date'] : $prevDate);
            $updateValues = array_filter($allowedFields, function($value, $key) use ($oldValues) {
               return (!array_key_exists($key, $oldValues) || $value !== $oldValues[$key]);
            }, ARRAY_FILTER_USE_BOTH);
            error_log('Setting values for '.$timeline['id'].': '.print_r($updateValues, true));
            $meta->setAll($updateValues);
            //Update Menu Order, if applicable
            if ((int) $timeline['id'] !== $parent_id) {
               $post = get_post((int) $timeline['id']);
               if ($post && $post->menu_order !== $order) {
                  $updated = wp_update_post([
                     'ID'        => $post->ID,
                     'menu_order'   => $order,
                  ]);
                  if (!$updated || is_wp_error($updated)) {
                     $errors[] = [
                        'message'   => 'Could not update timeline order for post',
                        'data'      => $timeline
                     ];
                  }
               }
            }
            $timeline['id'] = (int) $timeline['id'];
            $success[] = $timeline['id'];
         }
      }
      //Delete any remaining children that no longer exist
      if (!empty($existing_children)) {
         foreach ($existing_children as $ID) {
@@ -557,6 +702,12 @@
         }
      }
      if ($clearParent) {
         $this->cache->clear();
         CacheManager::onPostSave($parent_id, $parent_post);
      }
      return ['success' => true, 'data' => [
         'success'   => $success,
         'errors' => $errors
@@ -650,6 +801,7 @@
        $this->meta = new MetaManager($post->ID, 'post');
        $data = [
            'id'        => $post->ID,
         'title'     => $post->post_title,
            'status'    => $post->post_status,
            'date'      => $post->post_date,
            'modified'  => $post->post_modified,
@@ -719,7 +871,7 @@
      $item['fields'] = $mainMeta->getAll($this->timelineSharedFields);
      //Step 2: Get the fields for each individual posts
      $children = get_children(['post_parent' => $post->ID, 'orderby' => 'menu_order', 'post_status' => ['publish', 'draft'], 'fields'=> 'ids']);
      $children = get_children(['post_parent' => $post->ID, 'orderby' => 'date', 'order' => 'ASC', 'post_status' => ['publish', 'draft'], 'fields'=> 'ids']);
      array_unshift($children, $post->ID);
      $subFields = [];
@@ -728,7 +880,7 @@
         $meta = new MetaManager($child, 'post');
         $f = $meta->getAll($this->timelineUniqueFields);
         $f =  ['id' => $child] + $f;
         $subFields[$f['post_thumbnail']] = $f;
         $subFields[] = $f;
         $images[$f['post_thumbnail']] = jvbImageData((int) $f['post_thumbnail']);
      }