diff --git a/src/arrange-v4/app/books/page.tsx b/src/arrange-v4/app/books/page.tsx index cfebbd4..ec2830f 100644 --- a/src/arrange-v4/app/books/page.tsx +++ b/src/arrange-v4/app/books/page.tsx @@ -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'; @@ -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([]); + const store = useStore(); + const [books, setBooks] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [userName, setUserName] = useState(''); @@ -31,7 +33,7 @@ export default function BooksPage() { }); }; - const fetchCalendars = async () => { + const fetchBooks = async () => { if (!isAuthenticated) return; setLoading(true); @@ -39,48 +41,45 @@ export default function BooksPage() { 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]); @@ -97,11 +96,11 @@ export default function BooksPage() { ) : ( <> ), - [isAuthenticated, inProgress, loading, deleting, bookId, calendars, selectedIds.size], + [isAuthenticated, inProgress, loading, deleting, bookId, books, selectedIds.size], ); const toggleSelect = (id: string) => { @@ -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(); @@ -145,9 +142,8 @@ 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; @@ -155,13 +151,11 @@ function CancelledPageContent() { 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); diff --git a/src/arrange-v4/app/matrix/page.tsx b/src/arrange-v4/app/matrix/page.tsx index 5cea2bc..9860edd 100644 --- a/src/arrange-v4/app/matrix/page.tsx +++ b/src/arrange-v4/app/matrix/page.tsx @@ -1,9 +1,8 @@ 'use client'; import React, { useState, useEffect, useMemo, useRef, Suspense } from 'react'; -import { getCalendars, getCalendarEvents } from '@/lib/graphService'; -import { createTodoItem, updateTodoItem, sweepStaleTodos, TodoItem, parseTodoData, TodoStatus, ALL_STATUSES, STATUS_LABELS } from '@/lib/todoDataService'; -import { filterArrangeCalendars, getCalendarDisplayName } from '@/lib/calendarUtils'; +import { useStore } from '@/lib/store/useStore'; +import { TodoItem, TodoItemWithId, TodoStatus, ALL_STATUSES, STATUS_LABELS } from '@/lib/store/types'; import { formatRelativeDate } from '@/lib/dateUtils'; import { hasSessionSweepRun, isSessionSweepInProgress, markSessionSweepInProgress, clearSessionSweepInProgress, markSessionSweepDone } from '@/lib/bookStorage'; import { useGraphToken } from '@/lib/hooks/useGraphToken'; @@ -49,11 +48,11 @@ function passesTodayFilter(todo: TodoItem): boolean { } // TodoCard component for rendering individual todo items -function TodoCard({ todo, onDragStart, onClick, onStatusChange }: { - todo: TodoItem & { id?: string }, - onDragStart?: (todo: TodoItem & { id?: string }) => void, - onClick?: (todo: TodoItem & { id?: string }) => void, - onStatusChange?: (todo: TodoItem & { id?: string }, newStatus: TodoStatus) => void +function TodoCard({ todo, onDragStart, onClick, onStatusChange }: { + todo: TodoItemWithId, + onDragStart?: (todo: TodoItemWithId) => void, + onClick?: (todo: TodoItemWithId) => void, + onStatusChange?: (todo: TodoItemWithId, newStatus: TodoStatus) => void }) { const currentStatus = todo.status || 'new'; @@ -65,7 +64,7 @@ function TodoCard({ todo, onDragStart, onClick, onStatusChange }: { }; return ( -
onDragStart?.(todo)} @@ -188,17 +187,18 @@ export default function MatrixPage() { } function MatrixPageContent() { - const { acquireToken, isAuthenticated, inProgress, handleLogin: graphLogin } = useGraphToken(); - const { bookId, calendars, currentCalendarName, handleCalendarSwitch, error: calendarError } = useBookId('/matrix'); + const { isAuthenticated, inProgress, handleLogin: graphLogin } = useGraphToken(); + const store = useStore(); + const { bookId, books, handleBookSwitch, error: bookError } = useBookId('/matrix'); const bookIdRef = useRef(bookId); const sweepAttemptedRef = useRef(false); bookIdRef.current = bookId; - const [todoItems, setTodoItems] = useState<(TodoItem & { id?: string })[]>([]); + const [todoItems, setTodoItems] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [draggedItem, setDraggedItem] = useState<(TodoItem & { id?: string }) | null>(null); - const [selectedTodo, setSelectedTodo] = useState<(TodoItem & { id?: string }) | null>(null); + const [draggedItem, setDraggedItem] = useState(null); + const [selectedTodo, setSelectedTodo] = useState(null); const [statusFilters, setStatusFilters] = useState>(DEFAULT_STATUS_FILTERS); const [showFilters, setShowFilters] = useState(false); const [showTags, setShowTags] = useState(true); @@ -206,8 +206,8 @@ function MatrixPageContent() { const [showUncategorized, setShowUncategorized] = useState(false); const [showManageTags, setShowManageTags] = useState(false); - // Merge calendar-level errors into the page error state - const displayError = error || calendarError; + // Merge book-level errors into the page error state + const displayError = error || bookError; const allCategories = useMemo(() => { const cats = new Set(); @@ -251,53 +251,44 @@ function MatrixPageContent() { setError(null); try { - const accessToken = await acquireToken(); - // Fetch events from last 30 days to next 30 days const today = new Date(); today.setHours(0, 0, 0, 0); const startDate = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000); const endDate = new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000); - - const eventsData = await getCalendarEvents( - accessToken, - bookId, - startDate.toISOString(), - endDate.toISOString() - ); - - // Parse events to TodoItems - const todos = eventsData.map(event => parseTodoData(event)); + + const todos = await store.listItems(bookId, { + range: 'window', + fromDate: startDate.toISOString(), + toDate: endDate.toISOString(), + }); setTodoItems(todos); - // Sweep stale items across ALL calendars once per session (non-blocking; per-load ref prevents retries on failure) + // Sweep stale items across ALL books once per session (non-blocking; per-load ref prevents retries on failure) if (!hasSessionSweepRun() && !isSessionSweepInProgress() && !sweepAttemptedRef.current) { sweepAttemptedRef.current = true; markSessionSweepInProgress(); - const sweepAccessToken = accessToken; const sweepBookId = bookId; - const snapshotCalendars = calendars.length > 0 ? [...calendars] : null; + const snapshotBooks = books.length > 0 ? [...books] : null; void (async () => { try { - const sweepCalendars = snapshotCalendars - ?? filterArrangeCalendars(await getCalendars(sweepAccessToken)); - const calendarQueue = sweepCalendars.filter(c => c.id); + const sweepBooks = snapshotBooks ?? (await store.listBooks()); const CONCURRENCY = 5; let i = 0; let hasFailure = false; const processNext = async () => { - while (i < calendarQueue.length) { - const cal = calendarQueue[i++]; + while (i < sweepBooks.length) { + const book = sweepBooks[i++]; try { - const calEvents = await getCalendarEvents( - sweepAccessToken, cal.id!, - startDate.toISOString(), endDate.toISOString() - ); - const calTodos = calEvents.map(event => parseTodoData(event)); - await sweepStaleTodos(sweepAccessToken, cal.id!, calTodos); + const calItems = await store.listItems(book.id, { + range: 'window', + fromDate: startDate.toISOString(), + toDate: endDate.toISOString(), + }); + await store.calendar.sweepStaleItems(book.id, calItems); } catch (calError) { hasFailure = true; - console.error(`Error sweeping calendar ${cal.id}:`, calError); + console.error(`Error sweeping book ${book.id}:`, calError); } } }; @@ -311,12 +302,13 @@ function MatrixPageContent() { // Refresh current view if still on the same book (independent of sweep status) try { if (bookIdRef.current === sweepBookId) { - const refreshedEvents = await getCalendarEvents( - sweepAccessToken, sweepBookId, - startDate.toISOString(), endDate.toISOString() - ); + const refreshed = await store.listItems(sweepBookId, { + range: 'window', + fromDate: startDate.toISOString(), + toDate: endDate.toISOString(), + }); if (bookIdRef.current === sweepBookId) { - setTodoItems(refreshedEvents.map(event => parseTodoData(event))); + setTodoItems(refreshed); } } } catch (refreshError) { @@ -328,9 +320,10 @@ function MatrixPageContent() { } })(); } - } catch (error: any) { - console.error('Error fetching events:', error); - setError(error.message || 'Failed to fetch events'); + } catch (err: unknown) { + console.error('Error fetching events:', err); + const message = err instanceof Error ? err.message : 'Failed to fetch events'; + setError(message); } finally { setLoading(false); } @@ -342,18 +335,16 @@ function MatrixPageContent() { } try { - const accessToken = await acquireToken(); - - const createdEvent = await createTodoItem(accessToken, bookId, todoItem); - const newTodo = parseTodoData(createdEvent); + const newTodo = await store.createItem(bookId, todoItem); setTodoItems(prev => [...prev, newTodo]); - } catch (error: any) { - console.error('Error creating TODO item:', error); - throw new Error(error.message || 'Failed to create TODO item'); + } catch (err: unknown) { + console.error('Error creating TODO item:', err); + const message = err instanceof Error ? err.message : 'Failed to create TODO item'; + throw new Error(message); } }; - const handleDragStart = (todo: TodoItem & { id?: string }) => { + const handleDragStart = (todo: TodoItemWithId) => { setDraggedItem(todo); }; @@ -362,7 +353,7 @@ function MatrixPageContent() { }; const handleDrop = async (urgent: boolean, important: boolean) => { - if (!draggedItem || !draggedItem.id || !bookId) return; + if (!draggedItem || !bookId) return; // Don't update if already in correct quadrant if (draggedItem.urgent === urgent && draggedItem.important === important) { @@ -372,9 +363,9 @@ function MatrixPageContent() { // Optimistic update - update UI immediately const previousItems = [...todoItems]; - setTodoItems(items => - items.map(item => - item.id === draggedItem.id + setTodoItems(items => + items.map(item => + item.id === draggedItem.id ? { ...item, urgent, important } : item ) @@ -382,57 +373,38 @@ function MatrixPageContent() { setDraggedItem(null); try { - const accessToken = await acquireToken(); - - await updateTodoItem(accessToken, bookId, draggedItem.id, { - urgent, - important, - }); - } catch (error: any) { - console.error('Error updating TODO item:', error); - // Revert on error + await store.updateItem(bookId, draggedItem.id, { urgent, important }); + } catch (err: unknown) { + console.error('Error updating TODO item:', err); setTodoItems(previousItems); - setError(error.message || 'Failed to update TODO item'); + const message = err instanceof Error ? err.message : 'Failed to update TODO item'; + setError(message); } }; - const handleStatusChange = async (todo: TodoItem & { id?: string }, newStatus: TodoStatus) => { - if (!todo.id || !bookId) return; + const handleStatusChange = async (todo: TodoItemWithId, newStatus: TodoStatus) => { + if (!bookId) return; const currentStatus = todo.status || 'new'; const now = new Date().toISOString(); // Calculate timestamp changes based on status transition - let updatedTimestamps: Partial = {}; - - // Set startDateTime when status changes to inProgress (if not already set) + const updatedTimestamps: Partial = {}; + if (newStatus === 'inProgress' && !todo.startDateTime) { updatedTimestamps.startDateTime = now; } - - // Remove startDateTime when status changes to new if (newStatus === 'new') { updatedTimestamps.startDateTime = undefined; } - - // Set timestamps when status changes to finished if (newStatus === 'finished') { - // Set startDateTime if not already set (direct new → finished) - if (!todo.startDateTime) { - updatedTimestamps.startDateTime = now; - } - // Set finishDateTime if not already set - if (!todo.finishDateTime) { - updatedTimestamps.finishDateTime = now; - } + if (!todo.startDateTime) updatedTimestamps.startDateTime = now; + if (!todo.finishDateTime) updatedTimestamps.finishDateTime = now; } - - // Remove finishDateTime when status changes from finished to anything else if (newStatus !== 'finished' && currentStatus === 'finished') { updatedTimestamps.finishDateTime = undefined; } - // Optimistic update - update UI immediately const previousItems = [...todoItems]; setTodoItems(items => items.map(item => @@ -443,16 +415,12 @@ function MatrixPageContent() { ); try { - const accessToken = await acquireToken(); - - await updateTodoItem(accessToken, bookId, todo.id, { - status: newStatus, - }); - } catch (error: any) { - console.error('Error updating TODO status:', error); - // Revert on error + await store.updateItem(bookId, todo.id, { status: newStatus }); + } catch (err: unknown) { + console.error('Error updating TODO status:', err); setTodoItems(previousItems); - setError(error.message || 'Failed to update status'); + const message = err instanceof Error ? err.message : 'Failed to update status'; + setError(message); } }; @@ -468,21 +436,19 @@ function MatrixPageContent() { setSelectedTodo(prev => prev ? { ...prev, ...updatedFields } : prev); try { - const accessToken = await acquireToken(); - - await updateTodoItem(accessToken, bookId, selectedTodo.id, updatedFields); - } catch (error: any) { - console.error('Error updating TODO:', error); + await store.updateItem(bookId, selectedTodo.id, updatedFields); + } catch (err: unknown) { + console.error('Error updating TODO:', err); setTodoItems(previousItems); const reverted = previousItems.find(i => i.id === selectedTodo.id); if (reverted) setSelectedTodo(reverted); - throw error; + throw err; } }; const bulkUpdateCategories = async ( - affectedItems: (TodoItem & { id?: string })[], - computeNewCategories: (item: TodoItem & { id?: string }) => string[], + affectedItems: TodoItemWithId[], + computeNewCategories: (item: TodoItemWithId) => string[], updateFilterState: () => void, ) => { if (!bookId || affectedItems.length === 0) return; @@ -490,7 +456,7 @@ function MatrixPageContent() { const previousItems = [...todoItems]; const previousSelectedCategories = new Set(selectedCategories); const previousShowUncategorized = showUncategorized; - const affectedIds = new Set(affectedItems.filter(a => a.id).map(a => a.id)); + const affectedIds = new Set(affectedItems.map(a => a.id)); setTodoItems(items => items.map(item => @@ -502,25 +468,23 @@ function MatrixPageContent() { updateFilterState(); try { - const accessToken = await acquireToken(); const CONCURRENCY = 5; let idx = 0; const worker = async () => { while (idx < affectedItems.length) { const item = affectedItems[idx++]; - if (!item.id) continue; - await updateTodoItem(accessToken, bookId, item.id, { + await store.updateItem(bookId, item.id, { categories: computeNewCategories(item), }); } }; await Promise.all(Array.from({ length: Math.min(CONCURRENCY, affectedItems.length) }, () => worker())); - } catch (error: any) { - console.error('Error updating tags:', error); + } catch (err: unknown) { + console.error('Error updating tags:', err); setTodoItems(previousItems); setSelectedCategories(previousSelectedCategories); setShowUncategorized(previousShowUncategorized); - throw error; + throw err; } }; @@ -578,15 +542,15 @@ function MatrixPageContent() { // Push page actions into the shared top bar useSetTopBarActions( - calendars.length > 1 ? ( + books.length > 1 ? ( @@ -611,7 +575,7 @@ function MatrixPageContent() { ), - [isAuthenticated, inProgress, loading, bookId, calendars, allCategories], + [isAuthenticated, inProgress, loading, bookId, books, allCategories], ); if (!bookId) { diff --git a/src/arrange-v4/app/page.tsx b/src/arrange-v4/app/page.tsx index 4e4eed6..c2317ab 100644 --- a/src/arrange-v4/app/page.tsx +++ b/src/arrange-v4/app/page.tsx @@ -3,59 +3,12 @@ import { useMsal } from '@azure/msal-react'; import { loginRequest } from '@/lib/msalConfig'; import { useRouter } from 'next/navigation'; -import { getCalendars } from '@/lib/graphService'; -import { filterArrangeCalendars } from '@/lib/calendarUtils'; +import { MultiBackendStore } from '@/lib/store/multiStore'; +import { normalizeBookId } from '@/lib/store/types'; import { getLastBookId } from '@/lib/bookStorage'; import { useState, useEffect } from 'react'; import styles from './page.module.css'; -/** - * Determines the appropriate landing page after user authentication - * @returns The path to navigate to after login - */ -async function getPostLoginRoute(accessToken: string): Promise { - try { - const calendars = await getCalendars(accessToken); - const arrangeCalendars = filterArrangeCalendars(calendars); - - if (arrangeCalendars.length === 1 && arrangeCalendars[0].id) { - return `/matrix?bookId=${encodeURIComponent(arrangeCalendars[0].id)}`; - } - - const savedBookId = getLastBookId(); - if (savedBookId && arrangeCalendars.some(cal => cal.id === savedBookId)) { - return `/matrix?bookId=${encodeURIComponent(savedBookId)}`; - } - } catch (error) { - console.error('Error fetching calendars for route decision:', error); - } - - return '/books'; -} - -/** - * Checks if the matrix view should be available - */ -async function shouldShowMatrixButton(accessToken: string): Promise<{ show: boolean; bookId?: string }> { - try { - const calendars = await getCalendars(accessToken); - const arrangeCalendars = filterArrangeCalendars(calendars); - - if (arrangeCalendars.length === 1 && arrangeCalendars[0].id) { - return { show: true, bookId: arrangeCalendars[0].id }; - } - - const savedBookId = getLastBookId(); - if (savedBookId && arrangeCalendars.some(cal => cal.id === savedBookId)) { - return { show: true, bookId: savedBookId }; - } - } catch (error) { - console.error('Error checking matrix availability:', error); - } - - return { show: false }; -} - export default function Home() { const { instance, accounts, inProgress } = useMsal(); const router = useRouter(); @@ -63,31 +16,88 @@ export default function Home() { const isAuthenticated = accounts.length > 0; + // Background availability check: silent-only token acquisition so we never + // open an unexpected popup from a useEffect. If the silent acquisition fails + // (e.g. the cached token expired and interaction is needed), we just don't + // show the matrix buttons — the user can sign in via the Get Started button. + // The `cancelled` flag prevents a slow listBooks() response from a previous + // account from clobbering newer state if the user switches accounts mid-flight. useEffect(() => { - const checkMatrixAvailability = async () => { - if (isAuthenticated && accounts[0]) { - try { - const response = await instance.acquireTokenSilent({ - ...loginRequest, - account: accounts[0], - }); - const result = await shouldShowMatrixButton(response.accessToken); - setMatrixAvailable(result); - } catch (error) { - console.error('Error checking matrix availability:', error); + let cancelled = false; + const check = async () => { + if (!isAuthenticated || !accounts[0]) return; + try { + const response = await instance.acquireTokenSilent({ + ...loginRequest, + account: accounts[0], + }); + if (cancelled) return; + const silentStore = new MultiBackendStore({ + acquireToken: async () => response.accessToken, + }); + const books = await silentStore.listBooks(); + if (cancelled) return; + + if (books.length === 1) { + setMatrixAvailable({ show: true, bookId: books[0].id }); + return; } + + const savedBookId = normalizeBookId(getLastBookId()); + if (savedBookId && books.some(b => b.id === savedBookId)) { + setMatrixAvailable({ show: true, bookId: savedBookId }); + return; + } + + setMatrixAvailable({ show: false }); + } catch (error) { + if (cancelled) return; + // Silent failure is fine for this background check — don't open a popup. + // But reset matrixAvailable so stale data from a previous account or + // a previously-successful check doesn't linger. + setMatrixAvailable({ show: false }); + console.error('Error checking matrix availability:', error); } }; - checkMatrixAvailability(); + check(); + return () => { + cancelled = true; + }; }, [isAuthenticated, accounts, instance]); const handleLogin = async () => { try { const result = await instance.loginPopup(loginRequest); - const accessToken = result.accessToken; - const route = await getPostLoginRoute(accessToken); - router.push(route); + + // MSAL's React state hasn't re-rendered yet — useGraphToken would still + // return a stale closure that throws "no account". Build a one-shot store + // bound to the access token we just got back from loginPopup. + const postLoginStore = new MultiBackendStore({ + acquireToken: async () => result.accessToken, + }); + + // Wrap the post-login routing decision in its own try/catch. A transient + // Graph error must not leave the user stuck on the landing page after a + // successful login — fall back to /books in that case. + try { + const books = await postLoginStore.listBooks(); + + if (books.length === 1) { + router.push(`/matrix?bookId=${encodeURIComponent(books[0].id)}`); + return; + } + + const savedBookId = normalizeBookId(getLastBookId()); + if (savedBookId && books.some(b => b.id === savedBookId)) { + router.push(`/matrix?bookId=${encodeURIComponent(savedBookId)}`); + return; + } + } catch (routingError) { + console.error('Error during post-login routing — falling back to /books:', routingError); + } + + router.push('/books'); } catch (error) { console.error('Login failed:', error); } diff --git a/src/arrange-v4/app/scrum/page.tsx b/src/arrange-v4/app/scrum/page.tsx index cb3f4a5..810d590 100644 --- a/src/arrange-v4/app/scrum/page.tsx +++ b/src/arrange-v4/app/scrum/page.tsx @@ -1,9 +1,8 @@ 'use client'; import React, { useState, useEffect, useCallback, useMemo, Suspense } from 'react'; -import { getCalendarEvents } from '@/lib/graphService'; -import { createTodoItem, updateTodoItem, TodoItem, parseTodoData, TodoStatus, ALL_STATUSES, STATUS_LABELS } from '@/lib/todoDataService'; -import { getCalendarDisplayName } from '@/lib/calendarUtils'; +import { useStore } from '@/lib/store/useStore'; +import { TodoItem, TodoItemWithId, TodoStatus, ALL_STATUSES, STATUS_LABELS } from '@/lib/store/types'; import { useGraphToken } from '@/lib/hooks/useGraphToken'; import { useBookId } from '@/lib/hooks/useBookId'; import { useSetTopBarActions } from '@/components/TopBarProvider'; @@ -58,7 +57,7 @@ const LANE_STYLES: Record = { cancelled: { lane: styles.laneCancelled, title: styles.laneTitleCancelled }, }; -function sortByPriority(items: (TodoItem & { id?: string })[]) { +function sortByPriority(items: TodoItemWithId[]) { return [...items].sort((a, b) => { const ai = a.important ? 1 : 0; const bi = b.important ? 1 : 0; @@ -71,14 +70,15 @@ function sortByPriority(items: (TodoItem & { id?: string })[]) { } function ScrumPageContent() { - const { acquireToken, isAuthenticated, inProgress, handleLogin: graphLogin } = useGraphToken(); - const { bookId, calendars, currentCalendarName, handleCalendarSwitch, error: calendarError } = useBookId('/scrum'); + const { isAuthenticated, inProgress, handleLogin: graphLogin } = useGraphToken(); + const store = useStore(); + const { bookId, books, handleBookSwitch, error: bookError } = useBookId('/scrum'); - const [todoItems, setTodoItems] = useState<(TodoItem & { id?: string })[]>([]); + const [todoItems, setTodoItems] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [draggedItem, setDraggedItem] = useState<(TodoItem & { id?: string }) | null>(null); - const [selectedTodo, setSelectedTodo] = useState<(TodoItem & { id?: string }) | null>(null); + const [draggedItem, setDraggedItem] = useState(null); + const [selectedTodo, setSelectedTodo] = useState(null); const [showTags, setShowTags] = useState(true); const [selectedCategories, setSelectedCategories] = useState>(new Set()); const [showUncategorized, setShowUncategorized] = useState(false); @@ -86,7 +86,7 @@ function ScrumPageContent() { const [statusFilters, setStatusFilters] = useState>(DEFAULT_STATUS_FILTERS); const [showStatusFilters, setShowStatusFilters] = useState(false); - const displayError = error || calendarError; + const displayError = error || bookError; const allCategories = useMemo(() => { const cats = new Set(); @@ -126,7 +126,7 @@ function ScrumPageContent() { }, [statusFilters]); const lanes = useMemo(() => { - const result = {} as Record; + const result = {} as Record; for (const status of LANE_STATUSES) { result[status] = sortByPriority(filteredItems.filter(t => (t.status || 'new') === status)); } @@ -140,17 +140,17 @@ function ScrumPageContent() { setError(null); try { - const accessToken = await acquireToken(); const today = new Date(); today.setHours(0, 0, 0, 0); const startDate = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000); const endDate = new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000); - const eventsData = await getCalendarEvents( - accessToken, bookId, - startDate.toISOString(), endDate.toISOString() - ); - setTodoItems(eventsData.map(event => parseTodoData(event))); + const items = await store.listItems(bookId, { + range: 'window', + fromDate: startDate.toISOString(), + toDate: endDate.toISOString(), + }); + setTodoItems(items); } catch (err: unknown) { console.error('Error fetching events:', err); const message = err instanceof Error ? err.message : 'Failed to fetch events'; @@ -158,7 +158,7 @@ function ScrumPageContent() { } finally { setLoading(false); } - }, [isAuthenticated, bookId, acquireToken]); + }, [isAuthenticated, bookId, store]); useEffect(() => { if (isAuthenticated && inProgress === 'none' && bookId) { @@ -169,9 +169,7 @@ function ScrumPageContent() { const handleAddTodo = async (todoItem: TodoItem) => { if (!bookId) throw new Error('No book selected'); try { - const accessToken = await acquireToken(); - const createdEvent = await createTodoItem(accessToken, bookId, todoItem); - const newTodo = parseTodoData(createdEvent); + const newTodo = await store.createItem(bookId, todoItem); setTodoItems(prev => [...prev, newTodo]); } catch (err: unknown) { console.error('Error creating TODO item:', err); @@ -190,15 +188,15 @@ function ScrumPageContent() { }; useSetTopBarActions( - calendars.length > 1 ? ( + books.length > 1 ? ( @@ -223,10 +221,10 @@ function ScrumPageContent() { ), - [isAuthenticated, inProgress, loading, bookId, calendars, allCategories], + [isAuthenticated, inProgress, loading, bookId, books, allCategories], ); - const handleDragStart = (todo: TodoItem & { id?: string }) => { + const handleDragStart = (todo: TodoItemWithId) => { setDraggedItem(todo); }; @@ -235,7 +233,7 @@ function ScrumPageContent() { }; const handleDrop = async (newStatus: TodoStatus) => { - if (!draggedItem || !draggedItem.id || !bookId) return; + if (!draggedItem || !bookId) return; const currentStatus = draggedItem.status || 'new'; if (currentStatus === newStatus) { @@ -271,8 +269,7 @@ function ScrumPageContent() { setDraggedItem(null); try { - const accessToken = await acquireToken(); - await updateTodoItem(accessToken, bookId, draggedItem.id, { status: newStatus }); + await store.updateItem(bookId, draggedItem.id, { status: newStatus }); } catch (err: unknown) { console.error('Error updating TODO status:', err); setTodoItems(previousItems); @@ -292,8 +289,7 @@ function ScrumPageContent() { setSelectedTodo(prev => prev ? { ...prev, ...updatedFields } : prev); try { - const accessToken = await acquireToken(); - await updateTodoItem(accessToken, bookId, selectedTodo.id, updatedFields); + await store.updateItem(bookId, selectedTodo.id, updatedFields); } catch (err: unknown) { console.error('Error updating TODO:', err); setTodoItems(previousItems); @@ -304,8 +300,8 @@ function ScrumPageContent() { }; const bulkUpdateCategories = async ( - affectedItems: (TodoItem & { id?: string })[], - computeNewCategories: (item: TodoItem & { id?: string }) => string[], + affectedItems: TodoItemWithId[], + computeNewCategories: (item: TodoItemWithId) => string[], updateFilterState: () => void, ) => { if (!bookId || affectedItems.length === 0) return; @@ -313,7 +309,7 @@ function ScrumPageContent() { const previousItems = [...todoItems]; const previousSelectedCategories = new Set(selectedCategories); const previousShowUncategorized = showUncategorized; - const affectedIds = new Set(affectedItems.filter(a => a.id).map(a => a.id)); + const affectedIds = new Set(affectedItems.map(a => a.id)); setTodoItems(items => items.map(item => @@ -325,14 +321,12 @@ function ScrumPageContent() { updateFilterState(); try { - const accessToken = await acquireToken(); const CONCURRENCY = 5; let idx = 0; const worker = async () => { while (idx < affectedItems.length) { const item = affectedItems[idx++]; - if (!item.id) continue; - await updateTodoItem(accessToken, bookId, item.id, { + await store.updateItem(bookId, item.id, { categories: computeNewCategories(item), }); } diff --git a/src/arrange-v4/components/AddTodoItem.tsx b/src/arrange-v4/components/AddTodoItem.tsx index 797872f..6da9ccf 100644 --- a/src/arrange-v4/components/AddTodoItem.tsx +++ b/src/arrange-v4/components/AddTodoItem.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState, useEffect, useCallback } from 'react'; -import { TodoItem, TodoStatus } from '@/lib/todoDataService'; +import { TodoItem, TodoStatus } from '@/lib/store/types'; import ChecklistEditor from './ChecklistEditor'; import TagPicker from './TagPicker'; import styles from './AddTodoItem.module.css'; diff --git a/src/arrange-v4/components/CalendarList.tsx b/src/arrange-v4/components/CalendarList.tsx index c5659a2..399c716 100644 --- a/src/arrange-v4/components/CalendarList.tsx +++ b/src/arrange-v4/components/CalendarList.tsx @@ -2,18 +2,17 @@ import { useState, useEffect, useCallback, useRef } from 'react'; import { useRouter } from 'next/navigation'; -import { Calendar } from '@/lib/graphService'; -import { getCalendarDisplayName } from '@/lib/calendarUtils'; +import type { Book } from '@/lib/store/types'; import styles from './CalendarList.module.css'; interface CalendarListProps { - calendars: Calendar[]; + books: Book[]; loading: boolean; error: string | null; - onDeleteCalendar: (calendarId: string) => Promise; + onDeleteBook: (bookId: string) => Promise; } -export default function CalendarList({ calendars, loading, error, onDeleteCalendar }: CalendarListProps) { +export default function CalendarList({ books, loading, error, onDeleteBook }: CalendarListProps) { const [deletingId, setDeletingId] = useState(null); const [confirmingId, setConfirmingId] = useState(null); const [dismissResetKey, setDismissResetKey] = useState(0); @@ -47,34 +46,28 @@ export default function CalendarList({ calendars, loading, error, onDeleteCalend } }, [confirmingId]); - const handleCalendarClick = (calendar: Calendar) => { - if (calendar.id) { - router.push(`/matrix?bookId=${encodeURIComponent(calendar.id)}`); - } + const handleBookClick = (book: Book) => { + router.push(`/matrix?bookId=${encodeURIComponent(book.id)}`); }; - const handleDelete = useCallback(async (calendar: Calendar) => { - if (!calendar.id) return; - - setDeletingId(calendar.id); + const handleDelete = useCallback(async (book: Book) => { + setDeletingId(book.id); try { - await onDeleteCalendar(calendar.id); + await onDeleteBook(book.id); } catch (error) { console.error('Failed to delete book:', error); alert('Failed to delete book. Please try again.'); } finally { - const calId = calendar.id; + const bookId = book.id; setDeletingId(null); - setConfirmingId(prev => prev === calId ? null : prev); + setConfirmingId(prev => prev === bookId ? null : prev); // Defer focus restoration until delete button remounts after confirmation row unmounts - if (calId) { - requestAnimationFrame(() => { - const btn = deleteButtonRefs.current.get(calId); - if (btn) btn.focus(); - }); - } + requestAnimationFrame(() => { + const btn = deleteButtonRefs.current.get(bookId); + if (btn) btn.focus(); + }); } - }, [onDeleteCalendar]); + }, [onDeleteBook]); if (loading) { return (
@@ -92,7 +85,7 @@ export default function CalendarList({ calendars, loading, error, onDeleteCalend ); } - if (calendars.length === 0) { + if (books.length === 0) { return (

No books yet

@@ -107,66 +100,64 @@ export default function CalendarList({ calendars, loading, error, onDeleteCalend

Your Books

- {calendars.map((calendar) => ( + {books.map((book) => (
handleCalendarClick(calendar)} + onClick={() => handleBookClick(book)} role="button" tabIndex={0} onKeyDown={(e) => { if ((e.target as HTMLElement).closest('button')) return; if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); - handleCalendarClick(calendar); + handleBookClick(book); } }} >

- {getCalendarDisplayName(calendar)} + {book.name}

- {calendar.owner && ( + {book.owner && (

- Owner: {calendar.owner.name || calendar.owner.address} + Owner: {book.owner.name || book.owner.address}

)}
- {calendar.canEdit && ( + {book.canEdit && ( Can Edit )} - {calendar.canShare && ( + {book.canShare && ( Can Share )} - {calendar.canViewPrivateItems && ( + {book.canViewPrivateItems && ( View Private )}
- {calendar.color && ( + {book.color && (
)}
- {calendar.id && ( -

- ID: {calendar.id} -

- )} +

+ ID: {book.id} +

- {calendar.id && (() => { - const bookName = getCalendarDisplayName(calendar); - const bookId = calendar.id; + {(() => { + const bookName = book.name; + const bookId = book.id; return confirmingId === bookId ? (