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, authorizedFetch,
bootstrapSession, bootstrapSession,
performLogout, performLogout,
handleUnauthorized,
storeToken, storeToken,
getStoredToken getStoredToken
} = useSessionManager({ } = useSessionManager({
@@ -456,31 +455,34 @@ function App() {
} }
}; };
const handleDateRangeSelection = useCallback((entryId, startDate, endDate) => { const handleDateRangeSelection = useCallback(
setIsDirty(true); (entryId, startDate, endDate) => {
setConfig((prev) => setIsDirty(true);
prev.map((item) => { setConfig((prev) =>
if (item.id !== entryId) { prev.map((item) => {
return item; if (item.id !== entryId) {
} return item;
const updated = { ...item }; }
const startValue = formatDateValue(startDate); const updated = { ...item };
const endValue = formatDateValue(endDate); const startValue = formatDateValue(startDate);
if (startValue || endValue) { const endValue = formatDateValue(endDate);
updated.desiredDateRange = { if (startValue || endValue) {
start: startValue || endValue, updated.desiredDateRange = {
end: endValue || startValue start: startValue || endValue,
}; end: endValue || startValue
} else if (updated.desiredDateRange) { };
delete updated.desiredDateRange; } else if (updated.desiredDateRange) {
} delete updated.desiredDateRange;
if (updated.desiredDate) { }
delete updated.desiredDate; if (updated.desiredDate) {
} delete updated.desiredDate;
return updated; }
}) return updated;
); })
}, [setConfig]); );
},
[setConfig, setIsDirty]
);
const configMap = useMemo(() => { const configMap = useMemo(() => {
const map = new Map(); const map = new Map();

View File

@@ -16,14 +16,144 @@ const ColumnTextFilter = ({ column, placeholder }) => {
if (!column.getCanFilter()) { if (!column.getCanFilter()) {
return null; return null;
} }
const configTableData = useMemo(() => { return (
return Array.isArray(visibleConfig) <input
? visibleConfig.map((item) => ({ type="text"
...item, value={column.getFilterValue() ?? ''}
normalizedLabel: (item.label || '').toLowerCase() 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"
}, [visibleConfig]); />
);
};
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]
);
const weekdaysOptions = useMemo( const weekdaysOptions = useMemo(
() => () =>
@@ -251,134 +381,6 @@ const ColumnTextFilter = ({ column, placeholder }) => {
getFilteredRowModel: getFilteredRowModel() 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(() => { const handleDetectLocation = useCallback(() => {
if (!navigator.geolocation) { if (!navigator.geolocation) {
setGeoError('Standortbestimmung wird von diesem Browser nicht unterstützt.'); setGeoError('Standortbestimmung wird von diesem Browser nicht unterstützt.');

View File

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