258 lines
No EOL
7.8 KiB
JavaScript
258 lines
No EOL
7.8 KiB
JavaScript
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);
|
|
|
|
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);
|
|
// 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; |