Feat: Geolocation
This commit is contained in:
54
src/App.js
54
src/App.js
@@ -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();
|
||||||
|
|||||||
@@ -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.');
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user