diff --git a/src/app/portfolio/[slug]/components/PropertyTable.tsx b/src/app/portfolio/[slug]/components/PropertyTable.tsx index 6cbf399c..92419d1a 100644 --- a/src/app/portfolio/[slug]/components/PropertyTable.tsx +++ b/src/app/portfolio/[slug]/components/PropertyTable.tsx @@ -327,6 +327,21 @@ export default function PropertyTable({ // Row selection for bulk tagging — keyed by property id (see DataTable getRowId). const [rowSelection, setRowSelection] = useState({}); const selectedIds = Object.keys(rowSelection).filter((id) => rowSelection[id]); + // Deliberate "select all matching" escalation (whole set, not just loaded rows). + const [selectAllMatching, setSelectAllMatching] = useState(false); + + const clearSelection = () => { + setRowSelection({}); + setSelectAllMatching(false); + }; + // Any manual checkbox change drops the all-matching escalation back to the + // explicit (possibly now-smaller) row selection. + const handleRowSelectionChange = ( + updater: Updater, + ) => { + setSelectAllMatching(false); + setRowSelection(updater); + }; // Bulk-tag flows launched from the Tags menu: a contextual assign/remove mode, // and the file-upload modal. @@ -456,6 +471,16 @@ export default function PropertyTable({ // loaded rows (cheap; avoids a hook per project convention): exact for a // fully-loaded portfolio, a floor when more rows lazy-load beyond view. const unmodelledInView = tableData.filter((p) => p.planId == null).length; + + // Bulk-select scope: how many properties match the current view, whether every + // loaded row is ticked, and whether more match than are loaded (so "select all + // matching" is worth offering). matchingCount is filtered-total when a filter + // is active, else the whole portfolio. + const matchingCount = hasActiveFilters ? filteredTotal : totalCount; + const allLoadedSelected = + selectedIds.length > 0 && selectedIds.length === tableData.length; + const moreMatchThanLoaded = tableData.length < matchingCount; + // The not-modelled banner owns the Run-modelling CTA in this state, so the // toolbar must NOT also show one — otherwise two identical navy buttons appear. const showModelNudge = @@ -477,7 +502,7 @@ export default function PropertyTable({ if (pagination.pageIndex !== 0) setPagination((p) => ({ ...p, pageIndex: 0 })); // The selection is filter-scoped — a changed filter invalidates it. - if (selectedIds.length > 0) setRowSelection({}); + if (selectedIds.length > 0 || selectAllMatching) clearSelection(); } const [isFetchingMore, setIsFetchingMore] = useState(false); @@ -586,11 +611,13 @@ export default function PropertyTable({ properties )} - {selectedIds.length > 0 && ( + {(selectedIds.length > 0 || selectAllMatching) && ( - {selectedIds.length.toLocaleString()} selected + {selectAllMatching + ? `All ${matchingCount.toLocaleString()} selected` + : `${selectedIds.length.toLocaleString()} selected`} + )} + {selectAllMatching && ( + + )} +