Feat: Geolocation
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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.');
|
||||
|
||||
@@ -500,7 +500,7 @@ const StoreWatchPage = ({ authorizedFetch, knownStores = [], userLocation }) =>
|
||||
} finally {
|
||||
setRegionLoading(false);
|
||||
}
|
||||
}, [authorizedFetch, selectedRegionId]);
|
||||
}, [authorizedFetch]);
|
||||
|
||||
const loadSubscriptions = useCallback(async () => {
|
||||
if (!authorizedFetch) {
|
||||
|
||||
Reference in New Issue
Block a user