Feat: Geolocation

This commit is contained in:
2025-11-10 18:00:07 +01:00
parent 41ed69a058
commit 0f941b7174
3 changed files with 167 additions and 163 deletions

View File

@@ -92,7 +92,6 @@ function App() {
authorizedFetch,
bootstrapSession,
performLogout,
handleUnauthorized,
storeToken,
getStoredToken
} = useSessionManager({
@@ -456,7 +455,8 @@ function App() {
}
};
const handleDateRangeSelection = useCallback((entryId, startDate, endDate) => {
const handleDateRangeSelection = useCallback(
(entryId, startDate, endDate) => {
setIsDirty(true);
setConfig((prev) =>
prev.map((item) => {
@@ -480,7 +480,9 @@ function App() {
return updated;
})
);
}, [setConfig]);
},
[setConfig, setIsDirty]
);
const configMap = useMemo(() => {
const map = new Map();

View File

@@ -16,14 +16,144 @@ const ColumnTextFilter = ({ column, placeholder }) => {
if (!column.getCanFilter()) {
return null;
}
const configTableData = useMemo(() => {
return Array.isArray(visibleConfig)
return (
<input
type="text"
value={column.getFilterValue() ?? ''}
onChange={(event) => column.setFilterValue(event.target.value || undefined)}
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, placeholder = 'Alle' }) => {
if (!column.getCanFilter()) {
return null;
}
return (
<select
value={column.getFilterValue() ?? ''}
onChange={(event) => column.setFilterValue(event.target.value || undefined)}
className="mt-1 w-full rounded border px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-blue-500"
>
<option value="">{placeholder}</option>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
);
};
function readConfigTableState() {
if (typeof window === 'undefined') {
return { sorting: [], columnFilters: [] };
}
try {
const raw = window.localStorage.getItem(CONFIG_TABLE_STATE_KEY);
if (!raw) {
return { sorting: [], columnFilters: [] };
}
const parsed = JSON.parse(raw);
return {
sorting: Array.isArray(parsed.sorting) ? parsed.sorting : [],
columnFilters: Array.isArray(parsed.columnFilters) ? parsed.columnFilters : []
};
} catch {
return { sorting: [], columnFilters: [] };
}
}
function persistConfigTableState(state) {
if (typeof window === 'undefined') {
return;
}
try {
window.localStorage.setItem(CONFIG_TABLE_STATE_KEY, JSON.stringify(state));
} catch {
/* ignore */
}
}
const DashboardView = ({
session,
onRefresh,
onLogout,
notificationPanelOpen,
onToggleNotificationPanel,
notificationProps,
stores,
availableCollapsed,
onToggleStores,
onStoreSelect,
configMap,
error,
onDismissError,
status,
visibleConfig,
config,
onToggleActive,
onToggleProfileCheck,
onToggleOnlyNotify,
onWeekdayChange,
weekdays,
onRangePickerRequest,
formatRangeLabel,
onSaveConfig,
onResetConfig,
onHideEntry,
onDeleteEntry,
canDelete,
focusedStoreId,
onClearFocus,
userLocation,
locationLoading,
locationSaving,
locationError,
onUpdateLocation
}) => {
useEffect(() => {
if (!focusedStoreId) {
return;
}
const row = document.querySelector(`[data-store-row="${focusedStoreId}"]`);
if (!row) {
onClearFocus();
return;
}
row.scrollIntoView({ behavior: 'smooth', block: 'center' });
row.classList.add('dashboard-row-highlight', 'ring-4', 'ring-blue-400');
const timeout = setTimeout(() => {
row.classList.remove('dashboard-row-highlight', 'ring-4', 'ring-blue-400');
onClearFocus();
}, 2500);
return () => {
clearTimeout(timeout);
row.classList.remove('dashboard-row-highlight', 'ring-4', 'ring-blue-400');
};
}, [focusedStoreId, onClearFocus]);
const [geoBusy, setGeoBusy] = useState(false);
const [geoError, setGeoError] = useState('');
const initialTableState = useMemo(() => readConfigTableState(), []);
const [tableSorting, setTableSorting] = useState(initialTableState.sorting);
const [tableFilters, setTableFilters] = useState(initialTableState.columnFilters);
useEffect(() => {
persistConfigTableState({ sorting: tableSorting, columnFilters: tableFilters });
}, [tableSorting, tableFilters]);
const configTableData = useMemo(
() =>
Array.isArray(visibleConfig)
? visibleConfig.map((item) => ({
...item,
normalizedLabel: (item.label || '').toLowerCase()
}))
: [];
}, [visibleConfig]);
: [],
[visibleConfig]
);
const weekdaysOptions = useMemo(
() =>
@@ -251,134 +381,6 @@ const ColumnTextFilter = ({ column, placeholder }) => {
getFilteredRowModel: getFilteredRowModel()
});
return (
<input
type="text"
value={column.getFilterValue() ?? ''}
onChange={(event) => column.setFilterValue(event.target.value || undefined)}
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, placeholder = 'Alle' }) => {
if (!column.getCanFilter()) {
return null;
}
return (
<select
value={column.getFilterValue() ?? ''}
onChange={(event) => column.setFilterValue(event.target.value || undefined)}
className="mt-1 w-full rounded border px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-blue-500"
>
<option value="">{placeholder}</option>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
);
};
const DashboardView = ({
session,
onRefresh,
onLogout,
notificationPanelOpen,
onToggleNotificationPanel,
notificationProps,
stores,
availableCollapsed,
onToggleStores,
onStoreSelect,
configMap,
error,
onDismissError,
status,
visibleConfig,
config,
onToggleActive,
onToggleProfileCheck,
onToggleOnlyNotify,
onWeekdayChange,
weekdays,
onRangePickerRequest,
formatRangeLabel,
onSaveConfig,
onResetConfig,
onHideEntry,
onDeleteEntry,
canDelete,
focusedStoreId,
onClearFocus,
userLocation,
locationLoading,
locationSaving,
locationError,
onUpdateLocation
}) => {
const loadTableState = useCallback(() => {
if (typeof window === 'undefined') {
return { sorting: [], columnFilters: [] };
}
try {
const raw = window.localStorage.getItem(CONFIG_TABLE_STATE_KEY);
if (!raw) {
return { sorting: [], columnFilters: [] };
}
const parsed = JSON.parse(raw);
return {
sorting: Array.isArray(parsed.sorting) ? parsed.sorting : [],
columnFilters: Array.isArray(parsed.columnFilters) ? parsed.columnFilters : []
};
} catch {
return { sorting: [], columnFilters: [] };
}
}, []);
const initialTableState = loadTableState();
useEffect(() => {
if (!focusedStoreId) {
return;
}
const row = document.querySelector(`[data-store-row="${focusedStoreId}"]`);
if (!row) {
onClearFocus();
return;
}
row.scrollIntoView({ behavior: 'smooth', block: 'center' });
row.classList.add('dashboard-row-highlight', 'ring-4', 'ring-blue-400');
const timeout = setTimeout(() => {
row.classList.remove('dashboard-row-highlight', 'ring-4', 'ring-blue-400');
onClearFocus();
}, 2500);
return () => {
clearTimeout(timeout);
row.classList.remove('dashboard-row-highlight', 'ring-4', 'ring-blue-400');
};
}, [focusedStoreId, onClearFocus]);
const [geoBusy, setGeoBusy] = useState(false);
const [geoError, setGeoError] = useState('');
const [tableSorting, setTableSorting] = useState(initialTableState.sorting);
const [tableFilters, setTableFilters] = useState(initialTableState.columnFilters);
useEffect(() => {
if (typeof window === 'undefined') {
return;
}
try {
window.localStorage.setItem(
CONFIG_TABLE_STATE_KEY,
JSON.stringify({ sorting: tableSorting, columnFilters: tableFilters })
);
} catch {
/* ignore */
}
}, [tableSorting, tableFilters]);
const handleDetectLocation = useCallback(() => {
if (!navigator.geolocation) {
setGeoError('Standortbestimmung wird von diesem Browser nicht unterstützt.');

View File

@@ -500,7 +500,7 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) =>
} finally {
setRegionLoading(false);
}
}, [authorizedFetch, selectedRegionId]);
}, [authorizedFetch]);
const loadSubscriptions = useCallback(async () => {
if (!authorizedFetch) {