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 && (
-
- | Betrieb |
- Ort |
- Kooperation seit |
- Mitglied |
- Überwachen |
-
+ {table.getHeaderGroups().map((headerGroup) => (
+
+ {headerGroup.headers.map((header) => (
+ |
+ {flexRender(header.column.columnDef.header, header.getContext())}
+ |
+ ))}
+
+ ))}
- {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 (
-
- |
- {store.name}
- #{store.id}
+ {table.getRowModel().rows.map((row) => (
+ |
+ {row.getVisibleCells().map((cell) => (
+ |
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
|
-
- {store.city || 'unbekannt'}
- {store.street || ''}
- |
- {sinceLabel} |
-
-
- {isMember ? 'Ja' : 'Nein'}
-
- |
-
- handleToggleStore(store, event.target.checked)}
- />
- |
-
- );
- })}
+ ))}
+
+ ))}