Fix timeline display with 24h format and timezone handling
This commit is contained in:
parent
935162ea70
commit
cd3a29942f
14 changed files with 408 additions and 143 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -2,3 +2,4 @@
|
||||||
/docker/data
|
/docker/data
|
||||||
/.claude
|
/.claude
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
|
/coverage
|
||||||
|
|
@ -41,4 +41,4 @@ RUN mkdir -p storage/app/public storage/framework/cache storage/framework/sessio
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
# Start Laravel development server with composer install
|
# 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"
|
||||||
|
|
@ -67,22 +67,26 @@
|
||||||
/* Content Layout */
|
/* Content Layout */
|
||||||
.trip-detail-content {
|
.trip-detail-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: grid;
|
||||||
|
grid-template-columns: 1fr var(--sidebar-width);
|
||||||
gap: 0;
|
gap: 0;
|
||||||
height: calc(100vh - var(--header-height));
|
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 {
|
.trip-detail-sidebar {
|
||||||
width: var(--sidebar-width);
|
width: var(--sidebar-width);
|
||||||
background: var(--color-bg-primary);
|
background: var(--color-bg-primary);
|
||||||
border-right: 1px solid var(--color-border);
|
border-left: 1px solid var(--color-border);
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trip-detail-main {
|
|
||||||
flex: 1;
|
|
||||||
padding: var(--spacing-xl);
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
order: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Calendar Placeholder */
|
/* Calendar Placeholder */
|
||||||
|
|
@ -165,19 +169,23 @@
|
||||||
/* Responsive */
|
/* Responsive */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.trip-detail-content {
|
.trip-detail-content {
|
||||||
flex-direction: column;
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trip-detail-sidebar {
|
.trip-detail-main {
|
||||||
width: 100%;
|
order: 2;
|
||||||
border-right: none;
|
padding: var(--spacing-md);
|
||||||
border-bottom: 1px solid var(--color-border);
|
|
||||||
min-height: 400px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.trip-detail-main {
|
.trip-detail-sidebar {
|
||||||
padding: var(--spacing-md);
|
order: 1;
|
||||||
|
width: 100%;
|
||||||
|
border-left: none;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
min-height: 300px;
|
||||||
|
max-height: 50vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trip-detail-header {
|
.trip-detail-header {
|
||||||
|
|
|
||||||
|
|
@ -211,40 +211,6 @@ const PlannablesList = ({ tripId, onItemsChange }) => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{showForm && (
|
{showForm && (
|
||||||
|
|
|
||||||
32
frontend/src/components/timeline/DaySection.css
Normal file
32
frontend/src/components/timeline/DaySection.css
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import HourRow from './HourRow';
|
import HourRow from './HourRow';
|
||||||
|
import './DaySection.css';
|
||||||
|
|
||||||
const START_HOUR = 6; // 6 AM
|
const START_HOUR = 6; // 6 AM
|
||||||
const END_HOUR = 23; // 11 PM
|
const END_HOUR = 23; // 11 PM
|
||||||
|
|
@ -12,16 +13,16 @@ function DaySection({ date, dayNumber, slots, plannableItems, onScheduleItem })
|
||||||
year: 'numeric'
|
year: 'numeric'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Memoize slot-to-hour mapping to avoid recalculating on every render
|
// Memoize slot-to-hour mapping to avoid recalculating on every render
|
||||||
const slotsByHour = useMemo(() => {
|
const slotsByHour = useMemo(() => {
|
||||||
const mapping = {};
|
const mapping = {};
|
||||||
|
|
||||||
for (let hour = START_HOUR; hour <= END_HOUR; hour++) {
|
for (let hour = START_HOUR; hour <= END_HOUR; hour++) {
|
||||||
// Pre-compute hour boundaries once per hour (not per slot)
|
// Pre-compute hour boundaries once per hour (not per slot)
|
||||||
const hourStart = new Date(date);
|
// Create hour boundaries explicitly to avoid DST issues
|
||||||
hourStart.setHours(hour, 0, 0, 0);
|
const hourStart = new Date(date.getFullYear(), date.getMonth(), date.getDate(), hour, 0, 0, 0);
|
||||||
const hourEnd = new Date(date);
|
const hourEnd = new Date(date.getFullYear(), date.getMonth(), date.getDate(), hour, 59, 59, 999);
|
||||||
hourEnd.setHours(hour, 59, 59, 999);
|
|
||||||
|
|
||||||
mapping[hour] = slots.filter(slot => {
|
mapping[hour] = slots.filter(slot => {
|
||||||
if (!slot.datetime_start) return false;
|
if (!slot.datetime_start) return false;
|
||||||
|
|
@ -35,18 +36,18 @@ function DaySection({ date, dayNumber, slots, plannableItems, onScheduleItem })
|
||||||
}
|
}
|
||||||
|
|
||||||
return mapping;
|
return mapping;
|
||||||
}, [slots, date]);
|
}, [slots, date.getTime()]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="day-section border border-gray-200 rounded-lg p-4 bg-white shadow-sm">
|
<div className="day-section">
|
||||||
<div className="day-header mb-4 pb-2 border-b border-gray-200">
|
<div className="day-header">
|
||||||
<h4 className="text-lg font-semibold text-gray-800">
|
<h4>
|
||||||
Day {dayNumber}: {dayName}
|
Day {dayNumber}: {dayName}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm text-gray-500">{dateString}</p>
|
<p>{dateString}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hours-grid space-y-1">
|
<div className="hours-grid">
|
||||||
{Object.keys(slotsByHour).map(hour => (
|
{Object.keys(slotsByHour).map(hour => (
|
||||||
<HourRow
|
<HourRow
|
||||||
key={hour}
|
key={hour}
|
||||||
|
|
|
||||||
98
frontend/src/components/timeline/HourRow.css
Normal file
98
frontend/src/components/timeline/HourRow.css
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { PlusIcon } from '@heroicons/react/24/outline';
|
import { PlusIcon } from '@heroicons/react/24/outline';
|
||||||
import ScheduleItemModal from './ScheduleItemModal';
|
import ScheduleItemModal from './ScheduleItemModal';
|
||||||
|
import './HourRow.css';
|
||||||
|
|
||||||
function HourRow({ hour, date, slots, plannableItems, onScheduleItem }) {
|
function HourRow({ hour, date, slots, plannableItems, onScheduleItem }) {
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
|
||||||
const formatHour = (h) => {
|
const formatHour = (h) => {
|
||||||
const period = h >= 12 ? 'PM' : 'AM';
|
return `${h.toString().padStart(2, '0')}:00`;
|
||||||
const displayHour = h > 12 ? h - 12 : h === 0 ? 12 : h;
|
|
||||||
return `${displayHour}:00 ${period}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddClick = () => {
|
const handleAddClick = () => {
|
||||||
|
|
@ -16,47 +15,56 @@ function HourRow({ hour, date, slots, plannableItems, onScheduleItem }) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSchedule = async (plannableItemId, startDatetime, endDatetime) => {
|
const handleSchedule = async (plannableItemId, startDatetime, endDatetime) => {
|
||||||
|
try {
|
||||||
await onScheduleItem(plannableItemId, startDatetime, endDatetime);
|
await onScheduleItem(plannableItemId, startDatetime, endDatetime);
|
||||||
setShowModal(false);
|
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
|
// Check if there's a slot that starts at this hour
|
||||||
const slotStartsHere = slots.find(slot => {
|
const slotStartsHere = slots.find(slot => {
|
||||||
|
// Parse the datetime string and get the local hours
|
||||||
const slotStart = new Date(slot.datetime_start);
|
const slotStart = new Date(slot.datetime_start);
|
||||||
return slotStart.getHours() === hour;
|
const startHour = slotStart.getHours();
|
||||||
|
return startHour === hour;
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* 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)}
|
{formatHour(hour)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content area */}
|
{/* Content area */}
|
||||||
<div className="hour-content flex-1 px-3 py-2 relative">
|
<div className="hour-content">
|
||||||
{/* Scheduled items that start at this hour */}
|
{/* Scheduled items that start at this hour */}
|
||||||
{slotStartsHere && (
|
{slotStartsHere && (
|
||||||
<div className="scheduled-slot bg-blue-50 border border-blue-200 rounded p-2 mb-2">
|
<div className="scheduled-slot">
|
||||||
<div className="flex justify-between items-start">
|
<div className="scheduled-slot-header">
|
||||||
<div className="flex-1">
|
<div className="scheduled-slot-content">
|
||||||
<div className="font-medium text-blue-900">
|
<div className="scheduled-slot-title">
|
||||||
{slotStartsHere.name}
|
{slotStartsHere.name}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-blue-700">
|
<div className="scheduled-slot-time">
|
||||||
{new Date(slotStartsHere.datetime_start).toLocaleTimeString('en-US', {
|
{new Date(slotStartsHere.datetime_start).toLocaleTimeString('en-US', {
|
||||||
hour: 'numeric',
|
hour: '2-digit',
|
||||||
minute: '2-digit'
|
minute: '2-digit',
|
||||||
|
hour12: false
|
||||||
})}
|
})}
|
||||||
{' - '}
|
{' - '}
|
||||||
{new Date(slotStartsHere.datetime_end).toLocaleTimeString('en-US', {
|
{new Date(slotStartsHere.datetime_end).toLocaleTimeString('en-US', {
|
||||||
hour: 'numeric',
|
hour: '2-digit',
|
||||||
minute: '2-digit'
|
minute: '2-digit',
|
||||||
|
hour12: false
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{slotStartsHere.planned_items && slotStartsHere.planned_items.length > 0 && (
|
{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(', ')}
|
{slotStartsHere.planned_items.map(pi => pi.plannable_item?.name).join(', ')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -68,10 +76,10 @@ function HourRow({ hour, date, slots, plannableItems, onScheduleItem }) {
|
||||||
{/* Add button - CSS-only hover */}
|
{/* Add button - CSS-only hover */}
|
||||||
<button
|
<button
|
||||||
onClick={handleAddClick}
|
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"
|
title="Schedule an item"
|
||||||
>
|
>
|
||||||
<PlusIcon className="w-4 h-4" />
|
<PlusIcon />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
98
frontend/src/components/timeline/ScheduleItemModal.css
Normal file
98
frontend/src/components/timeline/ScheduleItemModal.css
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import BaseModal from '../BaseModal';
|
import BaseModal from '../BaseModal';
|
||||||
|
import './ScheduleItemModal.css';
|
||||||
|
|
||||||
function ScheduleItemModal({ date, hour, plannableItems, onSchedule, onClose }) {
|
function ScheduleItemModal({ date, hour, plannableItems, onSchedule, onClose }) {
|
||||||
const [selectedItemId, setSelectedItemId] = useState('');
|
const [selectedItemId, setSelectedItemId] = useState('');
|
||||||
|
|
@ -30,8 +31,11 @@ function ScheduleItemModal({ date, hour, plannableItems, onSchedule, onClose })
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct full datetime strings
|
// Construct full datetime strings using local date
|
||||||
const dateString = date.toISOString().split('T')[0];
|
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 startDatetime = `${dateString} ${startTime}:00`;
|
||||||
const endDatetime = `${dateString} ${endTime}:00`;
|
const endDatetime = `${dateString} ${endTime}:00`;
|
||||||
|
|
||||||
|
|
@ -44,9 +48,10 @@ function ScheduleItemModal({ date, hour, plannableItems, onSchedule, onClose })
|
||||||
try {
|
try {
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
await onSchedule(selectedItemId, startDatetime, endDatetime);
|
await onSchedule(selectedItemId, startDatetime, endDatetime);
|
||||||
|
// Modal will be closed by parent (HourRow) on success
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.response?.data?.message || 'Failed to schedule item');
|
console.error('Error in modal submit:', err);
|
||||||
} finally {
|
setError(err.response?.data?.message || err.message || 'Failed to schedule item');
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -64,22 +69,22 @@ function ScheduleItemModal({ date, hour, plannableItems, onSchedule, onClose })
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseModal onClose={onClose} title="Schedule Item">
|
<BaseModal isOpen={true} onClose={onClose} title="Schedule Item">
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="schedule-form">
|
||||||
{error && (
|
{error && (
|
||||||
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-2 rounded">
|
<div className="schedule-form-error">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div className="schedule-form-group">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="schedule-form-label">
|
||||||
Select Item
|
Select Item
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={selectedItemId}
|
value={selectedItemId}
|
||||||
onChange={(e) => setSelectedItemId(e.target.value)}
|
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
|
required
|
||||||
>
|
>
|
||||||
<option value="">-- Choose an item --</option>
|
<option value="">-- Choose an item --</option>
|
||||||
|
|
@ -91,15 +96,15 @@ function ScheduleItemModal({ date, hour, plannableItems, onSchedule, onClose })
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="schedule-time-group">
|
||||||
<div>
|
<div className="schedule-form-group">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="schedule-form-label">
|
||||||
Start Time
|
Start Time
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={startTime}
|
value={startTime}
|
||||||
onChange={(e) => setStartTime(e.target.value)}
|
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
|
required
|
||||||
>
|
>
|
||||||
{timeOptions.map(time => (
|
{timeOptions.map(time => (
|
||||||
|
|
@ -110,14 +115,14 @@ function ScheduleItemModal({ date, hour, plannableItems, onSchedule, onClose })
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="schedule-form-group">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="schedule-form-label">
|
||||||
End Time
|
End Time
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={endTime}
|
value={endTime}
|
||||||
onChange={(e) => setEndTime(e.target.value)}
|
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
|
required
|
||||||
>
|
>
|
||||||
{timeOptions.map(time => (
|
{timeOptions.map(time => (
|
||||||
|
|
@ -129,18 +134,18 @@ function ScheduleItemModal({ date, hour, plannableItems, onSchedule, onClose })
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 pt-4">
|
<div className="schedule-form-actions">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
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}
|
disabled={submitting}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
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}
|
disabled={submitting}
|
||||||
>
|
>
|
||||||
{submitting ? 'Scheduling...' : 'Schedule'}
|
{submitting ? 'Scheduling...' : 'Schedule'}
|
||||||
|
|
|
||||||
24
frontend/src/components/timeline/TripTimeline.css
Normal file
24
frontend/src/components/timeline/TripTimeline.css
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import DaySection from './DaySection';
|
import DaySection from './DaySection';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import './TripTimeline.css';
|
||||||
|
|
||||||
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000';
|
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`, {
|
const response = await axios.get(`${API_URL}/api/trips/${trip.id}/calendar-slots`, {
|
||||||
headers: { Authorization: `Bearer ${token}` }
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
});
|
});
|
||||||
setCalendarSlots(response.data.data || []);
|
const slots = response.data.data || [];
|
||||||
|
setCalendarSlots(slots);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching calendar slots:', error);
|
console.error('Error fetching calendar slots:', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -29,14 +31,16 @@ function TripTimeline({ trip, plannableItems, onScheduleSuccess }) {
|
||||||
const handleScheduleItem = async (plannableItemId, startDatetime, endDatetime) => {
|
const handleScheduleItem = async (plannableItemId, startDatetime, endDatetime) => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
await axios.post(
|
const payload = {
|
||||||
`${API_URL}/api/planned-items`,
|
|
||||||
{
|
|
||||||
plannable_item_id: plannableItemId,
|
plannable_item_id: plannableItemId,
|
||||||
trip_id: trip.id,
|
trip_id: trip.id,
|
||||||
start_datetime: startDatetime,
|
start_datetime: startDatetime,
|
||||||
end_datetime: endDatetime,
|
end_datetime: endDatetime,
|
||||||
},
|
};
|
||||||
|
|
||||||
|
const response = await axios.post(
|
||||||
|
`${API_URL}/api/planned-items`,
|
||||||
|
payload,
|
||||||
{
|
{
|
||||||
headers: { Authorization: `Bearer ${token}` }
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
}
|
}
|
||||||
|
|
@ -48,6 +52,7 @@ function TripTimeline({ trip, plannableItems, onScheduleSuccess }) {
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error scheduling item:', error);
|
console.error('Error scheduling item:', error);
|
||||||
|
console.error('Error response:', error.response?.data);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -57,31 +62,42 @@ function TripTimeline({ trip, plannableItems, onScheduleSuccess }) {
|
||||||
const start = new Date(trip.start_date);
|
const start = new Date(trip.start_date);
|
||||||
const end = new Date(trip.end_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));
|
days.push(new Date(d));
|
||||||
|
d.setDate(d.getDate() + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return days;
|
return days;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSlotsForDay = (date) => {
|
const getSlotsForDay = (date) => {
|
||||||
const dateString = date.toISOString().split('T')[0];
|
// Format date as YYYY-MM-DD in local timezone, not UTC
|
||||||
return calendarSlots.filter(slot => slot.slot_date === dateString);
|
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) {
|
if (loading) {
|
||||||
return <div className="p-4 text-gray-500">Loading timeline...</div>;
|
return <div className="timeline-loading">Loading timeline...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const days = generateDays();
|
const days = generateDays();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="trip-timeline">
|
<div className="trip-timeline">
|
||||||
<h3 className="text-xl font-semibold mb-4">Trip Timeline</h3>
|
<h3>Trip Timeline</h3>
|
||||||
<div className="space-y-8">
|
<div className="timeline-days-container">
|
||||||
{days.map((day, index) => (
|
{days.map((day, index) => (
|
||||||
<DaySection
|
<DaySection
|
||||||
key={day.toISOString()}
|
key={`${day.getFullYear()}-${day.getMonth()}-${day.getDate()}`}
|
||||||
date={day}
|
date={day}
|
||||||
dayNumber={index + 1}
|
dayNumber={index + 1}
|
||||||
slots={getSlotsForDay(day)}
|
slots={getSlotsForDay(day)}
|
||||||
|
|
|
||||||
|
|
@ -2,5 +2,5 @@
|
||||||
"testEnvironment": "node",
|
"testEnvironment": "node",
|
||||||
"testMatch": ["**/specs/**/*.test.js"],
|
"testMatch": ["**/specs/**/*.test.js"],
|
||||||
"setupFilesAfterEnv": ["<rootDir>/support/config/jest.setup.local.js"],
|
"setupFilesAfterEnv": ["<rootDir>/support/config/jest.setup.local.js"],
|
||||||
"testTimeout": 30000
|
"testTimeout": 60000
|
||||||
}
|
}
|
||||||
|
|
@ -110,9 +110,14 @@ describe('Timeline Scheduling Feature Test', () => {
|
||||||
|
|
||||||
// Helper function to create a plannable item
|
// Helper function to create a plannable item
|
||||||
async function createPlannableItem(itemData) {
|
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")]'));
|
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
|
// Wait for form modal
|
||||||
await driver.wait(until.elementLocated(By.className('plannable-form-modal')), 5000);
|
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
|
// Wait for modal to close
|
||||||
await driver.sleep(1500);
|
await driver.sleep(1500);
|
||||||
await driver.wait(async () => {
|
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) {
|
for (const overlay of overlays) {
|
||||||
try {
|
try {
|
||||||
if (await overlay.isDisplayed()) {
|
if (await overlay.isDisplayed()) {
|
||||||
|
|
@ -154,6 +159,9 @@ describe('Timeline Scheduling Feature Test', () => {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
|
// Additional wait to ensure UI is stable
|
||||||
|
await driver.sleep(500);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to wait for modal close
|
// Helper to wait for modal close
|
||||||
|
|
@ -211,15 +219,15 @@ describe('Timeline Scheduling Feature Test', () => {
|
||||||
// Get first day section
|
// Get first day section
|
||||||
const firstDaySection = await driver.findElement(By.className('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'));
|
const hourRows = await firstDaySection.findElements(By.className('hour-row'));
|
||||||
expect(hourRows.length).toBeGreaterThanOrEqual(18);
|
expect(hourRows.length).toBeGreaterThanOrEqual(18);
|
||||||
|
|
||||||
// Verify specific hour labels exist
|
// 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);
|
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);
|
expect(await hour11pm.isDisplayed()).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -250,8 +258,8 @@ describe('Timeline Scheduling Feature Test', () => {
|
||||||
notes: 'Morning coffee'
|
notes: 'Morning coffee'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Now find the 8 AM hour row
|
// Now find the 08:00 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")]'));
|
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
|
// Hover to show + button
|
||||||
await driver.actions().move({ origin: hour8am }).perform();
|
await driver.actions().move({ origin: hour8am }).perform();
|
||||||
|
|
@ -285,11 +293,11 @@ describe('Timeline Scheduling Feature Test', () => {
|
||||||
|
|
||||||
await driver.sleep(1000);
|
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 daySections = await driver.findElements(By.className('day-section'));
|
||||||
const day1Section = daySections[0];
|
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 +
|
// Hover and click +
|
||||||
await driver.actions().move({ origin: hour10am }).perform();
|
await driver.actions().move({ origin: hour10am }).perform();
|
||||||
|
|
@ -321,12 +329,12 @@ describe('Timeline Scheduling Feature Test', () => {
|
||||||
// Wait for modal to close
|
// Wait for modal to close
|
||||||
await waitForModalClose();
|
await waitForModalClose();
|
||||||
|
|
||||||
// Verify item appears in timeline at 10 AM
|
// Verify item appears in timeline at 10:00
|
||||||
await driver.sleep(1000);
|
await driver.sleep(1000);
|
||||||
const scheduledItem = await driver.findElement(By.xpath('//div[contains(@class, "scheduled-slot") and contains(., "Louvre Museum Visit")]'));
|
const scheduledItem = await driver.findElement(By.xpath('//div[contains(@class, "scheduled-slot") and contains(., "Louvre Museum Visit")]'));
|
||||||
expect(await scheduledItem.isDisplayed()).toBe(true);
|
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();
|
const timeDisplay = await scheduledItem.getText();
|
||||||
expect(timeDisplay).toContain('10:00');
|
expect(timeDisplay).toContain('10:00');
|
||||||
expect(timeDisplay).toContain('12:00');
|
expect(timeDisplay).toContain('12:00');
|
||||||
|
|
@ -350,11 +358,11 @@ describe('Timeline Scheduling Feature Test', () => {
|
||||||
|
|
||||||
await driver.sleep(1000);
|
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 daySections = await driver.findElements(By.className('day-section'));
|
||||||
const day2Section = daySections[1]; // Day 2
|
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.actions().move({ origin: hour8am }).perform();
|
||||||
await driver.sleep(500);
|
await driver.sleep(500);
|
||||||
|
|
@ -371,8 +379,8 @@ describe('Timeline Scheduling Feature Test', () => {
|
||||||
|
|
||||||
await driver.sleep(1000);
|
await driver.sleep(1000);
|
||||||
|
|
||||||
// Schedule second item at 3 PM
|
// Schedule second item at 15:00
|
||||||
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")]'));
|
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.actions().move({ origin: hour3pm }).perform();
|
||||||
await driver.sleep(500);
|
await driver.sleep(500);
|
||||||
|
|
@ -411,11 +419,11 @@ describe('Timeline Scheduling Feature Test', () => {
|
||||||
|
|
||||||
await driver.sleep(1000);
|
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 daySections = await driver.findElements(By.className('day-section'));
|
||||||
const day3Section = daySections[2]; // Day 3
|
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.actions().move({ origin: hour2pm }).perform();
|
||||||
await driver.sleep(500);
|
await driver.sleep(500);
|
||||||
|
|
@ -463,7 +471,7 @@ describe('Timeline Scheduling Feature Test', () => {
|
||||||
const daySections = await driver.findElements(By.className('day-section'));
|
const daySections = await driver.findElements(By.className('day-section'));
|
||||||
const day1Section = daySections[0];
|
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.actions().move({ origin: hour11am }).perform();
|
||||||
await driver.sleep(500);
|
await driver.sleep(500);
|
||||||
|
|
@ -502,8 +510,8 @@ describe('Timeline Scheduling Feature Test', () => {
|
||||||
const daySections = await driver.findElements(By.className('day-section'));
|
const daySections = await driver.findElements(By.className('day-section'));
|
||||||
const day1Section = daySections[0];
|
const day1Section = daySections[0];
|
||||||
|
|
||||||
// Schedule Dinner first (7 PM)
|
// Schedule Dinner first (19:00)
|
||||||
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")]'));
|
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.actions().move({ origin: hour7pm }).perform();
|
||||||
await driver.sleep(500);
|
await driver.sleep(500);
|
||||||
await hour7pm.findElement(By.css('button')).click();
|
await hour7pm.findElement(By.css('button')).click();
|
||||||
|
|
@ -515,8 +523,8 @@ describe('Timeline Scheduling Feature Test', () => {
|
||||||
await waitForModalClose();
|
await waitForModalClose();
|
||||||
await driver.sleep(1000);
|
await driver.sleep(1000);
|
||||||
|
|
||||||
// Schedule Breakfast (7 AM)
|
// Schedule Breakfast (07:00)
|
||||||
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")]'));
|
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.actions().move({ origin: hour7am }).perform();
|
||||||
await driver.sleep(500);
|
await driver.sleep(500);
|
||||||
await hour7am.findElement(By.css('button')).click();
|
await hour7am.findElement(By.css('button')).click();
|
||||||
|
|
@ -528,8 +536,8 @@ describe('Timeline Scheduling Feature Test', () => {
|
||||||
await waitForModalClose();
|
await waitForModalClose();
|
||||||
await driver.sleep(1000);
|
await driver.sleep(1000);
|
||||||
|
|
||||||
// Schedule Lunch (12 PM)
|
// Schedule Lunch (12:00)
|
||||||
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")]'));
|
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.actions().move({ origin: hour12pm }).perform();
|
||||||
await driver.sleep(500);
|
await driver.sleep(500);
|
||||||
await hour12pm.findElement(By.css('button')).click();
|
await hour12pm.findElement(By.css('button')).click();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue