Skip to content
67 changes: 33 additions & 34 deletions src/arrange-v4/app/books/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
'use client';

import { useState, useEffect } from 'react';
import { getCalendars, getUserInfo, createCalendar, deleteCalendar, Calendar } from '@/lib/graphService';
import { filterArrangeCalendars } from '@/lib/calendarUtils';
import { getUserInfo } from '@/lib/graphService';
import { useStore } from '@/lib/store/useStore';
import type { Book } from '@/lib/store/types';
import { useGraphToken } from '@/lib/hooks/useGraphToken';
import { useSetTopBarActions } from '@/components/TopBarProvider';
import CalendarList from '@/components/CalendarList';
Expand All @@ -11,7 +12,8 @@ import styles from './page.module.css';

export default function BooksPage() {
const { acquireToken, isAuthenticated, inProgress, handleLogin: graphLogin, instance } = useGraphToken();
const [calendars, setCalendars] = useState<Calendar[]>([]);
const store = useStore();
const [books, setBooks] = useState<Book[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [userName, setUserName] = useState<string>('');
Expand All @@ -31,56 +33,53 @@ export default function BooksPage() {
});
};

const fetchCalendars = async () => {
const fetchBooks = async () => {
if (!isAuthenticated) return;

setLoading(true);
setError(null);

try {
const accessToken = await acquireToken();

// Fetch user info
const userInfo = await getUserInfo(accessToken);
setUserName(userInfo.displayName || userInfo.userPrincipalName || '');

// Fetch calendars and filter arrange books
const calendarsData = await getCalendars(accessToken);
const filteredCalendars = filterArrangeCalendars(calendarsData);
setCalendars(filteredCalendars);
} catch (error: any) {
console.error('Error fetching calendars:', error);
setError(error.message || 'Failed to fetch books');
const allBooks = await store.listBooks();
setBooks(allBooks);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Failed to fetch books';
console.error('Error fetching books:', err);
setError(message);
} finally {
setLoading(false);
}
};

const handleCreateCalendar = async (name: string) => {
const handleCreateBook = async (name: string) => {
try {
const accessToken = await acquireToken();
const newCalendar = await createCalendar(accessToken, name);
setCalendars(prev => [...prev, newCalendar]);
} catch (error: any) {
console.error('Error creating calendar:', error);
throw new Error(error.message || 'Failed to create book');
const newBook = await store.createBook(name, { backend: 'calendar' });
setBooks(prev => [...prev, newBook]);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Failed to create book';
console.error('Error creating book:', err);
throw new Error(message);
}
};

const handleDeleteCalendar = async (calendarId: string) => {
const handleDeleteBook = async (bookId: string) => {
try {
const accessToken = await acquireToken();
await deleteCalendar(accessToken, calendarId);
setCalendars(prev => prev.filter(c => c.id !== calendarId));
} catch (error: any) {
console.error('Error deleting calendar:', error);
throw new Error(error.message || 'Failed to delete book');
await store.deleteBook(bookId);
setBooks(prev => prev.filter(b => b.id !== bookId));
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Failed to delete book';
console.error('Error deleting book:', err);
throw new Error(message);
}
};

useEffect(() => {
if (isAuthenticated && inProgress === 'none') {
fetchCalendars();
fetchBooks();
}
}, [isAuthenticated, inProgress]);

Expand All @@ -97,11 +96,11 @@ export default function BooksPage() {
) : (
<>
<CreateCalendar
onCreateCalendar={handleCreateCalendar}
onCreateCalendar={handleCreateBook}
disabled={loading}
/>
<button
onClick={fetchCalendars}
onClick={fetchBooks}
disabled={loading}
className={`${styles.button} ${styles.buttonSecondary}`}
>
Expand Down Expand Up @@ -130,11 +129,11 @@ export default function BooksPage() {
</p>
</div>
<div className={styles.card}>
<CalendarList
calendars={calendars}
loading={loading}
<CalendarList
books={books}
loading={loading}
error={error}
onDeleteCalendar={handleDeleteCalendar}
onDeleteBook={handleDeleteBook}
/>
</div>
</>
Expand Down
46 changes: 20 additions & 26 deletions src/arrange-v4/app/cancelled/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
'use client';

import React, { useState, useEffect, useCallback, useRef, Suspense } from 'react';
import { getAllCalendarEvents } from '@/lib/graphService';
import { deleteTodoItem, TodoItem, parseTodoData } from '@/lib/todoDataService';
import { getCalendarDisplayName } from '@/lib/calendarUtils';
import { useStore } from '@/lib/store/useStore';
import type { TodoItem, TodoItemWithId } from '@/lib/store/types';
import { formatRelativeDate } from '@/lib/dateUtils';
import { useGraphToken } from '@/lib/hooks/useGraphToken';
import { useBookId } from '@/lib/hooks/useBookId';
Expand All @@ -13,10 +12,11 @@ import Link from 'next/link';
import styles from './page.module.css';

function CancelledPageContent() {
const { acquireToken, isAuthenticated, inProgress, handleLogin: graphLogin } = useGraphToken();
const { bookId, calendars, currentCalendarName, handleCalendarSwitch, error: calendarError } = useBookId('/cancelled');
const { isAuthenticated, inProgress, handleLogin: graphLogin } = useGraphToken();
const store = useStore();
const { bookId, books, handleBookSwitch, error: bookError } = useBookId('/cancelled');

const [cancelledItems, setCancelledItems] = useState<(TodoItem & { id?: string })[]>([]);
const [cancelledItems, setCancelledItems] = useState<TodoItemWithId[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
Expand All @@ -26,9 +26,9 @@ function CancelledPageContent() {
const [selectedTodo, setSelectedTodo] = useState<(TodoItem & { id?: string }) | null>(null);
const confirmButtonRef = useRef<HTMLButtonElement>(null);

const displayError = error || calendarError;
const displayError = error || bookError;

const allSelected = cancelledItems.length > 0 && cancelledItems.every(t => t.id && selectedIds.has(t.id));
const allSelected = cancelledItems.length > 0 && cancelledItems.every(t => selectedIds.has(t.id));

const fetchEvents = useCallback(async () => {
if (!isAuthenticated || !bookId) return;
Expand All @@ -37,10 +37,8 @@ function CancelledPageContent() {
setError(null);

try {
const accessToken = await acquireToken();
const eventsData = await getAllCalendarEvents(accessToken, bookId);
const todos = eventsData.map(event => parseTodoData(event));
setCancelledItems(todos.filter(t => t.status === 'cancelled'));
const items = await store.listItems(bookId, { range: 'all' });
setCancelledItems(items.filter(t => t.status === 'cancelled'));
setSelectedIds(new Set());
} catch (err: unknown) {
console.error('Error fetching events:', err);
Expand All @@ -49,7 +47,7 @@ function CancelledPageContent() {
} finally {
setLoading(false);
}
}, [isAuthenticated, bookId, acquireToken]);
}, [isAuthenticated, bookId, store]);

useEffect(() => {
if (isAuthenticated && inProgress === 'none' && bookId) {
Expand All @@ -72,16 +70,16 @@ function CancelledPageContent() {
};

useSetTopBarActions(
calendars.length > 1 ? (
books.length > 1 ? (
<select
className={styles.bookSwitcher}
value={bookId || ''}
onChange={(e) => handleCalendarSwitch(e.target.value)}
onChange={(e) => handleBookSwitch(e.target.value)}
disabled={loading || deleting}
>
{calendars.map(cal => (
<option key={cal.id} value={cal.id}>
{getCalendarDisplayName(cal)}
{books.map(b => (
<option key={b.id} value={b.id}>
{b.name}
</option>
))}
</select>
Expand Down Expand Up @@ -112,7 +110,7 @@ function CancelledPageContent() {
</button>
</>
),
[isAuthenticated, inProgress, loading, deleting, bookId, calendars, selectedIds.size],
[isAuthenticated, inProgress, loading, deleting, bookId, books, selectedIds.size],
);

const toggleSelect = (id: string) => {
Expand All @@ -127,11 +125,10 @@ function CancelledPageContent() {
if (allSelected) {
setSelectedIds(new Set());
} else {
setSelectedIds(new Set(cancelledItems.filter(t => t.id).map(t => t.id!)));
setSelectedIds(new Set(cancelledItems.map(t => t.id)));
}
};

// Focus the confirm button when the dialog appears
useEffect(() => {
if (showConfirm) {
confirmButtonRef.current?.focus();
Expand All @@ -145,23 +142,20 @@ function CancelledPageContent() {
const idsToDelete = Array.from(selectedIds);
setDeleteProgress({ done: 0, total: idsToDelete.length });

// Optimistic removal — keep a snapshot for rollback
const previousItems = [...cancelledItems];
setCancelledItems(items => items.filter(item => !item.id || !selectedIds.has(item.id)));
setCancelledItems(items => items.filter(item => !selectedIds.has(item.id)));

const CONCURRENCY = 5;
let idx = 0;
let completed = 0;
let hasFailure = false;

try {
const accessToken = await acquireToken();

const worker = async () => {
while (idx < idsToDelete.length) {
const eventId = idsToDelete[idx++];
try {
await deleteTodoItem(accessToken, bookId, eventId);
await store.deleteItem(bookId, eventId);
} catch (err) {
hasFailure = true;
console.error(`Error deleting event ${eventId}:`, err);
Expand Down
Loading