Fix timeline display with 24h format and timezone handling

This commit is contained in:
myrmidex 2025-10-07 22:51:10 +02:00
parent 935162ea70
commit cd3a29942f
14 changed files with 408 additions and 143 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@
/docker/data
/.claude
CLAUDE.md
/coverage

View file

@ -41,4 +41,4 @@ RUN mkdir -p storage/app/public storage/framework/cache storage/framework/sessio
EXPOSE 8000
# Start Laravel development server with composer install
CMD sh -c "composer install && php artisan key:generate --force && php artisan serve --host=0.0.0.0 --port=8000"
CMD sh -c "composer install && php artisan key:generate --force && php artisan migrate --force && php artisan serve --host=0.0.0.0 --port=8000"

View file

@ -67,22 +67,26 @@
/* Content Layout */
.trip-detail-content {
flex: 1;
display: flex;
display: grid;
grid-template-columns: 1fr var(--sidebar-width);
gap: 0;
height: calc(100vh - var(--header-height));
min-height: 0;
}
.trip-detail-main {
padding: var(--spacing-xl);
overflow-y: auto;
background: var(--color-bg-secondary);
order: 1;
}
.trip-detail-sidebar {
width: var(--sidebar-width);
background: var(--color-bg-primary);
border-right: 1px solid var(--color-border);
overflow-y: auto;
}
.trip-detail-main {
flex: 1;
padding: var(--spacing-xl);
border-left: 1px solid var(--color-border);
overflow-y: auto;
order: 2;
}
/* Calendar Placeholder */
@ -165,19 +169,23 @@
/* Responsive */
@media (max-width: 768px) {
.trip-detail-content {
flex-direction: column;
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
height: auto;
}
.trip-detail-sidebar {
width: 100%;
border-right: none;
border-bottom: 1px solid var(--color-border);
min-height: 400px;
.trip-detail-main {
order: 2;
padding: var(--spacing-md);
}
.trip-detail-main {
padding: var(--spacing-md);
.trip-detail-sidebar {
order: 1;
width: 100%;
border-left: none;
border-bottom: 1px solid var(--color-border);
min-height: 300px;
max-height: 50vh;
}
.trip-detail-header {

View file

@ -211,40 +211,6 @@ const PlannablesList = ({ tripId, onItemsChange }) => {
)}
</div>
</div>
{/* Calendar Slots Sections */}
{calendarSlots.map(slot => {
const items = plannedItemsBySlot[slot.id] || [];
const slotDate = new Date(slot.slot_date);
const dayName = slotDate.toLocaleDateString('en-US', { weekday: 'long' });
const dateStr = slotDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
return (
<div key={slot.id} className="plannables-section">
<h3 className="section-title">
📅 {slot.name}
<span className="section-date">{dayName}, {dateStr}</span>
{items.length > 0 && (
<span className="item-count">{items.length}</span>
)}
</h3>
<div className="section-items">
{items.length === 0 ? (
<p className="empty-message">No items planned for this day</p>
) : (
items.map(item => (
<PlannableItem
key={item.id}
item={item}
onEdit={handleEditItem}
onDelete={handleDeleteItem}
/>
))
)}
</div>
</div>
);
})}
</div>
{showForm && (

View file

@ -0,0 +1,32 @@
.day-section {
background: var(--bg-card);
border: 1px solid var(--primary-color);
border-radius: var(--radius-lg);
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.day-header {
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 2px solid rgba(228, 93, 4, 0.2);
}
.day-header h4 {
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
margin: 0 0 0.25rem 0;
}
.day-header p {
font-size: 0.9rem;
color: var(--text-secondary);
margin: 0;
}
.hours-grid {
display: flex;
flex-direction: column;
gap: 0.25rem;
}

View file

@ -1,5 +1,6 @@
import { useMemo } from 'react';
import HourRow from './HourRow';
import './DaySection.css';
const START_HOUR = 6; // 6 AM
const END_HOUR = 23; // 11 PM
@ -12,16 +13,16 @@ function DaySection({ date, dayNumber, slots, plannableItems, onScheduleItem })
year: 'numeric'
});
// Memoize slot-to-hour mapping to avoid recalculating on every render
const slotsByHour = useMemo(() => {
const mapping = {};
for (let hour = START_HOUR; hour <= END_HOUR; hour++) {
// Pre-compute hour boundaries once per hour (not per slot)
const hourStart = new Date(date);
hourStart.setHours(hour, 0, 0, 0);
const hourEnd = new Date(date);
hourEnd.setHours(hour, 59, 59, 999);
// Create hour boundaries explicitly to avoid DST issues
const hourStart = new Date(date.getFullYear(), date.getMonth(), date.getDate(), hour, 0, 0, 0);
const hourEnd = new Date(date.getFullYear(), date.getMonth(), date.getDate(), hour, 59, 59, 999);
mapping[hour] = slots.filter(slot => {
if (!slot.datetime_start) return false;
@ -35,18 +36,18 @@ function DaySection({ date, dayNumber, slots, plannableItems, onScheduleItem })
}
return mapping;
}, [slots, date]);
}, [slots, date.getTime()]);
return (
<div className="day-section border border-gray-200 rounded-lg p-4 bg-white shadow-sm">
<div className="day-header mb-4 pb-2 border-b border-gray-200">
<h4 className="text-lg font-semibold text-gray-800">
<div className="day-section">
<div className="day-header">
<h4>
Day {dayNumber}: {dayName}
</h4>
<p className="text-sm text-gray-500">{dateString}</p>
<p>{dateString}</p>
</div>
<div className="hours-grid space-y-1">
<div className="hours-grid">
{Object.keys(slotsByHour).map(hour => (
<HourRow
key={hour}

View file

@ -0,0 +1,98 @@
.hour-row {
display: flex;
align-items: stretch;
border-left: 2px solid var(--primary-color);
min-height: 60px;
transition: background-color 0.2s ease;
position: relative;
}
.hour-row:hover {
background-color: rgba(228, 93, 4, 0.03);
}
.hour-label {
width: 100px;
flex-shrink: 0;
padding: 0.75rem 1rem;
font-size: 0.875rem;
font-weight: 500;
color: var(--text-secondary);
}
.hour-content {
flex: 1;
padding: 0.75rem 1rem;
position: relative;
}
.scheduled-slot {
background: rgba(59, 130, 246, 0.08);
border: 1px solid rgba(59, 130, 246, 0.3);
border-radius: var(--radius-md);
padding: 0.75rem;
margin-bottom: 0.5rem;
}
.scheduled-slot-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.scheduled-slot-content {
flex: 1;
}
.scheduled-slot-title {
font-weight: 600;
color: #1e40af;
margin: 0 0 0.25rem 0;
font-size: 0.95rem;
}
.scheduled-slot-time {
font-size: 0.85rem;
color: #1d4ed8;
margin-bottom: 0.25rem;
}
.scheduled-slot-items {
margin-top: 0.5rem;
font-size: 0.8rem;
color: #2563eb;
}
.add-button {
position: absolute;
right: 0.5rem;
top: 0.5rem;
padding: 0.375rem;
background-color: #22c55e;
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 6px rgba(34, 197, 94, 0.3);
transition: all 0.2s ease;
opacity: 0;
width: 28px;
height: 28px;
}
.hour-row:hover .add-button {
opacity: 1;
}
.add-button:hover {
background-color: #16a34a;
transform: scale(1.1);
}
.add-button svg {
width: 16px;
height: 16px;
}

View file

@ -1,14 +1,13 @@
import { useState } from 'react';
import { PlusIcon } from '@heroicons/react/24/outline';
import ScheduleItemModal from './ScheduleItemModal';
import './HourRow.css';
function HourRow({ hour, date, slots, plannableItems, onScheduleItem }) {
const [showModal, setShowModal] = useState(false);
const formatHour = (h) => {
const period = h >= 12 ? 'PM' : 'AM';
const displayHour = h > 12 ? h - 12 : h === 0 ? 12 : h;
return `${displayHour}:00 ${period}`;
return `${h.toString().padStart(2, '0')}:00`;
};
const handleAddClick = () => {
@ -16,47 +15,56 @@ function HourRow({ hour, date, slots, plannableItems, onScheduleItem }) {
};
const handleSchedule = async (plannableItemId, startDatetime, endDatetime) => {
try {
await onScheduleItem(plannableItemId, startDatetime, endDatetime);
setShowModal(false);
} catch (error) {
// Error is already logged in parent, just rethrow to let modal handle it
throw error;
}
};
// Check if there's a slot that starts at this hour
const slotStartsHere = slots.find(slot => {
// Parse the datetime string and get the local hours
const slotStart = new Date(slot.datetime_start);
return slotStart.getHours() === hour;
const startHour = slotStart.getHours();
return startHour === hour;
});
return (
<>
<div className="hour-row flex items-stretch border-l-2 border-gray-300 hover:bg-gray-50 transition-colors min-h-[60px] group">
<div className="hour-row">
{/* Hour label */}
<div className="hour-label w-24 flex-shrink-0 px-3 py-2 text-sm font-medium text-gray-600">
<div className="hour-label">
{formatHour(hour)}
</div>
{/* Content area */}
<div className="hour-content flex-1 px-3 py-2 relative">
<div className="hour-content">
{/* Scheduled items that start at this hour */}
{slotStartsHere && (
<div className="scheduled-slot bg-blue-50 border border-blue-200 rounded p-2 mb-2">
<div className="flex justify-between items-start">
<div className="flex-1">
<div className="font-medium text-blue-900">
<div className="scheduled-slot">
<div className="scheduled-slot-header">
<div className="scheduled-slot-content">
<div className="scheduled-slot-title">
{slotStartsHere.name}
</div>
<div className="text-sm text-blue-700">
<div className="scheduled-slot-time">
{new Date(slotStartsHere.datetime_start).toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit'
hour: '2-digit',
minute: '2-digit',
hour12: false
})}
{' - '}
{new Date(slotStartsHere.datetime_end).toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit'
hour: '2-digit',
minute: '2-digit',
hour12: false
})}
</div>
{slotStartsHere.planned_items && slotStartsHere.planned_items.length > 0 && (
<div className="mt-1 text-xs text-blue-600">
<div className="scheduled-slot-items">
{slotStartsHere.planned_items.map(pi => pi.plannable_item?.name).join(', ')}
</div>
)}
@ -68,10 +76,10 @@ function HourRow({ hour, date, slots, plannableItems, onScheduleItem }) {
{/* Add button - CSS-only hover */}
<button
onClick={handleAddClick}
className="add-button absolute right-2 top-2 p-1 bg-green-500 text-white rounded-full hover:bg-green-600 transition-all shadow-md opacity-0 group-hover:opacity-100"
className="add-button"
title="Schedule an item"
>
<PlusIcon className="w-4 h-4" />
<PlusIcon />
</button>
</div>
</div>

View file

@ -0,0 +1,98 @@
.schedule-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.schedule-form-error {
background-color: #fee;
border: 1px solid #fcc;
color: #c33;
padding: 0.75rem 1rem;
border-radius: var(--border-radius-md);
font-size: 0.9rem;
}
.schedule-form-group {
display: flex;
flex-direction: column;
}
.schedule-form-label {
display: block;
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text-primary);
margin-bottom: 0.5rem;
}
.schedule-form-select {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--color-border);
border-radius: var(--border-radius-md);
font-size: 1rem;
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
transition: border-color var(--transition-normal);
}
.schedule-form-select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(228, 93, 4, 0.1);
}
.schedule-time-group {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.schedule-form-actions {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
padding-top: 1rem;
border-top: 1px solid var(--color-border-light);
}
.schedule-btn-cancel {
padding: 0.75rem 1.5rem;
color: var(--color-text-primary);
background-color: var(--color-bg-secondary);
border: none;
border-radius: var(--border-radius-md);
cursor: pointer;
font-weight: 500;
transition: background-color var(--transition-normal);
}
.schedule-btn-cancel:hover:not(:disabled) {
background-color: var(--color-border);
}
.schedule-btn-cancel:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.schedule-btn-submit {
padding: 0.75rem 1.5rem;
color: white;
background-color: var(--primary-color);
border: none;
border-radius: var(--border-radius-md);
cursor: pointer;
font-weight: 500;
transition: background-color var(--transition-normal);
}
.schedule-btn-submit:hover:not(:disabled) {
background-color: var(--primary-hover);
}
.schedule-btn-submit:disabled {
background-color: var(--color-border);
cursor: not-allowed;
}

View file

@ -1,5 +1,6 @@
import { useState, useEffect, useMemo } from 'react';
import BaseModal from '../BaseModal';
import './ScheduleItemModal.css';
function ScheduleItemModal({ date, hour, plannableItems, onSchedule, onClose }) {
const [selectedItemId, setSelectedItemId] = useState('');
@ -30,8 +31,11 @@ function ScheduleItemModal({ date, hour, plannableItems, onSchedule, onClose })
return;
}
// Construct full datetime strings
const dateString = date.toISOString().split('T')[0];
// Construct full datetime strings using local date
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const dateString = `${year}-${month}-${day}`;
const startDatetime = `${dateString} ${startTime}:00`;
const endDatetime = `${dateString} ${endTime}:00`;
@ -44,9 +48,10 @@ function ScheduleItemModal({ date, hour, plannableItems, onSchedule, onClose })
try {
setSubmitting(true);
await onSchedule(selectedItemId, startDatetime, endDatetime);
// Modal will be closed by parent (HourRow) on success
} catch (err) {
setError(err.response?.data?.message || 'Failed to schedule item');
} finally {
console.error('Error in modal submit:', err);
setError(err.response?.data?.message || err.message || 'Failed to schedule item');
setSubmitting(false);
}
};
@ -64,22 +69,22 @@ function ScheduleItemModal({ date, hour, plannableItems, onSchedule, onClose })
}, []);
return (
<BaseModal onClose={onClose} title="Schedule Item">
<form onSubmit={handleSubmit} className="space-y-4">
<BaseModal isOpen={true} onClose={onClose} title="Schedule Item">
<form onSubmit={handleSubmit} className="schedule-form">
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-2 rounded">
<div className="schedule-form-error">
{error}
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<div className="schedule-form-group">
<label className="schedule-form-label">
Select Item
</label>
<select
value={selectedItemId}
onChange={(e) => setSelectedItemId(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
className="schedule-form-select"
required
>
<option value="">-- Choose an item --</option>
@ -91,15 +96,15 @@ function ScheduleItemModal({ date, hour, plannableItems, onSchedule, onClose })
</select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<div className="schedule-time-group">
<div className="schedule-form-group">
<label className="schedule-form-label">
Start Time
</label>
<select
value={startTime}
onChange={(e) => setStartTime(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
className="schedule-form-select"
required
>
{timeOptions.map(time => (
@ -110,14 +115,14 @@ function ScheduleItemModal({ date, hour, plannableItems, onSchedule, onClose })
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<div className="schedule-form-group">
<label className="schedule-form-label">
End Time
</label>
<select
value={endTime}
onChange={(e) => setEndTime(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
className="schedule-form-select"
required
>
{timeOptions.map(time => (
@ -129,18 +134,18 @@ function ScheduleItemModal({ date, hour, plannableItems, onSchedule, onClose })
</div>
</div>
<div className="flex justify-end gap-2 pt-4">
<div className="schedule-form-actions">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-gray-700 bg-gray-100 rounded hover:bg-gray-200 transition-colors"
className="schedule-btn-cancel"
disabled={submitting}
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 text-white bg-blue-600 rounded hover:bg-blue-700 transition-colors disabled:bg-blue-300"
className="schedule-btn-submit"
disabled={submitting}
>
{submitting ? 'Scheduling...' : 'Schedule'}

View file

@ -0,0 +1,24 @@
.trip-timeline {
width: 100%;
}
.trip-timeline h3 {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 1.5rem;
color: var(--text-primary);
font-family: var(--font-secondary);
}
.timeline-days-container {
display: flex;
flex-direction: column;
gap: 2rem;
}
.timeline-loading {
padding: 2rem;
color: var(--text-muted);
text-align: center;
font-size: 1.1rem;
}

View file

@ -1,6 +1,7 @@
import { useState, useEffect } from 'react';
import DaySection from './DaySection';
import axios from 'axios';
import './TripTimeline.css';
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000';
@ -18,7 +19,8 @@ function TripTimeline({ trip, plannableItems, onScheduleSuccess }) {
const response = await axios.get(`${API_URL}/api/trips/${trip.id}/calendar-slots`, {
headers: { Authorization: `Bearer ${token}` }
});
setCalendarSlots(response.data.data || []);
const slots = response.data.data || [];
setCalendarSlots(slots);
} catch (error) {
console.error('Error fetching calendar slots:', error);
} finally {
@ -29,14 +31,16 @@ function TripTimeline({ trip, plannableItems, onScheduleSuccess }) {
const handleScheduleItem = async (plannableItemId, startDatetime, endDatetime) => {
try {
const token = localStorage.getItem('token');
await axios.post(
`${API_URL}/api/planned-items`,
{
const payload = {
plannable_item_id: plannableItemId,
trip_id: trip.id,
start_datetime: startDatetime,
end_datetime: endDatetime,
},
};
const response = await axios.post(
`${API_URL}/api/planned-items`,
payload,
{
headers: { Authorization: `Bearer ${token}` }
}
@ -48,6 +52,7 @@ function TripTimeline({ trip, plannableItems, onScheduleSuccess }) {
}
} catch (error) {
console.error('Error scheduling item:', error);
console.error('Error response:', error.response?.data);
throw error;
}
};
@ -57,31 +62,42 @@ function TripTimeline({ trip, plannableItems, onScheduleSuccess }) {
const start = new Date(trip.start_date);
const end = new Date(trip.end_date);
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
for (let d = new Date(start); d <= end; ) {
days.push(new Date(d));
d.setDate(d.getDate() + 1);
}
return days;
};
const getSlotsForDay = (date) => {
const dateString = date.toISOString().split('T')[0];
return calendarSlots.filter(slot => slot.slot_date === dateString);
// Format date as YYYY-MM-DD in local timezone, not UTC
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const dateString = `${year}-${month}-${day}`;
const slots = calendarSlots.filter(slot => {
// slot_date comes as "2025-12-12T00:00:00.000000Z", extract just the date part
const slotDateString = slot.slot_date.split('T')[0];
return slotDateString === dateString;
});
return slots;
};
if (loading) {
return <div className="p-4 text-gray-500">Loading timeline...</div>;
return <div className="timeline-loading">Loading timeline...</div>;
}
const days = generateDays();
return (
<div className="trip-timeline">
<h3 className="text-xl font-semibold mb-4">Trip Timeline</h3>
<div className="space-y-8">
<h3>Trip Timeline</h3>
<div className="timeline-days-container">
{days.map((day, index) => (
<DaySection
key={day.toISOString()}
key={`${day.getFullYear()}-${day.getMonth()}-${day.getDate()}`}
date={day}
dayNumber={index + 1}
slots={getSlotsForDay(day)}

View file

@ -2,5 +2,5 @@
"testEnvironment": "node",
"testMatch": ["**/specs/**/*.test.js"],
"setupFilesAfterEnv": ["<rootDir>/support/config/jest.setup.local.js"],
"testTimeout": 30000
"testTimeout": 60000
}

View file

@ -110,9 +110,14 @@ describe('Timeline Scheduling Feature Test', () => {
// Helper function to create a plannable item
async function createPlannableItem(itemData) {
// Click Add Item button in sidebar
// Ensure no modal is already open
await driver.sleep(500);
// Click Add Item button in sidebar using JavaScript click
const addItemButton = await driver.findElement(By.xpath('//button[contains(text(), "Add Item")]'));
await addItemButton.click();
await driver.executeScript("arguments[0].scrollIntoView(true);", addItemButton);
await driver.sleep(300);
await driver.executeScript("arguments[0].click();", addItemButton);
// Wait for form modal
await driver.wait(until.elementLocated(By.className('plannable-form-modal')), 5000);
@ -142,7 +147,7 @@ describe('Timeline Scheduling Feature Test', () => {
// Wait for modal to close
await driver.sleep(1500);
await driver.wait(async () => {
const overlays = await driver.findElements(By.css('.plannable-form-overlay'));
const overlays = await driver.findElements(By.css('.plannable-form-overlay, .plannable-form-modal'));
for (const overlay of overlays) {
try {
if (await overlay.isDisplayed()) {
@ -154,6 +159,9 @@ describe('Timeline Scheduling Feature Test', () => {
}
return true;
}, 10000);
// Additional wait to ensure UI is stable
await driver.sleep(500);
}
// Helper to wait for modal close
@ -211,15 +219,15 @@ describe('Timeline Scheduling Feature Test', () => {
// Get first day section
const firstDaySection = await driver.findElement(By.className('day-section'));
// Should have hour rows (6 AM to 11 PM = 18 hours)
// Should have hour rows (06:00 to 23:00 = 18 hours)
const hourRows = await firstDaySection.findElements(By.className('hour-row'));
expect(hourRows.length).toBeGreaterThanOrEqual(18);
// Verify specific hour labels exist
const hour6am = await driver.findElement(By.xpath('//div[contains(@class, "hour-label") and contains(text(), "6:00 AM")]'));
const hour6am = await driver.findElement(By.xpath('//div[contains(@class, "hour-label") and contains(text(), "06:00")]'));
expect(await hour6am.isDisplayed()).toBe(true);
const hour11pm = await driver.findElement(By.xpath('//div[contains(@class, "hour-label") and contains(text(), "11:00 PM")]'));
const hour11pm = await driver.findElement(By.xpath('//div[contains(@class, "hour-label") and contains(text(), "23:00")]'));
expect(await hour11pm.isDisplayed()).toBe(true);
});
});
@ -250,8 +258,8 @@ describe('Timeline Scheduling Feature Test', () => {
notes: 'Morning coffee'
});
// Now find the 8 AM hour row
const hour8am = await driver.findElement(By.xpath('//div[contains(@class, "hour-label") and contains(text(), "8:00 AM")]/following-sibling::div[contains(@class, "hour-content")]'));
// Now find the 08:00 hour row
const hour8am = await driver.findElement(By.xpath('//div[contains(@class, "hour-label") and contains(text(), "08:00")]/following-sibling::div[contains(@class, "hour-content")]'));
// Hover to show + button
await driver.actions().move({ origin: hour8am }).perform();
@ -285,11 +293,11 @@ describe('Timeline Scheduling Feature Test', () => {
await driver.sleep(1000);
// Find the 10 AM hour row on Day 1
// Find the 10:00 hour row on Day 1
const daySections = await driver.findElements(By.className('day-section'));
const day1Section = daySections[0];
const hour10am = await day1Section.findElement(By.xpath('.//div[contains(@class, "hour-label") and contains(text(), "10:00 AM")]/following-sibling::div[contains(@class, "hour-content")]'));
const hour10am = await day1Section.findElement(By.xpath('.//div[contains(@class, "hour-label") and contains(text(), "10:00")]/following-sibling::div[contains(@class, "hour-content")]'));
// Hover and click +
await driver.actions().move({ origin: hour10am }).perform();
@ -321,12 +329,12 @@ describe('Timeline Scheduling Feature Test', () => {
// Wait for modal to close
await waitForModalClose();
// Verify item appears in timeline at 10 AM
// Verify item appears in timeline at 10:00
await driver.sleep(1000);
const scheduledItem = await driver.findElement(By.xpath('//div[contains(@class, "scheduled-slot") and contains(., "Louvre Museum Visit")]'));
expect(await scheduledItem.isDisplayed()).toBe(true);
// Verify time display shows 10:00 AM - 12:00 PM
// Verify time display shows 10:00 - 12:00
const timeDisplay = await scheduledItem.getText();
expect(timeDisplay).toContain('10:00');
expect(timeDisplay).toContain('12:00');
@ -350,11 +358,11 @@ describe('Timeline Scheduling Feature Test', () => {
await driver.sleep(1000);
// Schedule first item at 8 AM
// Schedule first item at 08:00
const daySections = await driver.findElements(By.className('day-section'));
const day2Section = daySections[1]; // Day 2
const hour8am = await day2Section.findElement(By.xpath('.//div[contains(@class, "hour-label") and contains(text(), "8:00 AM")]/following-sibling::div[contains(@class, "hour-content")]'));
const hour8am = await day2Section.findElement(By.xpath('.//div[contains(@class, "hour-label") and contains(text(), "08:00")]/following-sibling::div[contains(@class, "hour-content")]'));
await driver.actions().move({ origin: hour8am }).perform();
await driver.sleep(500);
@ -371,8 +379,8 @@ describe('Timeline Scheduling Feature Test', () => {
await driver.sleep(1000);
// Schedule second item at 3 PM
const hour3pm = await day2Section.findElement(By.xpath('.//div[contains(@class, "hour-label") and contains(text(), "3:00 PM")]/following-sibling::div[contains(@class, "hour-content")]'));
// Schedule second item at 15:00
const hour3pm = await day2Section.findElement(By.xpath('.//div[contains(@class, "hour-label") and contains(text(), "15:00")]/following-sibling::div[contains(@class, "hour-content")]'));
await driver.actions().move({ origin: hour3pm }).perform();
await driver.sleep(500);
@ -411,11 +419,11 @@ describe('Timeline Scheduling Feature Test', () => {
await driver.sleep(1000);
// Open schedule modal at 2 PM
// Open schedule modal at 14:00
const daySections = await driver.findElements(By.className('day-section'));
const day3Section = daySections[2]; // Day 3
const hour2pm = await day3Section.findElement(By.xpath('.//div[contains(@class, "hour-label") and contains(text(), "2:00 PM")]/following-sibling::div[contains(@class, "hour-content")]'));
const hour2pm = await day3Section.findElement(By.xpath('.//div[contains(@class, "hour-label") and contains(text(), "14:00")]/following-sibling::div[contains(@class, "hour-content")]'));
await driver.actions().move({ origin: hour2pm }).perform();
await driver.sleep(500);
@ -463,7 +471,7 @@ describe('Timeline Scheduling Feature Test', () => {
const daySections = await driver.findElements(By.className('day-section'));
const day1Section = daySections[0];
const hour11am = await day1Section.findElement(By.xpath('.//div[contains(@class, "hour-label") and contains(text(), "11:00 AM")]/following-sibling::div[contains(@class, "hour-content")]'));
const hour11am = await day1Section.findElement(By.xpath('.//div[contains(@class, "hour-label") and contains(text(), "11:00")]/following-sibling::div[contains(@class, "hour-content")]'));
await driver.actions().move({ origin: hour11am }).perform();
await driver.sleep(500);
@ -502,8 +510,8 @@ describe('Timeline Scheduling Feature Test', () => {
const daySections = await driver.findElements(By.className('day-section'));
const day1Section = daySections[0];
// Schedule Dinner first (7 PM)
const hour7pm = await day1Section.findElement(By.xpath('.//div[contains(@class, "hour-label") and contains(text(), "7:00 PM")]/following-sibling::div[contains(@class, "hour-content")]'));
// Schedule Dinner first (19:00)
const hour7pm = await day1Section.findElement(By.xpath('.//div[contains(@class, "hour-label") and contains(text(), "19:00")]/following-sibling::div[contains(@class, "hour-content")]'));
await driver.actions().move({ origin: hour7pm }).perform();
await driver.sleep(500);
await hour7pm.findElement(By.css('button')).click();
@ -515,8 +523,8 @@ describe('Timeline Scheduling Feature Test', () => {
await waitForModalClose();
await driver.sleep(1000);
// Schedule Breakfast (7 AM)
const hour7am = await day1Section.findElement(By.xpath('.//div[contains(@class, "hour-label") and contains(text(), "7:00 AM")]/following-sibling::div[contains(@class, "hour-content")]'));
// Schedule Breakfast (07:00)
const hour7am = await day1Section.findElement(By.xpath('.//div[contains(@class, "hour-label") and contains(text(), "07:00")]/following-sibling::div[contains(@class, "hour-content")]'));
await driver.actions().move({ origin: hour7am }).perform();
await driver.sleep(500);
await hour7am.findElement(By.css('button')).click();
@ -528,8 +536,8 @@ describe('Timeline Scheduling Feature Test', () => {
await waitForModalClose();
await driver.sleep(1000);
// Schedule Lunch (12 PM)
const hour12pm = await day1Section.findElement(By.xpath('.//div[contains(@class, "hour-label") and contains(text(), "12:00 PM")]/following-sibling::div[contains(@class, "hour-content")]'));
// Schedule Lunch (12:00)
const hour12pm = await day1Section.findElement(By.xpath('.//div[contains(@class, "hour-label") and contains(text(), "12:00")]/following-sibling::div[contains(@class, "hour-content")]'));
await driver.actions().move({ origin: hour12pm }).perform();
await driver.sleep(500);
await hour12pm.findElement(By.css('button')).click();