Jake Vanderwerf
2025-11-23 d7dbe7fee362d587dfc334135d9581b6216a4295
inc/rest/routes/ContentRoutes.php
@@ -25,14 +25,18 @@
    protected string $post_type = '';
    protected string $user_id = '';
   //For Timeline-specific posts
   protected array $timelineSharedFields = [];
   protected array $timelineUniqueFields = [];
    //TODO: Ensure we are handling the bulk operations for all processes
    //TODO: be sure to clear cache ($this->>cache->invalidateGroup($this->>cache_name)) on content update/create
    //TODO: Also invalidate feed caches on updates!!
    public function __construct()
    {
        $this->cache_name = 'user_content_'.get_current_user_id();
        parent::__construct();
        $this->action = 'dash-';
        $this->operation_type = 'content_update';
        add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
@@ -75,6 +79,59 @@
        ]);
    }
   protected function initTimelineFields(string $content):void
   {
      $content = jvbNoBase($content);
      if (!Features::forContent($content)->has('is_timeline')){
         return;
      }
      $config = Features::getConfig($content);
      $this->fields = $config['fields'];
      $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;
      }));
   }
   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;
      }));
   }
    /**
     * Handle content update/creation
     * @param WP_REST_Request $request
@@ -168,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([
@@ -187,9 +241,6 @@
        }
        $post_type = str_replace('-', '_',jvbCheckBase($params['content']));
      $config = Features::getConfig($params['content']);
        // Build query args
        $args = [
@@ -224,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'];
@@ -312,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;
         }
@@ -389,21 +436,18 @@
            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
            ];
         }
            CacheManager::invalidateGroup($post_data['content']);
            CacheManager::for($post_data['content'])->clear();
         if (jvbSiteUsesFeedBlock()) {
            CacheManager::invalidateGroup($post_data['feed']);
            CacheManager::for('feed')->clear();
         }
        }
        CacheManager::invalidateGroup('user_content');
      if (jvbSiteHasNotifications()) {
         $this->notifications = JVB()->notification();
         $this->notifications->addNotification(
@@ -433,83 +477,241 @@
         return ['success' => false, 'message' => 'No permission'];
      }
      $rows = $post_data['fields'] ?? [];
      if (empty($rows)) {
         return ['success' => false, 'message' => 'No data'];
      }
      $ignore = ['content', 'user'];
      $this->fields = jvbGetFields($post_data['content']);
      $this->initTimelineFields($post_data['content']);
      $fields = jvbGetFields($post_data['content']);
      // Get parent post details
      $parent_post = get_post($parent_id);
      $parent_title = $parent_post->post_title;
      $parent_is_published = ($parent_post->post_status === 'publish');
      // First row = parent post
      $parent_row = array_shift($rows);
      if (($parent_row['id'] ?? null) != $parent_id) {
         return ['success' => false, 'message' => 'Parent ID mismatch'];
      }
      $allowedFields = array_filter($parent_row, function($key) use ($fields) {
         return array_key_exists($key, $fields);
      // 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);
      $parentMeta = new MetaManager($parent_id, 'post');
      $parentMeta->setAll($allowedFields);
      // Get existing children to track deletions
      $existing_children = get_children([
         'post_parent' => $parent_id,
         'post_type' => jvbCheckBase($post_data['content']),
         'fields' => 'ids'
      ]);
      $processed_ids = [];
      // Process remaining rows as children
      foreach ($rows as $index => $row_data) {
         $row_id = $row_data['id'] ?? null;
         // New child post
         if (!$row_id || str_starts_with($row_id, 'new')) {
            $child_id = wp_insert_post([
               'post_type' => jvbCheckBase($post_data['content']),
               'post_parent' => $parent_id,
               'post_author' => $this->user_id,
               'post_status' => $post_data['status'] ?? 'draft',
               'menu_order' => $index
            ]);
         }
         // Existing child post
         else {
            $child_id = (int) $row_id;
            // Verify ownership via parent
            if (!in_array($child_id, $existing_children)) {
               continue; // Skip if not actually a child of this parent
            }
            // Update menu_order (position may have changed)
            wp_update_post([
               'ID' => $child_id,
               'menu_order' => $index
            ]);
         }
         // Update child meta
         $allowedChildFields = array_filter($row_data, function($key) use ($fields) {
            return array_key_exists($key, $fields) && $key !== 'id' && $key !== 'draggable';
      // 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'])) {
         // 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);
         $childMeta = new MetaManager($child_id, 'post');
         $childMeta->setAll($allowedChildFields);
         // Ensure the parent post exists and is still first in the array
         $index = array_search((string)$parent_id, array_column($post_data['timeline'], 'id'));
         $processed_ids[] = $child_id;
         if ($index === false) {
            return [
               'success' => false,
               'message' => 'Missing parent id. This should not have happened'
            ];
         }
         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 = [];
         $existing_children = get_children([
            'post_parent' => $parent_id,
            'orderby' => 'menu_order',
            'post_status' => ['publish', 'draft'],
            'fields'=> 'ids'
         ]);
         $prevDate = null;
         foreach($post_data['timeline'] as $order => $timeline) {
            // 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);
            // 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,
                  'post_status' => $parent_is_published ? 'publish' : 'draft'
               ]);
               if (!$newChild || is_wp_error($newChild)) {
                  $errors[] = [
                     'message' => 'Could not create child post',
                     'data' => $timeline
                  ];
                  continue;
               }
               $timeline['id'] = $newChild;
            }
            if (in_array((int)$timeline['id'], $existing_children)) {
               unset($existing_children[array_search((int)$timeline['id'], $existing_children)]);
            }
            // 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);
            $timeline['id'] = (int) $timeline['id'];
            $success[] = $timeline['id'];
         }
      }
      // Delete removed children
      $deleted_ids = array_diff($existing_children, $processed_ids);
      foreach ($deleted_ids as $delete_id) {
         wp_delete_post($delete_id, true);
      // Delete any remaining children that no longer exist
      if (!empty($existing_children)) {
         foreach ($existing_children as $ID) {
            wp_delete_post($ID);
         }
      }
      return ['success' => true, 'processed' => $processed_ids];
      if ($clearParent) {
         $this->cache->clear();
         CacheManager::onPostSave($parent_id, $parent_post);
      }
      return ['success' => true, 'data' => [
         'success' => $success,
         'errors' => $errors
      ]];
   }
    /**
@@ -590,14 +792,16 @@
     *
     * @return array
     */
    protected function prepareItem(WP_Post $post, $skip = false):array
    protected function prepareItem(WP_Post $post, bool $skip = false, bool $fields = true):array
    {
      if (!$skip && Features::forContent($post->post_type)->has('is_timeline')) {
         $this->initTimelineFields($post->post_type);
         return $this->formatTimeline($post);
      }
        $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,
@@ -605,7 +809,7 @@
            'alt'       => get_post_meta(get_post_thumbnail_id(), '_wp_attachment_image_alt', true),
            'icon'      => $this->post_type,
            'taxonomies'=> [],
            'fields'    => $this->meta->getAll(),
            'fields'    => ($fields) ? $this->meta->getAll() : [],
         'images' => [],
        ];
@@ -633,12 +837,13 @@
        return $data;
    }
   protected function extractImages():array
   protected function extractImages(array $fields = []):array
   {
      //Extract images
      $images = [];
      $get = [];
      foreach ($this->fields as $field => $config) {
      $fields = (empty($fields)) ? $this->fields : $fields;
      foreach ($fields as $field => $config) {
         if ($config['type'] === 'gallery' || $config['type'] === 'image' || $field === 'post_thumbnail') {
            $get[] = $field;
         }
@@ -660,36 +865,29 @@
   protected function formatTimeline(WP_Post $post):array
   {
      $data = $this->prepareItem($post, true);
      $firstRow = $data['fields'];
      $firstRow['id'] = $post->ID;
      $firstRow['draggable'] = false;
      $fields = [$firstRow];
      $item = $this->prepareItem($post, true, false);
      //Step 1: Get the fields that apply to all posts
      $mainMeta = new MetaManager($post->ID, 'post');
      $item['fields'] = $mainMeta->getAll($this->timelineSharedFields);
      $children = get_children(['post_parent' => $post->ID, 'orderby' => 'menu_order']);
      $allImages = [];
      //Step 2: Get the fields for each individual posts
      $children = get_children(['post_parent' => $post->ID, 'orderby' => 'date', 'order' => 'ASC', 'post_status' => ['publish', 'draft'], 'fields'=> 'ids']);
      array_unshift($children, $post->ID);
      $subFields = [];
      $images = [];
      foreach ($children as $child) {
         $this->meta = new MetaManager($child->ID, 'post');
         $row = $this->meta->getAll();  // Store in variable first
         $row['id'] = $child->ID;       // Add ID to the row
         $row['draggable'] = true;      // Mark as draggable
         $fields[] = $row;              // Then append to fields
         $meta = new MetaManager($child, 'post');
         $f = $meta->getAll($this->timelineUniqueFields);
         $f =  ['id' => $child] + $f;
         $subFields[] = $f;
         $images = $this->extractImages();
         if (!empty($images)) {
            $allImages = $allImages + $images;
         }
         $images[$f['post_thumbnail']] = jvbImageData((int) $f['post_thumbnail']);
      }
      $item['fields']['timeline'] = $subFields;
      $item['images'] = $item['images'] + $images;
      if (!empty($allImages)) {
         if (!array_key_exists('images', $data)) {
            $data['images'] = [];
         }
         $data['images'] = $data['images'] + $allImages;
      }
      $data['fields']['timeline'] = $fields;
      return $data;
      return $item;
   }
    /**
@@ -839,9 +1037,8 @@
                    }
                    //Clear cache
                    CacheManager::invalidateGroup($data['content']);
                    CacheManager::invalidateGroup('feed');
                    CacheManager::invalidateGroup('user_content');
               CacheManager::for($data['content'])->clear();
                    CacheManager::for('feed')->clear();
                }
            return [