- Add CalendarSlotService methods for slot order calculation - Update PlannedItemController to create slots from datetime - Update CalendarSlotController with proper eager loading - Create timeline UI components (TripTimeline, DaySection, HourRow, ScheduleItemModal) - Add comprehensive PHPUnit tests (PlannedItemTest, CalendarSlotServiceTest) - Add comprehensive Selenium E2E tests (timeline-scheduling.test.js) - Update PlannablesList to support onItemsChange callback
121 lines
No EOL
3.3 KiB
JavaScript
121 lines
No EOL
3.3 KiB
JavaScript
import { useState, useEffect, useMemo } from 'react';
|
|
import { useParams, useNavigate, Link } from 'react-router-dom';
|
|
import { formatDate } from '../utils/dateFormatter';
|
|
import { useTrip } from '../hooks/useTrip';
|
|
import PlannablesList from './plannables/PlannablesList';
|
|
import TripTimeline from './timeline/TripTimeline';
|
|
import axios from 'axios';
|
|
import './TripDetail.css';
|
|
|
|
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000';
|
|
|
|
const TripDetail = () => {
|
|
const { id } = useParams();
|
|
const navigate = useNavigate();
|
|
const [trip, setTrip] = useState(null);
|
|
const [plannableItems, setPlannableItems] = useState([]);
|
|
const { fetchTrip, loading, error } = useTrip();
|
|
|
|
useEffect(() => {
|
|
const loadTrip = async () => {
|
|
try {
|
|
const tripData = await fetchTrip(id);
|
|
setTrip(tripData);
|
|
} catch (err) {
|
|
console.error('Error loading trip:', err);
|
|
}
|
|
};
|
|
|
|
loadTrip();
|
|
}, [id, fetchTrip]);
|
|
|
|
useEffect(() => {
|
|
const loadPlannableItems = async () => {
|
|
try {
|
|
const token = localStorage.getItem('token');
|
|
const response = await axios.get(`${API_URL}/api/trips/${id}/plannables`, {
|
|
headers: { Authorization: `Bearer ${token}` }
|
|
});
|
|
setPlannableItems(response.data.data || []);
|
|
} catch (err) {
|
|
console.error('Error loading plannable items:', err);
|
|
}
|
|
};
|
|
|
|
if (id) {
|
|
loadPlannableItems();
|
|
}
|
|
}, [id]);
|
|
|
|
// Memoize trip dates display to prevent unnecessary re-renders
|
|
const tripDatesDisplay = useMemo(() => {
|
|
if (!trip) return null;
|
|
|
|
return (
|
|
<div className="trip-dates">
|
|
<span className="date-label">Start:</span>
|
|
<span className="date-value">{formatDate(trip.start_date)}</span>
|
|
<span className="date-separator">•</span>
|
|
<span className="date-label">End:</span>
|
|
<span className="date-value">{formatDate(trip.end_date)}</span>
|
|
</div>
|
|
);
|
|
}, [trip]);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="trip-detail-loading">
|
|
<div className="spinner"></div>
|
|
<p>Loading trip details...</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="trip-detail-error">
|
|
<h2>Error</h2>
|
|
<p>{error}</p>
|
|
<Link to="/" className="btn-back">Back to Dashboard</Link>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!trip) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="trip-detail">
|
|
<header className="trip-detail-header">
|
|
<div className="header-nav">
|
|
<Link to="/" className="btn-back">← Back to Dashboard</Link>
|
|
</div>
|
|
<div className="header-content">
|
|
<h1>{trip.name}</h1>
|
|
{trip.description && (
|
|
<p className="trip-description">{trip.description}</p>
|
|
)}
|
|
{tripDatesDisplay}
|
|
</div>
|
|
</header>
|
|
|
|
<div className="trip-detail-content">
|
|
<div className="trip-detail-sidebar">
|
|
<PlannablesList tripId={trip.id} onItemsChange={(items) => setPlannableItems(items)} />
|
|
</div>
|
|
<div className="trip-detail-main">
|
|
<TripTimeline
|
|
trip={trip}
|
|
plannableItems={plannableItems}
|
|
onScheduleSuccess={() => {
|
|
// Optional: refresh plannable items if needed
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default TripDetail; |