diff --git a/fdm-app/app/components/blocks/rotation/table.tsx b/fdm-app/app/components/blocks/rotation/table.tsx index 8daff223b..c3f709552 100644 --- a/fdm-app/app/components/blocks/rotation/table.tsx +++ b/fdm-app/app/components/blocks/rotation/table.tsx @@ -9,6 +9,7 @@ import { getFilteredRowModel, getSortedRowModel, type Row, + type RowSelectionState, type SortingState, useReactTable, type VisibilityState, @@ -22,8 +23,8 @@ import { NavLink, useLocation, useParams } from "react-router-dom" import { toast as notify } from "sonner" import { modifySearchParams } from "@/app/lib/url-utils" import { useActiveTableFormStore } from "@/app/store/active-table-form" -import { useFieldFilterStore } from "@/app/store/field-filter" -import { useFieldSelectionStore } from "@/app/store/field-selection" +import { useRotationFilterStore } from "@/app/store/field-filter" +import { useRotationSelectionStore } from "@/app/store/rotation-selection" import { Button } from "~/components/ui/button" import { DropdownMenu, @@ -64,7 +65,7 @@ export function DataTable({ }: DataTableProps) { const [sorting, setSorting] = useState([]) const [columnFilters, setColumnFilters] = useState([]) - const fieldFilter = useFieldFilterStore() + const fieldFilter = useRotationFilterStore() const isMobile = useIsMobile() const [columnVisibility, setColumnVisibility] = useState( isMobile @@ -74,9 +75,11 @@ export function DataTable({ const lastSelectedRowIndex = useRef(null) const location = useLocation() - const fieldIds = useFieldSelectionStore((state) => state.fieldIds) - const setFieldIds = useFieldSelectionStore((state) => state.setFieldIds) - const syncFarm = useFieldSelectionStore((state) => state.syncFarm) + const selection = useRotationSelectionStore((state) => state.selection) + const updateSelection = useRotationSelectionStore( + (state) => state.updateSelection, + ) + const syncFarm = useRotationSelectionStore((state) => state.syncFarm) useEffect(() => { setColumnVisibility( @@ -101,6 +104,24 @@ export function DataTable({ (store) => store.clearActiveForm, ) + function handleSelection(rowSelection: RowSelectionState) { + // Sync to store + const newSelection = Object.fromEntries( + table + .getFilteredRowModel() + .rows.map((row) => [ + (row.original as CropRow).b_lu_catalogue, + Object.fromEntries( + row.subRows.map((fieldRow) => [ + (fieldRow.original as FieldRow).b_id, + rowSelection[fieldRow.id], + ]), + ), + ]), + ) + updateSelection(newSelection) + } + const handleRowClick = ( row: Row, event: React.MouseEvent, @@ -151,11 +172,7 @@ export function DataTable({ } } - // Sync to store - const newFieldIds = Object.keys(newRowSelection).filter( - (k) => !k.startsWith("crop_") && newRowSelection[k], - ) - setFieldIds(newFieldIds) + handleSelection(newRowSelection) } } else { lastSelectedRowIndex.current = null @@ -186,6 +203,7 @@ export function DataTable({ return { ...field, + b_lu_catalogue: (item as CropRow).b_lu_catalogue, searchTarget: `${field.b_name} ${commonTerms} ${dateTermsArr(field.b_lu_start)} ${dateTermsArr(field.b_lu_end)} ${dateTermsArr(field.b_lu_harvest_date)}`, } }) @@ -221,40 +239,38 @@ export function DataTable({ ) } + // biome-ignore lint/correctness/useExhaustiveDependencies: the filter function is pure const rowSelection = useMemo(() => { - const sel: Record = {} - for (const id of fieldIds) { - sel[id] = true - } - for (const row of memoizedData) { - if (row.type === "crop") { - let all = true - let has = false - for (const field of row.fields) { - has = true - if (fieldIds.includes(field.b_id)) { - sel[field.b_id] = true - } else { - all = false - } - } - if (has && all) { - sel[`crop_${row.b_lu_catalogue}`] = true - } - } else { - if (fieldIds.includes(row.b_id)) { - sel[row.b_id] = true - } - } - } - return sel - }, [fieldIds, memoizedData]) + return Object.fromEntries([ + // Crop selection state is derived from whether all its fields are selected + ...memoizedData.map((crop) => [ + `crop_${crop.b_lu_catalogue}`, + crop.fields.every( + (field) => + !fuzzySearchAndProductivityFilter( + { original: field } as unknown as Row, + null, + fieldFilter, + ) || selection[crop.b_lu_catalogue]?.[field.b_id], + ), + ]), + // Include each field's selection state too + ...memoizedData.flatMap((crop) => + crop.fields.map((field) => [ + `${crop.b_lu_catalogue}_${field.b_id}`, + selection[crop.b_lu_catalogue]?.[field.b_id], + ]), + ), + ]) + }, [selection, memoizedData, fieldFilter]) const table = useReactTable({ data: memoizedData, columns, getRowId: (row) => - row.type === "crop" ? `crop_${row.b_lu_catalogue}` : row.b_id, + row.type === "crop" + ? `crop_${row.b_lu_catalogue}` + : `${row.b_lu_catalogue}_${row.b_id}`, getCoreRowModel: getCoreRowModel(), onSortingChange: setSorting, getSortedRowModel: getSortedRowModel(), @@ -274,10 +290,7 @@ export function DataTable({ }, onRowSelectionChange: (fn) => { const selection = typeof fn === "function" ? fn(rowSelection) : fn - const newFieldIds = Object.keys(selection).filter( - (k) => !k.startsWith("crop_") && selection[k], - ) - setFieldIds(newFieldIds) + handleSelection(selection) }, globalFilterFn: fuzzySearchAndProductivityFilter, // There are nulls in the columns which can cause false assumptions if this is not provided @@ -582,4 +595,4 @@ export function DataTable({ ) -} \ No newline at end of file +} diff --git a/fdm-app/app/store/field-filter.ts b/fdm-app/app/store/field-filter.ts index 0dfb1ba9e..0b2e3af0d 100644 --- a/fdm-app/app/store/field-filter.ts +++ b/fdm-app/app/store/field-filter.ts @@ -11,30 +11,37 @@ interface FieldFilterState { syncFarm: (farmId: string) => void } -export const useFieldFilterStore = create()( - persist( - (set, get) => ({ - farmId: null, - showProductiveOnly: false, // Default to showing all fields - searchTerms: "", - toggleShowProductiveOnly: () => - set((state) => ({ - showProductiveOnly: !state.showProductiveOnly, - })), - setSearchTerms: (value) => { - set({ - searchTerms: value, - }) +const createFilterStore = (name: string) => + create()( + persist( + (set, get) => ({ + farmId: null, + showProductiveOnly: false, // Default to showing all fields + searchTerms: "", + toggleShowProductiveOnly: () => + set((state) => ({ + showProductiveOnly: !state.showProductiveOnly, + })), + setSearchTerms: (value) => { + set({ + searchTerms: value, + }) + }, + syncFarm(farmId: string) { + if (get().farmId !== farmId) { + set({ farmId, searchTerms: "" }) + } + }, + }), + { + name: name, // unique name + storage: createJSONStorage(() => ssrSafeSessionJSONStorage), // Use SSR-safe storage }, - syncFarm(farmId: string) { - if (get().farmId !== farmId) { - set({ farmId, searchTerms: "" }) - } - }, - }), - { - name: "field-filter-storage", // unique name - storage: createJSONStorage(() => ssrSafeSessionJSONStorage), // Use SSR-safe storage - }, - ), + ), + ) + +export const useFieldFilterStore = createFilterStore("field-filter-storage") + +export const useRotationFilterStore = createFilterStore( + "rotation-filter-storage", ) diff --git a/fdm-app/app/store/rotation-selection.ts b/fdm-app/app/store/rotation-selection.ts new file mode 100644 index 000000000..a1f97599d --- /dev/null +++ b/fdm-app/app/store/rotation-selection.ts @@ -0,0 +1,43 @@ +import { create } from "zustand" +import { createJSONStorage, persist } from "zustand/middleware" +import { ssrSafeSessionJSONStorage } from "./storage" + +interface RotationSelectionState { + farmId: string | null + selection: Record> + updateSelection: ( + selection: Record>, + ) => void + syncFarm: (farmId: string) => void +} + +export const useRotationSelectionStore = create()( + persist( + (set, get) => ({ + farmId: null, + selection: {}, + updateSelection( + selection: Record>, + ) { + const currentSelection = get().selection + const result: Record> = {} + for (const currentKey of Object.keys(currentSelection)) { + result[currentKey] = { ...currentSelection[currentKey] } + } + for (const key of Object.keys(selection)) { + result[key] = { ...(result[key] ?? {}), ...selection[key] } + } + set({ selection: result }) + }, + syncFarm(farmId: string) { + if (get().farmId !== farmId) { + set({ farmId, selection: {} }) + } + }, + }), + { + name: "rotation-selection-storage", // unique name + storage: createJSONStorage(() => ssrSafeSessionJSONStorage), // Use SSR-safe storage + }, + ), +)