minor changes
This commit is contained in:
38
src/App.js
38
src/App.js
@@ -5,6 +5,7 @@ import './App.css';
|
||||
import 'react-date-range/dist/styles.css';
|
||||
import 'react-date-range/dist/theme/default.css';
|
||||
import { formatDateValue, formatRangeLabel } from './utils/dateUtils';
|
||||
import { inferLocationLabel } from './utils/locationLabel';
|
||||
import useSyncProgress from './hooks/useSyncProgress';
|
||||
import useNotificationSettings from './hooks/useNotificationSettings';
|
||||
import useConfigManager from './hooks/useConfigManager';
|
||||
@@ -634,14 +635,15 @@ function App() {
|
||||
);
|
||||
}
|
||||
|
||||
const dashboardContent = (
|
||||
<DashboardView
|
||||
session={session}
|
||||
onRefresh={refreshStoresAndConfig}
|
||||
onLogout={handleLogout}
|
||||
notificationPanelOpen={notificationPanelOpen}
|
||||
onToggleNotificationPanel={() => setNotificationPanelOpen((prev) => !prev)}
|
||||
notificationProps={{
|
||||
const userLocationWithLabel = useMemo(() => {
|
||||
if (!preferences?.location) {
|
||||
return null;
|
||||
}
|
||||
const label = inferLocationLabel(preferences.location, stores);
|
||||
return label ? { ...preferences.location, label } : { ...preferences.location };
|
||||
}, [preferences?.location, stores]);
|
||||
|
||||
const sharedNotificationProps = {
|
||||
error: notificationError,
|
||||
message: notificationMessage,
|
||||
settings: notificationSettings,
|
||||
@@ -656,7 +658,16 @@ function App() {
|
||||
onCopyLink: copyToClipboard,
|
||||
copyFeedback,
|
||||
ntfyPreviewUrl
|
||||
}}
|
||||
};
|
||||
|
||||
const dashboardContent = (
|
||||
<DashboardView
|
||||
session={session}
|
||||
onRefresh={refreshStoresAndConfig}
|
||||
onLogout={handleLogout}
|
||||
notificationPanelOpen={notificationPanelOpen}
|
||||
onToggleNotificationPanel={() => setNotificationPanelOpen((prev) => !prev)}
|
||||
notificationProps={sharedNotificationProps}
|
||||
stores={stores}
|
||||
availableCollapsed={availableCollapsed}
|
||||
onToggleStores={() => setAvailableCollapsed((prev) => !prev)}
|
||||
@@ -681,7 +692,7 @@ function App() {
|
||||
canDelete={Boolean(session?.isAdmin)}
|
||||
focusedStoreId={focusedStoreId}
|
||||
onClearFocus={() => setFocusedStoreId(null)}
|
||||
userLocation={preferences?.location || null}
|
||||
userLocation={userLocationWithLabel}
|
||||
locationLoading={preferencesLoading}
|
||||
locationSaving={locationSaving}
|
||||
locationError={preferencesError}
|
||||
@@ -721,9 +732,14 @@ function App() {
|
||||
<StoreWatchPage
|
||||
authorizedFetch={authorizedFetch}
|
||||
knownStores={stores}
|
||||
userLocation={preferences?.location || null}
|
||||
userLocation={userLocationWithLabel}
|
||||
locationLoading={preferencesLoading}
|
||||
locationSaving={locationSaving}
|
||||
locationError={preferencesError}
|
||||
onRequestLocation={updateLocation}
|
||||
notificationPanelOpen={notificationPanelOpen}
|
||||
onToggleNotificationPanel={() => setNotificationPanelOpen((prev) => !prev)}
|
||||
notificationProps={sharedNotificationProps}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -211,7 +211,11 @@ const NotificationPanel = ({
|
||||
<p>Standort wird geladen...</p>
|
||||
) : location ? (
|
||||
<p>
|
||||
Aktueller Standort: {location.lat.toFixed(4)}, {location.lon.toFixed(4)} (
|
||||
Aktueller Standort: {location.lat.toFixed(4)}, {location.lon.toFixed(4)}
|
||||
{location.label && (
|
||||
<span className="ml-1 font-semibold text-blue-900">– {location.label}</span>
|
||||
)}{' '}
|
||||
(
|
||||
{location.updatedAt ? new Date(location.updatedAt).toLocaleString('de-DE') : 'Zeit unbekannt'})
|
||||
</p>
|
||||
) : (
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
useReactTable
|
||||
} from '@tanstack/react-table';
|
||||
import { haversineDistanceKm } from '../utils/distance';
|
||||
import NotificationPanel from './NotificationPanel';
|
||||
|
||||
const REGION_STORAGE_KEY = 'storeWatchRegionSelection';
|
||||
const WATCH_TABLE_STATE_KEY = 'storeWatchTableState';
|
||||
@@ -92,7 +93,12 @@ const StoreWatchPage = ({
|
||||
knownStores = [],
|
||||
userLocation,
|
||||
onRequestLocation,
|
||||
locationLoading = false
|
||||
locationLoading = false,
|
||||
locationSaving = false,
|
||||
locationError = '',
|
||||
notificationPanelOpen = false,
|
||||
onToggleNotificationPanel = () => {},
|
||||
notificationProps
|
||||
}) => {
|
||||
const [regions, setRegions] = useState([]);
|
||||
const [selectedRegionId, setSelectedRegionId] = useState(() => {
|
||||
@@ -120,6 +126,9 @@ const StoreWatchPage = ({
|
||||
const [locationPromptTriggered, setLocationPromptTriggered] = useState(false);
|
||||
const [locationPromptPending, setLocationPromptPending] = useState(false);
|
||||
const [locationPromptError, setLocationPromptError] = useState('');
|
||||
const combinedLocationError = locationError || locationPromptError;
|
||||
const locationActionBusy = locationSaving || locationPromptPending;
|
||||
const formatCoordinate = (value) => (Number.isFinite(value) ? Number(value).toFixed(4) : '–');
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
@@ -183,6 +192,16 @@ const StoreWatchPage = ({
|
||||
requestLocation();
|
||||
}, [userLocation, locationLoading, onRequestLocation, locationPromptTriggered, requestLocation]);
|
||||
|
||||
const handleManualDetectLocation = useCallback(() => {
|
||||
setLocationPromptError('');
|
||||
requestLocation();
|
||||
}, [requestLocation]);
|
||||
|
||||
const handleClearStoredLocation = useCallback(async () => {
|
||||
setLocationPromptError('');
|
||||
await onRequestLocation?.(null);
|
||||
}, [onRequestLocation]);
|
||||
|
||||
const watchedIds = useMemo(
|
||||
() => new Set(watchList.map((entry) => String(entry.storeId))),
|
||||
[watchList]
|
||||
@@ -848,6 +867,58 @@ const StoreWatchPage = ({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 flex flex-wrap items-center justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleNotificationPanel}
|
||||
className={`flex items-center justify-center w-11 h-11 rounded-full border ${
|
||||
notificationPanelOpen ? 'border-blue-500 text-blue-600' : 'border-gray-300 text-gray-600'
|
||||
} hover:text-blue-700 hover:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-400 transition`}
|
||||
title="Benachrichtigungen konfigurieren"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543-.89 3.31.877 2.42 2.42a1.724 1.724 0 0 0 1.065 2.572c1.757.426 1.757 2.924 0 3.35a1.724 1.724 0 0 0-1.066 2.573c.89 1.543-.877 3.31-2.42 2.42a1.724 1.724 0 0 0-2.572 1.065c-.426 1.757-2.924 1.757-3.35 0a1.724 1.724 0 0 0-2.573-1.066c-1.543.89-3.31-.877-2.42-2.42a1.724 1.724 0 0 0-1.065-2.572c-1.757-.426-1.757-2.924 0-3.35a1.724 1.724 0 0 0 1.066-2.573c-.89-1.543.877-3.31 2.42-2.42.996.575 2.273.155 2.573-1.065z"
|
||||
/>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
{notificationProps?.loading && <span className="text-sm text-gray-500">Lade…</span>}
|
||||
</div>
|
||||
|
||||
{notificationPanelOpen && notificationProps && (
|
||||
<NotificationPanel
|
||||
{...notificationProps}
|
||||
location={userLocation}
|
||||
locationLoading={locationLoading}
|
||||
locationSaving={locationActionBusy}
|
||||
locationError={combinedLocationError}
|
||||
onDetectLocation={handleManualDetectLocation}
|
||||
onClearLocation={handleClearStoredLocation}
|
||||
/>
|
||||
)}
|
||||
|
||||
{userLocation && !notificationPanelOpen && (
|
||||
<div className="mb-4 rounded-lg border border-blue-100 bg-blue-50 p-4 text-sm text-blue-900">
|
||||
<p>
|
||||
Standort gespeichert:{' '}
|
||||
{userLocation.label ? (
|
||||
<span className="font-semibold">{userLocation.label}</span>
|
||||
) : (
|
||||
'Koordinaten'
|
||||
)}{' '}
|
||||
• {formatCoordinate(userLocation.lat)}, {formatCoordinate(userLocation.lon)}
|
||||
</p>
|
||||
<p className="text-xs text-blue-800 mt-1">
|
||||
Aktualisiert:{' '}
|
||||
{userLocation.updatedAt ? new Date(userLocation.updatedAt).toLocaleString('de-DE') : 'Zeit unbekannt'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(error || status) && (
|
||||
<div className="mb-4 space-y-2">
|
||||
{error && (
|
||||
@@ -863,16 +934,16 @@ const StoreWatchPage = ({
|
||||
|
||||
{!userLocation && !locationLoading && onRequestLocation && (
|
||||
<div className="mb-6 rounded-lg border border-blue-200 bg-blue-50 p-4 text-sm text-blue-900">
|
||||
{locationPromptPending ? (
|
||||
{locationActionBusy ? (
|
||||
<p>Standort wird automatisch angefragt, um Entfernungen berechnen zu können...</p>
|
||||
) : locationPromptError ? (
|
||||
) : combinedLocationError ? (
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<p className="flex-1">{locationPromptError}</p>
|
||||
<p className="flex-1">{combinedLocationError}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={requestLocation}
|
||||
onClick={handleManualDetectLocation}
|
||||
className="inline-flex items-center justify-center rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-700 disabled:opacity-60"
|
||||
disabled={locationPromptPending}
|
||||
disabled={locationActionBusy}
|
||||
>
|
||||
Erneut versuchen
|
||||
</button>
|
||||
|
||||
65
src/utils/locationLabel.js
Normal file
65
src/utils/locationLabel.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import { haversineDistanceKm } from './distance';
|
||||
|
||||
const DEFAULT_MAX_DISTANCE_KM = 60;
|
||||
|
||||
export function inferLocationLabel(location, stores = [], options = {}) {
|
||||
if (
|
||||
!location ||
|
||||
typeof location !== 'object' ||
|
||||
!Number.isFinite(location.lat) ||
|
||||
!Number.isFinite(location.lon) ||
|
||||
!Array.isArray(stores) ||
|
||||
stores.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const maxDistanceKm = Number.isFinite(options.maxDistanceKm)
|
||||
? options.maxDistanceKm
|
||||
: DEFAULT_MAX_DISTANCE_KM;
|
||||
|
||||
let closest = null;
|
||||
for (const store of stores) {
|
||||
const storeLat = Number(store?.location?.lat);
|
||||
const storeLon = Number(store?.location?.lon);
|
||||
if (!Number.isFinite(storeLat) || !Number.isFinite(storeLon)) {
|
||||
continue;
|
||||
}
|
||||
const distance = haversineDistanceKm(location.lat, location.lon, storeLat, storeLon);
|
||||
if (distance === null) {
|
||||
continue;
|
||||
}
|
||||
if (closest && distance >= closest.distance) {
|
||||
continue;
|
||||
}
|
||||
const labelParts = [];
|
||||
if (store.city) {
|
||||
labelParts.push(store.city);
|
||||
}
|
||||
if (store.region?.name) {
|
||||
const cityNormalized = store.city?.toLowerCase();
|
||||
const regionNormalized = store.region.name.toLowerCase();
|
||||
if (!cityNormalized || cityNormalized !== regionNormalized) {
|
||||
labelParts.push(store.region.name);
|
||||
}
|
||||
}
|
||||
if (labelParts.length === 0 && store.name) {
|
||||
labelParts.push(store.name);
|
||||
}
|
||||
const label =
|
||||
labelParts.length > 0 ? labelParts.join(' • ') : `Store ${store.id ?? ''}`.trim();
|
||||
closest = {
|
||||
label,
|
||||
distance
|
||||
};
|
||||
}
|
||||
|
||||
if (!closest) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (maxDistanceKm <= 0 || closest.distance <= maxDistanceKm) {
|
||||
return closest.label;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user