Files
travelplanning/DATE_FIX_TOUR.md
T

5.6 KiB

To AI Agent: Implement Smart Date-Based Accordion Auto-Expansion and Smooth Viewport Auto-Scroll

1. Context & UI/UX Requirements

We are enhancing the routing UX between MemberDashboard.tsx and ItineraryTimeline.tsx based on image_cc31a4.png.

Functional Specifications:

  1. Date-Matching Evaluation: When a user clicks "Chi tiết hành trình" on a tour card, calculate if the current user system local date falls inside any Stage/Leg timeline block.
  2. Auto-Expansion State: On timeline page load, the matched Leg accordion must be expanded by default (expandedStageId === leg.id).
  3. Smart Auto-Scroll Layout: The viewport must smoothly auto-scroll so that the expanded Leg's title bar lands exactly below the sticky navigation tab bar ("Lộ trình", "Chi phí", etc.), making it fully visible at the top of the viewport without layout clipping.

2. Technical Architecture & Layout Constraints

  • The Sticky Header Challenge: Since the top navigation tab bar uses sticky/fixed positioning, a naive scrollIntoView({ block: 'start' }) will cause the Leg's title to crawl underneath the tabs, hiding it.
  • The Solution: We will inject a dynamic CSS scroll-margin-top parameter (scroll-mt-[70px] or matching header height) on each Leg container, and trigger a minor delayed layout effect pool using a React setTimeout to wait for the DOM accordion expansion reflow before pulling the scroll trigger.

3. Code Refactoring Blueprint

Step 1: Update Navigation Payload Handler in Dashboard Component

Ensure the MemberDashboard.tsx accurately packages the matched target identifier into the client route state bucket:

import { useNavigate } from 'react-router-dom';

const navigate = useNavigate();

const handleNavigateToItinerary = (tour: any) => {
  let targetLegId = null;

  if (tour?.legs && tour.legs.length > 0) {
    const today = new Date();
    today.setHours(0, 0, 0, 0);

    for (const leg of tour.legs) {
      const rawStart = leg.startDate || leg.plannedStart || leg.date;
      const rawEnd = leg.endDate || leg.plannedEnd || leg.date;

      if (rawStart) {
        const startBound = new Date(rawStart);
        startBound.setHours(0, 0, 0, 0);

        const endBound = rawEnd ? new Date(rawEnd) : new Date(rawStart);
        endBound.setHours(23, 59, 59, 999);

        if (today >= startBound && today <= endBound) {
          targetLegId = leg.id;
          break;
        }
      }
    }

    if (!targetLegId) {
      targetLegId = tour.legs[0].id; // Fallback to Chặng 1 if out of tour bounds
    }
  }

  navigate(`/tour/${tour.id}`, { 
    state: { defaultExpandedLegId: targetLegId, shouldScrollToTarget: true } 
  });
};

### Step 2: Inject Safe Identifiers and Scroll Margin inside ItineraryTimeline
Locate the top-level outer container div of each Leg item inside the timeline loop (currentTour.legs.map). Assign a unique id and a scroll margin class utility:

{currentTour.legs.map((leg: any, legIdx: number) => (
  <div 
    key={leg.id}
    id={`leg-anchor-node-${leg.id}`}
    /* CRITICAL: scroll-mt-[70px] leaves a 70px buffer space at the top. 
      Adjust '70px' to match the exact height of your white Tour Navigation Tabs bar!
    */
    className="scroll-mt-[70px] transition-all w-full mb-4"
  >
    {/* Accordion Header Title ("Chặng 2: Dạo chơi ở xứ hoa vàng...") */}
    <div className="flex items-center justify-between ...">
       ...
    </div>
  </div>
))}

### Step 3: Implement Lifecycle Interaction Trigger Engine
Inside ItineraryTimeline.tsx, listen to the passed history route parameters. Set the state, then handle the async smooth scrolling action:

import { useLocation } from 'react-router-dom';

const location = useLocation();
const [expandedStageId, setExpandedStageId] = useState<string | null>(null);

useEffect(() => {
  if (location.state?.defaultExpandedLegId) {
    const targetId = location.state.defaultExpandedLegId;
    
    // 1. Instantly trigger the accordion state layout expansion
    setExpandedStageId(targetId);

    // 2. Schedule a deferred macro-task callback to allow DOM re-renders to finish
    if (location.state?.shouldScrollToTarget) {
      const scrollTimer = setTimeout(() => {
        const targetElement = document.getElementById(`leg-anchor-node-${targetId}`);
        if (targetElement) {
          targetElement.scrollIntoView({ 
            behavior: 'smooth', 
            block: 'start' 
          });
        }
        
        // Clear history router state token flags to prevent repeating scroll on subsequent reload shifts
        window.history.replaceState({}, document.title);
      }, 150); // 150ms ensures smooth accordion height deployment transition finishes

      return () => clearTimeout(scrollTimer);
    }
  } else if (currentTour?.legs && currentTour.legs.length > 0 && !expandedStageId) {
    setExpandedStageId(currentTour.legs[0].id);
  }
}, [location.state, currentTour]);

## 4. Quality Control & Acceptance Verification
[ ] Date Matching Accuracy: Set today's date context matching a target Leg configuration profile. Tap the transition interface button from dashboard. The page must route directly and expand the target container block node.

[ ] Flush Viewport Ceiling Test: The animated target header segment node must slide upwards smoothly. It must lock positions cleanly right below the lowest baseline layer shadow boundary of the Tab Controller without passing behind it.

[ ] State Cleanliness Check: Refreshing or navigating back and forth within the active Timeline panel after the initial landing should not lock or force layout views to keep jumping scroll heights automatically.