fix: hide unavailable by default

This commit is contained in:
devthejo 2026-03-07 20:55:37 +01:00
parent ec49fef2f3
commit 47928ce9f2
No known key found for this signature in database
GPG key ID: 00CCA7A92B1D5351
3 changed files with 144 additions and 23 deletions

View file

@ -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,
},
}); });

View file

@ -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,
}; };
} }

View file

@ -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,