From 49dec43c1e37e839b9317d2888242d339e7b4ec9 Mon Sep 17 00:00:00 2001 From: Meik Date: Mon, 10 Nov 2025 17:11:15 +0100 Subject: [PATCH] =?UTF-8?q?Neue=20Seite=20um=20Betriebe=20zu=20=C3=BCberwa?= =?UTF-8?q?chen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 34 +++ package.json | 1 + src/components/StoreWatchPage.js | 366 ++++++++++++++++++++----------- 3 files changed, 279 insertions(+), 122 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9792f28..522bc14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "temp-react-app", "version": "0.1.0", "dependencies": { + "@tanstack/react-table": "^8.21.3", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", @@ -3550,6 +3551,39 @@ "url": "https://github.com/sponsors/gregberge" } }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", diff --git a/package.json b/package.json index 8795cdb..b2dfcd7 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@tanstack/react-table": "^8.21.3", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", diff --git a/src/components/StoreWatchPage.js b/src/components/StoreWatchPage.js index e6eaf73..4b5c9c5 100644 --- a/src/components/StoreWatchPage.js +++ b/src/components/StoreWatchPage.js @@ -1,4 +1,49 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; +import { + createColumnHelper, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getSortedRowModel, + useReactTable +} from '@tanstack/react-table'; + +const columnHelper = createColumnHelper(); + +const ColumnTextFilter = ({ column, placeholder }) => { + if (!column.getCanFilter()) { + return null; + } + return ( + column.setFilterValue(event.target.value)} + placeholder={placeholder} + className="mt-1 w-full rounded border px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-blue-500" + /> + ); +}; + +const ColumnSelectFilter = ({ column, options }) => { + if (!column.getCanFilter()) { + return null; + } + return ( + + ); +}; const StoreWatchPage = ({ authorizedFetch, knownStores = [] }) => { const [regions, setRegions] = useState([]); @@ -12,8 +57,8 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [] }) => { const [error, setError] = useState(''); const [dirty, setDirty] = useState(false); const [saving, setSaving] = useState(false); - const [filterText, setFilterText] = useState(''); - const [sortBy, setSortBy] = useState('name'); + const [sorting, setSorting] = useState([]); + const [columnFilters, setColumnFilters] = useState([]); const watchedIds = useMemo( () => new Set(watchList.map((entry) => String(entry.storeId))), @@ -48,47 +93,186 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [] }) => { return map; }, [knownStores]); - const filteredStores = useMemo(() => { - const search = filterText.trim().toLowerCase(); - const data = !search - ? [...eligibleStores] - : eligibleStores.filter((store) => { - const haystack = [ - store.name, - store.city, - store.street, - store.zipCode, - store.id - ] - .filter(Boolean) - .map((value) => String(value).toLowerCase()); - return haystack.some((value) => value.includes(search)); - }); - const compareString = (a = '', b = '') => a.localeCompare(b, 'de', { sensitivity: 'base' }); - data.sort((a, b) => { - switch (sortBy) { - case 'city': - return compareString(a.city || '', b.city || '') || compareString(a.name || '', b.name || ''); - case 'created-desc': { - const timeA = a.createdAt ? new Date(a.createdAt).getTime() : 0; - const timeB = b.createdAt ? new Date(b.createdAt).getTime() : 0; - if (timeA === timeB) { - return compareString(a.name || '', b.name || ''); - } - return timeB - timeA; - } - case 'membership': + const tableData = useMemo( + () => + eligibleStores.map((store) => ({ + ...store, + membership: membershipMap.has(String(store.id)) + })), + [eligibleStores, membershipMap] + ); + + const columns = useMemo( + () => [ + columnHelper.accessor('name', { + header: ({ column }) => ( +
+ + +
+ ), + cell: ({ row }) => ( +
+

{row.original.name}

+

#{row.original.id}

+
+ ), + sortingFn: 'alphanumeric', + enableColumnFilter: true, + filterFn: 'includesString' + }), + columnHelper.accessor((row) => row.city || '', { + id: 'city', + header: ({ column }) => ( +
+ + +
+ ), + cell: ({ row }) => ( +
+

{row.original.city || 'unbekannt'}

+

{row.original.street || ''}

+
+ ), + sortingFn: 'alphanumeric', + filterFn: 'includesString' + }), + columnHelper.accessor('createdAt', { + header: ({ column }) => ( + + ), + cell: ({ getValue }) => { + const value = getValue(); return ( - Number(membershipMap.has(String(b.id))) - Number(membershipMap.has(String(a.id))) || - compareString(a.name || '', b.name || '') + + {value ? new Date(value).toLocaleDateString('de-DE') : 'unbekannt'} + ); - case 'name': - default: - return compareString(a.name || '', b.name || ''); - } - }); - return data; - }, [eligibleStores, filterText, sortBy, membershipMap]); + }, + sortingFn: (rowA, rowB, columnId) => { + const a = rowA.getValue(columnId); + const b = rowB.getValue(columnId); + return new Date(a || 0).getTime() - new Date(b || 0).getTime(); + } + }), + columnHelper.accessor('membership', { + header: ({ column }) => ( +
+ + +
+ ), + cell: ({ getValue }) => { + const value = getValue(); + return ( + + {value ? 'Ja' : 'Nein'} + + ); + }, + filterFn: (row, columnId, value) => { + if (value === undefined) { + return true; + } + const boolValue = value === 'true'; + return row.getValue(columnId) === boolValue; + }, + sortingFn: (rowA, rowB, columnId) => { + const a = rowA.getValue(columnId); + const b = rowB.getValue(columnId); + return Number(b) - Number(a); + } + }), + columnHelper.display({ + id: 'watch', + header: () => Überwachen, + cell: ({ row }) => { + const store = row.original; + const checked = watchedIds.has(String(store.id)); + return ( +
+ handleToggleStore(store, event.target.checked)} + /> +
+ ); + } + }) + ], + [handleToggleStore, watchedIds] + ); + + const table = useReactTable({ + data: tableData, + columns, + state: { + sorting, + columnFilters + }, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel() + }); const loadRegions = useCallback(async () => { if (!authorizedFetch) { @@ -333,42 +517,6 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [] }) => {

-
-
-
- - setFilterText(event.target.value)} - className="border rounded-md p-2 w-full" - placeholder="Name, Ort oder PLZ" - disabled={!selectedRegionId} - /> -
-
- - -
-
-
-

Betriebe in der Region

@@ -383,61 +531,35 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [] }) => { {!storesLoading && !selectedRegionId && (

Bitte zuerst eine Region auswählen.

)} - {!storesLoading && selectedRegionId && filteredStores.length === 0 && ( + {!storesLoading && selectedRegionId && table.getRowModel().rows.length === 0 && (

Keine Betriebe gefunden. Prüfe Filter oder sortiere anders.

)} - {!storesLoading && filteredStores.length > 0 && ( + {!storesLoading && table.getRowModel().rows.length > 0 && (
- - - - - - - + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} - {filteredStores.map((store) => { - const checked = watchedIds.has(String(store.id)); - const isMember = membershipMap.has(String(store.id)); - const sinceLabel = store.createdAt - ? new Date(store.createdAt).toLocaleDateString('de-DE') - : 'unbekannt'; - return ( - - + {row.getVisibleCells().map((cell) => ( + - - - - - - ); - })} + ))} + + ))}
BetriebOrtKooperation seitMitgliedÜberwachen
+ {flexRender(header.column.columnDef.header, header.getContext())} +
-

{store.name}

-

#{store.id}

+ {table.getRowModel().rows.map((row) => ( +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} -

{store.city || 'unbekannt'}

-

{store.street || ''}

-
{sinceLabel} - - {isMember ? 'Ja' : 'Nein'} - - - handleToggleStore(store, event.target.checked)} - /> -