trip-planner/frontend/src/components/plannables/PlannablesList.jsx

258 lines
7.8 KiB
React
Raw Normal View History

import { useState, useEffect, useMemo } from 'react';
import { useToast } from '../common/ToastContainer';
import { usePlannables } from '../../hooks/usePlannables';
import PlannableItem from './PlannableItem';
import PlannableForm from './PlannableForm';
import ConfirmDialog from '../common/ConfirmDialog';
import './PlannablesList.css';
const PlannablesList = ({ tripId }) => {
const { showSuccess, showError } = useToast();
const {
fetchBothData,
createPlannable,
updatePlannable,
deletePlannable,
loading: apiLoading
} = usePlannables();
const [plannables, setPlannables] = useState([]);
const [calendarSlots, setCalendarSlots] = useState([]);
const [loading, setLoading] = useState(true);
const [showForm, setShowForm] = useState(false);
const [editingItem, setEditingItem] = useState(null);
const [error, setError] = useState(null);
const [confirmDialog, setConfirmDialog] = useState({
isOpen: false,
title: '',
message: '',
onConfirm: null
});
useEffect(() => {
const loadData = async () => {
try {
setLoading(true);
const { plannables, calendarSlots, errors } = await fetchBothData(tripId);
2025-09-30 08:29:17 +02:00
console.log('PlannablesList: Received data:', {
plannablesCount: plannables.length,
calendarSlotsCount: calendarSlots.length,
firstFewSlots: calendarSlots.slice(0, 3).map(s => ({ id: s.id, name: s.name, date: s.slot_date }))
});
setPlannables(plannables);
2025-09-30 08:29:17 +02:00
// Safeguard: limit calendar slots to prevent performance issues
const limitedCalendarSlots = calendarSlots.slice(0, 365); // Max 1 year of slots
setCalendarSlots(limitedCalendarSlots);
if (errors.plannables) {
console.error('Failed to fetch plannables:', errors.plannables);
showError('Failed to load plannable items');
}
if (errors.calendarSlots) {
console.error('Failed to fetch calendar slots:', errors.calendarSlots);
showError('Failed to load calendar slots');
}
} catch (err) {
setError('Failed to load data');
console.error(err);
} finally {
setLoading(false);
}
};
loadData();
}, [tripId, fetchBothData, showError]);
const handleAddItem = () => {
setEditingItem(null);
setShowForm(true);
};
const handleEditItem = (item) => {
setEditingItem(item);
setShowForm(true);
};
const handleDeleteItem = (itemId) => {
const item = plannables.find(p => p.id === itemId);
setConfirmDialog({
isOpen: true,
title: 'Delete Item',
message: `Are you sure you want to delete "${item?.name}"? This action cannot be undone.`,
onConfirm: () => performDelete(itemId)
});
};
const performDelete = async (itemId) => {
try {
await deletePlannable(itemId);
setPlannables(plannables.filter(item => item.id !== itemId));
showSuccess('Item deleted successfully');
} catch (err) {
console.error('Error deleting item:', err);
showError('Failed to delete item. Please try again.');
} finally {
setConfirmDialog({ isOpen: false, title: '', message: '', onConfirm: null });
}
};
const handleFormSubmit = async (formData) => {
try {
const isEditing = !!editingItem;
let savedItem;
if (isEditing) {
savedItem = await updatePlannable(editingItem.id, formData);
setPlannables(plannables.map(item =>
item.id === editingItem.id ? savedItem : item
));
showSuccess('Item updated successfully');
} else {
savedItem = await createPlannable(tripId, formData);
setPlannables([...plannables, savedItem]);
showSuccess('Item added successfully');
}
setShowForm(false);
setEditingItem(null);
} catch (err) {
console.error('Error saving item:', err);
showError('Failed to save item. Please try again.');
}
};
const handleFormCancel = () => {
setShowForm(false);
setEditingItem(null);
};
// Memoize expensive grouping computation to prevent recalculation on every render
const { unplannedItems, plannedItemsBySlot } = useMemo(() => {
const unplanned = plannables.filter(item => !item.calendar_slot_id);
const planned = {};
plannables.forEach(item => {
if (item.calendar_slot_id) {
if (!planned[item.calendar_slot_id]) {
planned[item.calendar_slot_id] = [];
}
planned[item.calendar_slot_id].push(item);
}
});
return { unplannedItems: unplanned, plannedItemsBySlot: planned };
}, [plannables]);
if (loading) {
return (
<div className="plannables-loading">
<div className="spinner-small"></div>
<p>Loading items...</p>
</div>
);
}
return (
<div className="plannables-list">
<div className="plannables-header">
<h2>Itinerary Items</h2>
<button className="btn-add-item" onClick={handleAddItem}>
+ Add Item
</button>
</div>
{error && (
<div className="plannables-error">
{error}
</div>
)}
<div className="plannables-sections">
{/* Unplanned Items Section */}
<div className="plannables-section">
<h3 className="section-title">
📋 Unplanned Items
{unplannedItems.length > 0 && (
<span className="item-count">{unplannedItems.length}</span>
)}
</h3>
<div className="section-items">
{unplannedItems.length === 0 ? (
<p className="empty-message">No unplanned items</p>
) : (
unplannedItems.map(item => (
<PlannableItem
key={item.id}
item={item}
onEdit={handleEditItem}
onDelete={handleDeleteItem}
/>
))
)}
</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 && (
<PlannableForm
item={editingItem}
tripId={tripId}
calendarSlots={calendarSlots}
onSubmit={handleFormSubmit}
onCancel={handleFormCancel}
/>
)}
<ConfirmDialog
isOpen={confirmDialog.isOpen}
title={confirmDialog.title}
message={confirmDialog.message}
confirmText="Delete"
cancelText="Cancel"
variant="danger"
onConfirm={confirmDialog.onConfirm}
onCancel={() => setConfirmDialog({ isOpen: false, title: '', message: '', onConfirm: null })}
/>
</div>
);
};
export default PlannablesList;