feat(scan-detail): wire 'Ajouter ma position' button to GPS persist

useScanDetail :
- Nouvelle méthode setLocation(coords) qui persiste latitude / longitude
  (top-level pour MapScreen.hasLocation), locationCapturedAt et
  l'objet location.{latitude, longitude} dans AsyncStorage + state local

ScanDetailScreen :
- handleAddLocation appelle requestAndGetLocation() (useScanLocation hook
  existant) puis setLocation()
- État addingLocation : bouton désactivé + opacity 0.6 + ActivityIndicator
  spinner à la place de l'icône MapPin + label 'Localisation en cours...'
- Toast success / error + hapticSuccess en cas de succès
- La carte location bascule automatiquement vers l'affichage des coords
  une fois persisté → la plante apparaît aussi sur MapScreen et
  SearchScreen mode Map

i18n :
- myPlants.toasts.locationAdded
- myPlants.detail.locating

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yanis 2026-05-01 01:06:10 +02:00
parent 05ea1df6ff
commit bdbdcd7b85
4 changed files with 94 additions and 12 deletions

View file

@ -58,6 +58,44 @@ export function useScanDetail(scanId: string) {
[scanId],
);
const setLocation = useCallback(
async (coords: { latitude: number; longitude: number }) => {
const capturedAt = new Date().toISOString();
const all = await storage.get<ScanRecord[]>(storage.KEYS.SCAN_HISTORY);
if (!all) return;
const updated = all.map((s) =>
s.id === scanId
? {
...s,
latitude: coords.latitude,
longitude: coords.longitude,
locationCapturedAt: capturedAt,
location: {
latitude: coords.latitude,
longitude: coords.longitude,
},
}
: s,
);
await storage.set(storage.KEYS.SCAN_HISTORY, updated);
setScan((prev) =>
prev
? {
...prev,
latitude: coords.latitude,
longitude: coords.longitude,
locationCapturedAt: capturedAt,
location: {
latitude: coords.latitude,
longitude: coords.longitude,
},
}
: prev,
);
},
[scanId],
);
return {
scan,
loading,
@ -65,6 +103,7 @@ export function useScanDetail(scanId: string) {
toggleFavorite,
deleteScan,
renameScan,
setLocation,
refetch: load,
};
}

View file

@ -304,7 +304,8 @@
"favorited": "Added to favorites",
"unfavorited": "Removed from favorites",
"deleted": "Scan deleted",
"renamed": "Name updated"
"renamed": "Name updated",
"locationAdded": "Location added to the plant"
},
"status": {
"healthy": "Healthy",
@ -330,6 +331,7 @@
"location": "Location",
"noLocation": "No location recorded",
"addLocation": "Add my location",
"locating": "Getting location...",
"share": "Share",
"delete": "Delete",
"shareConfirmTitle": "Share this scan?",

View file

@ -304,7 +304,8 @@
"favorited": "Ajouté aux favoris",
"unfavorited": "Retiré des favoris",
"deleted": "Scan supprimé",
"renamed": "Nom mis à jour"
"renamed": "Nom mis à jour",
"locationAdded": "Position ajoutée à la plante"
},
"status": {
"healthy": "Saine",
@ -330,6 +331,7 @@
"location": "Localisation",
"noLocation": "Aucune localisation enregistrée",
"addLocation": "Ajouter ma position",
"locating": "Localisation en cours...",
"share": "Partager",
"delete": "Supprimer",
"shareConfirmTitle": "Partager ce scan ?",

View file

@ -40,6 +40,7 @@ import { toast } from "sonner-native";
import { Text } from "@/components/ui/text";
import { EditNameBottomSheet } from "@/components/my-plants/EditNameBottomSheet";
import { useScanDetail } from "@/hooks/useScanDetail";
import { useScanLocation } from "@/hooks/useScanLocation";
import { getCepageById } from "@/utils/cepages";
import { hapticSuccess } from "@/services/haptics";
import { colors } from "@/theme/colors";
@ -96,9 +97,18 @@ export default function ScanDetailScreen({ route }: Props) {
const { t, i18n } = useTranslation();
const navigation = useNavigation();
const insets = useSafeAreaInsets();
const { scan, loading, error, toggleFavorite, deleteScan, renameScan } =
useScanDetail(scanId);
const {
scan,
loading,
error,
toggleFavorite,
deleteScan,
renameScan,
setLocation,
} = useScanDetail(scanId);
const [editingName, setEditingName] = useState(false);
const [addingLocation, setAddingLocation] = useState(false);
const { requestAndGetLocation } = useScanLocation();
// Entry animation
const contentY = useSharedValue(30);
@ -185,6 +195,26 @@ export default function ScanDetailScreen({ route }: Props) {
toast.success(t("myPlants.toasts.renamed"));
}
async function handleAddLocation() {
if (addingLocation) return;
setAddingLocation(true);
try {
const coords = await requestAndGetLocation();
if (!coords) {
toast.error(t("location.permissionDenied"));
return;
}
await setLocation({
latitude: coords.latitude,
longitude: coords.longitude,
});
hapticSuccess();
toast.success(t("myPlants.toasts.locationAdded"));
} finally {
setAddingLocation(false);
}
}
async function handleToggleFavorite() {
await toggleFavorite();
hapticSuccess();
@ -426,17 +456,26 @@ export default function ScanDetailScreen({ route }: Props) {
{t("myPlants.detail.noLocation")}
</Text>
<TouchableOpacity
style={styles.addLocationBtn}
onPress={() =>
console.warn(
"[ScanDetail] add location — to be implemented in prompt 3",
)
}
style={[
styles.addLocationBtn,
addingLocation && { opacity: 0.6 },
]}
onPress={handleAddLocation}
disabled={addingLocation}
activeOpacity={0.7}
>
<MapPin size={14} color={colors.primary[700]} />
{addingLocation ? (
<ActivityIndicator
size="small"
color={colors.primary[700]}
/>
) : (
<MapPin size={14} color={colors.primary[700]} />
)}
<Text style={styles.addLocationText}>
{t("myPlants.detail.addLocation")}
{addingLocation
? t("myPlants.detail.locating")
: t("myPlants.detail.addLocation")}
</Text>
</TouchableOpacity>
</View>