fix: hide unavailable by default
This commit is contained in:
parent
ec49fef2f3
commit
47928ce9f2
3 changed files with 144 additions and 23 deletions
|
|
@ -1,11 +1,12 @@
|
||||||
import React, { useCallback } from "react";
|
import React, { useCallback } from "react";
|
||||||
import { View, FlatList, StyleSheet } from "react-native";
|
import { View, FlatList, StyleSheet } from "react-native";
|
||||||
import { Button } from "react-native-paper";
|
import { Button, Switch } from "react-native-paper";
|
||||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
|
|
||||||
import Text from "~/components/Text";
|
import Text from "~/components/Text";
|
||||||
import Loader from "~/components/Loader";
|
import Loader from "~/components/Loader";
|
||||||
import { useTheme } from "~/theme";
|
import { useTheme } from "~/theme";
|
||||||
|
import { defibsActions } from "~/stores";
|
||||||
|
|
||||||
import useNearbyDefibs from "./useNearbyDefibs";
|
import useNearbyDefibs from "./useNearbyDefibs";
|
||||||
import DefibRow from "./DefibRow";
|
import DefibRow from "./DefibRow";
|
||||||
|
|
@ -89,10 +90,81 @@ function EmptyNoResults() {
|
||||||
|
|
||||||
const keyExtractor = (item) => item.id;
|
const keyExtractor = (item) => item.id;
|
||||||
|
|
||||||
|
function EmptyNoAvailable({ showUnavailable }) {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
return (
|
||||||
|
<View style={styles.emptyContainer}>
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name="heart-pulse"
|
||||||
|
size={56}
|
||||||
|
color={colors.onSurfaceVariant || colors.grey}
|
||||||
|
style={styles.emptyIcon}
|
||||||
|
/>
|
||||||
|
<Text style={styles.emptyTitle}>Aucun défibrillateur disponible</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.emptyText,
|
||||||
|
{ color: colors.onSurfaceVariant || colors.grey },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
Aucun défibrillateur actuellement ouvert dans un rayon de 10 km. Activez
|
||||||
|
l'option « Afficher les indisponibles » pour voir tous les
|
||||||
|
défibrillateurs.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvailabilityToggle({ showUnavailable, allCount, filteredCount }) {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
const onToggle = useCallback(() => {
|
||||||
|
defibsActions.setShowUnavailable(!showUnavailable);
|
||||||
|
}, [showUnavailable]);
|
||||||
|
|
||||||
|
const countLabel =
|
||||||
|
!showUnavailable && allCount > filteredCount
|
||||||
|
? ` (${allCount - filteredCount} masqués)`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.toggleRow,
|
||||||
|
{ borderBottomColor: colors.outlineVariant || colors.grey },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View style={styles.toggleLabelContainer}>
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name="eye-off-outline"
|
||||||
|
size={18}
|
||||||
|
color={colors.onSurfaceVariant || colors.grey}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.toggleLabel,
|
||||||
|
{ color: colors.onSurfaceVariant || colors.grey },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
Afficher les indisponibles{countLabel}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Switch value={showUnavailable} onValueChange={onToggle} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default React.memo(function DAEListListe() {
|
export default React.memo(function DAEListListe() {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const { defibs, loading, error, noLocation, hasLocation, reload } =
|
const {
|
||||||
useNearbyDefibs();
|
defibs,
|
||||||
|
allDefibs,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
noLocation,
|
||||||
|
hasLocation,
|
||||||
|
reload,
|
||||||
|
showUnavailable,
|
||||||
|
} = useNearbyDefibs();
|
||||||
|
|
||||||
const renderItem = useCallback(({ item }) => <DefibRow defib={item} />, []);
|
const renderItem = useCallback(({ item }) => <DefibRow defib={item} />, []);
|
||||||
|
|
||||||
|
|
@ -102,23 +174,27 @@ export default React.memo(function DAEListListe() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loading initial data
|
// Loading initial data
|
||||||
if (loading && defibs.length === 0) {
|
if (loading && allDefibs.length === 0) {
|
||||||
return <Loader />;
|
return <Loader />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error state (non-blocking if we have stale data)
|
// Error state (non-blocking if we have stale data)
|
||||||
if (error && defibs.length === 0) {
|
if (error && allDefibs.length === 0) {
|
||||||
return <EmptyError error={error} onRetry={reload} />;
|
return <EmptyError error={error} onRetry={reload} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No results
|
// No results at all
|
||||||
if (!loading && defibs.length === 0 && hasLocation) {
|
if (!loading && allDefibs.length === 0 && hasLocation) {
|
||||||
return <EmptyNoResults />;
|
return <EmptyNoResults />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Has defibs but none available (filtered to empty)
|
||||||
|
const showEmptyAvailable =
|
||||||
|
!loading && defibs.length === 0 && allDefibs.length > 0 && !showUnavailable;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
||||||
{error && defibs.length > 0 && (
|
{error && allDefibs.length > 0 && (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.errorBanner,
|
styles.errorBanner,
|
||||||
|
|
@ -140,6 +216,14 @@ export default React.memo(function DAEListListe() {
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
<AvailabilityToggle
|
||||||
|
showUnavailable={showUnavailable}
|
||||||
|
allCount={allDefibs.length}
|
||||||
|
filteredCount={defibs.length}
|
||||||
|
/>
|
||||||
|
{showEmptyAvailable ? (
|
||||||
|
<EmptyNoAvailable />
|
||||||
|
) : (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={defibs}
|
data={defibs}
|
||||||
keyExtractor={keyExtractor}
|
keyExtractor={keyExtractor}
|
||||||
|
|
@ -149,6 +233,7 @@ export default React.memo(function DAEListListe() {
|
||||||
maxToRenderPerBatch={10}
|
maxToRenderPerBatch={10}
|
||||||
windowSize={5}
|
windowSize={5}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -194,4 +279,21 @@ const styles = StyleSheet.create({
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
|
toggleRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 8,
|
||||||
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
|
},
|
||||||
|
toggleLabelContainer: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
toggleLabel: {
|
||||||
|
fontSize: 13,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,23 @@
|
||||||
import { useEffect, useRef, useCallback, useState } from "react";
|
import { useEffect, useRef, useCallback, useMemo, useState } from "react";
|
||||||
import useLocation from "~/hooks/useLocation";
|
import useLocation from "~/hooks/useLocation";
|
||||||
import { defibsActions, useDefibsState } from "~/stores";
|
import { defibsActions, useDefibsState } from "~/stores";
|
||||||
|
import { getDefibAvailability } from "~/utils/dae/getDefibAvailability";
|
||||||
|
|
||||||
const RADIUS_METERS = 10_000;
|
const RADIUS_METERS = 10_000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shared hook: loads defibs near user and exposes location + loading state.
|
* Shared hook: loads defibs near user and exposes location + loading state.
|
||||||
* The results live in the zustand store so both Liste and Carte tabs share them.
|
* The results live in the zustand store so both Liste and Carte tabs share them.
|
||||||
|
* By default, only available (open) defibs are returned; toggle showUnavailable to see all.
|
||||||
*/
|
*/
|
||||||
export default function useNearbyDefibs() {
|
export default function useNearbyDefibs() {
|
||||||
const { coords, isLastKnown, lastKnownTimestamp } = useLocation();
|
const { coords, isLastKnown, lastKnownTimestamp } = useLocation();
|
||||||
const { nearUserDefibs, loadingNearUser, errorNearUser } = useDefibsState([
|
const { nearUserDefibs, loadingNearUser, errorNearUser, showUnavailable } =
|
||||||
|
useDefibsState([
|
||||||
"nearUserDefibs",
|
"nearUserDefibs",
|
||||||
"loadingNearUser",
|
"loadingNearUser",
|
||||||
"errorNearUser",
|
"errorNearUser",
|
||||||
|
"showUnavailable",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const hasLocation =
|
const hasLocation =
|
||||||
|
|
@ -58,8 +62,17 @@ export default function useNearbyDefibs() {
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [hasLocation]);
|
}, [hasLocation]);
|
||||||
|
|
||||||
|
const filteredDefibs = useMemo(() => {
|
||||||
|
if (showUnavailable) return nearUserDefibs;
|
||||||
|
return nearUserDefibs.filter((d) => {
|
||||||
|
const { status } = getDefibAvailability(d.horaires_std, d.disponible_24h);
|
||||||
|
return status === "open";
|
||||||
|
});
|
||||||
|
}, [nearUserDefibs, showUnavailable]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
defibs: nearUserDefibs,
|
defibs: filteredDefibs,
|
||||||
|
allDefibs: nearUserDefibs,
|
||||||
loading: loadingNearUser,
|
loading: loadingNearUser,
|
||||||
error: errorNearUser,
|
error: errorNearUser,
|
||||||
hasLocation,
|
hasLocation,
|
||||||
|
|
@ -68,5 +81,6 @@ export default function useNearbyDefibs() {
|
||||||
lastKnownTimestamp,
|
lastKnownTimestamp,
|
||||||
coords,
|
coords,
|
||||||
reload: loadDefibs,
|
reload: loadDefibs,
|
||||||
|
showUnavailable,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@ export default createAtom(({ merge, reset }) => {
|
||||||
merge({ showDaeSuggestModal });
|
merge({ showDaeSuggestModal });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setShowUnavailable: (showUnavailable) => {
|
||||||
|
merge({ showUnavailable });
|
||||||
|
},
|
||||||
|
|
||||||
loadNearUser: async ({
|
loadNearUser: async ({
|
||||||
userLonLat,
|
userLonLat,
|
||||||
radiusMeters = DEFAULT_NEAR_USER_RADIUS_M,
|
radiusMeters = DEFAULT_NEAR_USER_RADIUS_M,
|
||||||
|
|
@ -103,6 +107,7 @@ export default createAtom(({ merge, reset }) => {
|
||||||
showDefibsOnAlertMap: false,
|
showDefibsOnAlertMap: false,
|
||||||
selectedDefib: null,
|
selectedDefib: null,
|
||||||
showDaeSuggestModal: false,
|
showDaeSuggestModal: false,
|
||||||
|
showUnavailable: false,
|
||||||
|
|
||||||
loadingNearUser: false,
|
loadingNearUser: false,
|
||||||
loadingCorridor: false,
|
loadingCorridor: false,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue