fix(collections): keep stop expanded while working through its items

When a driver collects or reports a problem on an item, the supplier
group they were working through used to collapse — forcing them to
re-expand it for every single item in the stop.

Root cause was a useCallback dep cycle:

- loadItems had items.length in its dep array
- handleCollect / handleProblem filter the processed item out of state
- items.length decreasing rebuilt loadItems (new identity)
- the consuming useEffect([loadItems]) re-fired loadItems
- loadItems's setExpandedSuppliers(new Set()) wiped every group

Fix: mirror items.length into a useRef so loadItems can read the
current count for the loading-skeleton decision without subscribing
to it. loadItems now only rebuilds when statusFilter changes (mount,
filter chip tap, refresh), so processing an item no longer resets
the expansion state. Drivers can work through every item in a stop
without re-expanding.

Behavior preserved on real refreshes / tab switches / filter changes
— those still reset to collapsed, which matches the new 'flat list
of stops on open' UX.
This commit is contained in:
Dave 2026-09-08 22:15:31 +02:00
parent 04d56fa1fb
commit 85a40a76bf

View file

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useState, useEffect, useCallback, useMemo } from 'react'; import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { OrderItem } from '@/lib/types'; import { OrderItem } from '@/lib/types';
import { useAuth } from '@/components/AuthProvider'; import { useAuth } from '@/components/AuthProvider';
@ -100,6 +100,21 @@ export default function CollectionsPage() {
// collapsed independently of the supplier they're inside. // collapsed independently of the supplier they're inside.
const [expandedPOs, setExpandedPOs] = useState<Set<string>>(new Set()); const [expandedPOs, setExpandedPOs] = useState<Set<string>>(new Set());
// Mirror items.length into a ref so loadItems can decide whether to
// flash the loading skeleton WITHOUT making items.length a callback
// dependency. If items.length WERE a dep, every successful collect
// (which filters the collected item out of state) would rebuild
// loadItems, the auto-fire useEffect below would re-run loadItems,
// and the setExpandedSuppliers(new Set()) call inside it would
// collapse the supplier the driver is currently working through —
// forcing them to re-expand for every single item. The ref lets us
// keep the "don't flash a skeleton when items are already on screen"
// behavior while breaking the collapse loop.
const itemsLengthRef = useRef(0);
useEffect(() => {
itemsLengthRef.current = items.length;
}, [items.length]);
useEffect(() => { useEffect(() => {
if (!authLoading && !isLoggedIn) router.push('/login/'); if (!authLoading && !isLoggedIn) router.push('/login/');
}, [isLoggedIn, authLoading, router]); }, [isLoggedIn, authLoading, router]);
@ -116,9 +131,10 @@ export default function CollectionsPage() {
const loadItems = useCallback(async (showLoading = true) => { const loadItems = useCallback(async (showLoading = true) => {
if (!isAuthenticated()) return; if (!isAuthenticated()) return;
// Only show the loading skeleton when we genuinely have nothing // Only show the loading skeleton when we genuinely have nothing
// to render. If cached data is on screen, the re-fetch happens // to render. Read items.length from the ref so this stays a
// silently and the user sees fresh data appear without a flash. // current-value check, not a subscription — see the comment on
if (showLoading && items.length === 0) setIsLoading(true); // itemsLengthRef above for why.
if (showLoading && itemsLengthRef.current === 0) setIsLoading(true);
setError(''); setError('');
try { try {
let data: OrderItem[] = []; let data: OrderItem[] = [];
@ -162,7 +178,7 @@ const loadItems = useCallback(async (showLoading = true) => {
setIsLoading(false); setIsLoading(false);
setIsRefreshing(false); setIsRefreshing(false);
} }
}, [statusFilter, items.length]); }, [statusFilter]);
useEffect(() => { loadItems(); }, [loadItems]); useEffect(() => { loadItems(); }, [loadItems]);