import { useState, useEffect, useRef } from 'react';
import { useRouter } from 'next/router';
import Layout from '../components/Layout';
import CameraScanner from '../components/CameraScanner';
import ScannerDestinationPicker from '../components/ScannerDestinationPicker';
import ScannedCardItem, { CONDITION_OPTIONS } from '../components/ScannedCardItem';
import OCRSettings from '../components/OCRSettings';
import { useAuth } from '../lib/use-auth';
import { useFocusTrap } from '../lib/use-focus-trap.js';
import { VOCAB } from '../lib/collection-vocabulary.js';
import {
addScannedCardToCollection,
addScannedCardToDeck,
addScannedCardToOwned,
createScannerCollection,
fetchScannerCollections,
fetchScannerDecks,
routeScannedCardToDestination,
} from '../lib/scanner-route-api.js';
import {
DEFAULT_SCANNER_DESTINATION,
destinationActionKey,
loadSavedScannerSession,
mergeScannedCardEntry,
saveScannerSession,
} from '../lib/scanner-session.js';
export default function Scanner() {
const { user, loading: authLoading } = useAuth();
const router = useRouter();
const [scannedCards, setScannedCards] = useState([]);
const [collections, setCollections] = useState([]);
const [decks, setDecks] = useState([]);
const [showCreateCollection, setShowCreateCollection] = useState(false);
const [newCollectionName, setNewCollectionName] = useState('');
const createCollectionDialogRef = useFocusTrap(showCreateCollection);
const [showOCRSettings, setShowOCRSettings] = useState(false);
// Bulk action states
const [selectedCards, setSelectedCards] = useState(new Set());
const [bulkAction, setBulkAction] = useState(''); // 'owned', 'collection', 'deck'
const [bulkTarget, setBulkTarget] = useState('');
const [isProcessing, setIsProcessing] = useState(false);
const addingInFlightRef = useRef(new Set());
const [addingCardIds, setAddingCardIds] = useState(() => new Set());
const [sessionDestination, setSessionDestination] = useState(
() => loadSavedScannerSession().destination
);
const [gameFilter, setGameFilter] = useState(() => loadSavedScannerSession().gameFilter);
const [scanDefaults, setScanDefaults] = useState(() => loadSavedScannerSession().scanDefaults);
const [autoRouteError, setAutoRouteError] = useState(null);
// Redirect to login if not authenticated (wait for verify to finish)
useEffect(() => {
if (!authLoading && !user) {
router.push('/login');
}
}, [authLoading, user, router]);
useEffect(() => {
saveScannerSession({ destination: sessionDestination, gameFilter, scanDefaults });
}, [sessionDestination, gameFilter, scanDefaults]);
useEffect(() => {
if (!user) return;
let cancelled = false;
(async () => {
try {
const [loadedCollections, loadedDecks] = await Promise.all([
fetchScannerCollections(),
fetchScannerDecks(),
]);
if (cancelled) return;
setCollections(loadedCollections);
setDecks(loadedDecks);
} catch (error) {
console.error('Error loading scanner destinations:', error);
}
})();
return () => {
cancelled = true;
};
}, [user]);
const handleGameFilterChange = (nextFilter) => {
setGameFilter(nextFilter);
setSessionDestination((current) => {
if (!current || current.type === 'owned') return current;
return DEFAULT_SCANNER_DESTINATION;
});
};
const handleCardScanned = async (cardData) => {
setAutoRouteError(null);
const { cardEntry, scannedCards: nextQueue } = mergeScannedCardEntry(
scannedCards,
cardData,
scanDefaults
);
setScannedCards(nextQueue);
if (!sessionDestination) return;
if (!cardEntry.databaseId) {
setAutoRouteError(`"${cardEntry.name}" was queued but is not in the catalog yet — add it manually after review.`);
return;
}
try {
if (!tryBeginAdding(cardEntry.id)) return;
await routeScannedCardToDestination(cardEntry, sessionDestination);
markCardAsProcessed(cardEntry.id, destinationActionKey(sessionDestination));
} catch (error) {
console.error('Auto-route failed:', error);
setAutoRouteError(`Could not add "${cardEntry.name}" to ${sessionDestination.label}. Use the card actions below.`);
} finally {
endAdding(cardEntry.id);
}
};
// Quantity management functions
const incrementCardQuantity = (cardId) => {
setScannedCards(prev => prev.map(card =>
card.id === cardId
? { ...card, quantity: (card.quantity || 1) + 1 }
: card
));
};
const decrementCardQuantity = (cardId) => {
setScannedCards(prev => prev.map(card =>
card.id === cardId
? { ...card, quantity: Math.max(1, (card.quantity || 1) - 1) }
: card
));
};
const handleError = (error) => {
console.error('Scanner error:', error);
// You could show a toast notification here
};
const syncAddingState = () => {
setAddingCardIds(new Set(addingInFlightRef.current));
};
const tryBeginAdding = (cardId) => {
if (addingInFlightRef.current.has(cardId)) return false;
addingInFlightRef.current.add(cardId);
syncAddingState();
return true;
};
const endAdding = (cardId) => {
addingInFlightRef.current.delete(cardId);
syncAddingState();
};
// Individual card actions
const addSingleCardToOwned = async (card) => {
if (!tryBeginAdding(card.id)) return;
try {
await addScannedCardToOwned(card);
markCardAsProcessed(card.id, 'owned');
} catch (error) {
console.error('Error adding card to owned:', error);
} finally {
endAdding(card.id);
}
};
const addSingleCardToCollection = async (card, collectionId) => {
if (!tryBeginAdding(card.id)) return;
try {
await addScannedCardToCollection(card, collectionId);
markCardAsProcessed(card.id, 'collection');
} catch (error) {
console.error('Error adding card to collection:', error);
} finally {
endAdding(card.id);
}
};
const addSingleCardToDeck = async (card, deckId) => {
if (!tryBeginAdding(card.id)) return;
try {
await addScannedCardToDeck(card, deckId);
markCardAsProcessed(card.id, 'deck');
} catch (error) {
console.error('Error adding card to deck:', error);
} finally {
endAdding(card.id);
}
};
// Bulk actions
const handleBulkAction = async (actionOverride, targetOverride) => {
const action = actionOverride ?? bulkAction;
const target = targetOverride ?? bulkTarget;
if (!action || selectedCards.size === 0) return;
setIsProcessing(true);
try {
const cardsToProcess = scannedCards.filter(card => selectedCards.has(card.id));
for (const card of cardsToProcess) {
if (action === 'owned') {
if (!tryBeginAdding(card.id)) continue;
try {
await addScannedCardToOwned(card);
} finally {
endAdding(card.id);
}
} else if (action === 'collection' && target) {
if (!tryBeginAdding(card.id)) continue;
try {
await addScannedCardToCollection(card, target);
} finally {
endAdding(card.id);
}
} else if (action === 'deck' && target) {
if (!tryBeginAdding(card.id)) continue;
try {
await addScannedCardToDeck(card, target);
} finally {
endAdding(card.id);
}
} else {
continue;
}
markCardAsProcessed(card.id, action);
}
setSelectedCards(new Set());
setBulkAction('');
setBulkTarget('');
} catch (error) {
console.error('Error processing bulk action:', error);
} finally {
setIsProcessing(false);
}
};
const markCardAsProcessed = (cardId, action) => {
setScannedCards(prev => prev.map(card =>
card.id === cardId
? { ...card, processed: true, processedAction: action }
: card
));
};
const updateCardMetadata = (cardId, patch) => {
setScannedCards((prev) =>
prev.map((card) => (card.id === cardId ? { ...card, ...patch } : card))
);
};
const createCollection = async () => {
if (!newCollectionName.trim()) return;
try {
const newCollection = await createScannerCollection(newCollectionName.trim());
setCollections((prev) => [newCollection, ...prev]);
setBulkTarget(newCollection.id.toString());
setNewCollectionName('');
setShowCreateCollection(false);
} catch (error) {
console.error('Error creating collection:', error);
}
};
const clearScannedCards = () => {
setScannedCards([]);
setSelectedCards(new Set());
};
const removeScannedCard = (cardId) => {
setScannedCards(prev => prev.filter(card => card.id !== cardId));
setSelectedCards(prev => {
const newSet = new Set(prev);
newSet.delete(cardId);
return newSet;
});
};
const toggleCardSelection = (cardId) => {
setSelectedCards(prev => {
const newSet = new Set(prev);
if (newSet.has(cardId)) {
newSet.delete(cardId);
} else {
newSet.add(cardId);
}
return newSet;
});
};
if (authLoading) {
return (
Pick a destination once — every scan lands there until you change it
Applied to each card when it enters the queue