This repository has been archived on 2026-03-09. You can view files and clone it, but cannot push or open issues or pull requests.
as-app/plans/PLAN_DAE-claude.md
2026-03-07 07:53:08 +01:00

14 KiB

DAE (Défibrillateur) Feature - Implementation Plan

Common Context Prefix (include at the top of every task prompt)

## Project Context - DAE Feature Integration

You are working on the React Native app "Alerte Secours" at `/home/jo/lab/alerte-secours/apps/as-app/`.
Branch: `feat/dae`

### Architecture Overview

- **Framework:** React Native with Expo v53
- **Navigation:** React Navigation v6 (Stack → Drawer → Tabs)
  - `RootStack.js` (Stack) → `Drawer.js` (Drawer) → `Main/index.js` (Material Top Tabs)
  - Each drawer screen can be a standalone scene or use a Bottom Tab Navigator
- **State Management:** Zustand v5 with custom atomic wrapper (`~/lib/atomic-zustand`)
  - Stores defined in `~/stores/index.js`, each atom in `~/stores/{name}.js`
  - Usage: `const { foo } = useFooState(["foo"])` or `fooActions.doSomething()`
- **Maps:** MapLibre React Native (`@maplibre/maplibre-react-native`)
  - Map components in `~/containers/Map/` (Camera, MapView, FeatureImages, ShapePoints, etc.)
  - Route calculation via OSRM in `~/scenes/AlertCurMap/routing.js`
  - Clustering via supercluster in `useFeatures` hooks
- **Styling:** React Native Paper v5 + custom `createStyles()`/`useStyles()` from `~/theme`
- **Icons:** `@expo/vector-icons` (MaterialCommunityIcons, MaterialIcons, Entypo)
- **Forms:** React Hook Form
- **GraphQL:** Apollo Client v3
- **Module alias:** `~` maps to `src/`

### Key Existing Patterns

**Drawer Screen Registration** (`src/navigation/Drawer.js`):
- Screens are `<Drawer.Screen name="..." component={...} options={{drawerLabel, drawerIcon, ...}} />`
- Hidden screens use `options={{ hidden: true }}`
- Menu item sections are organized by index ranges in `DrawerItemList.js` (indices 0-4 = "Alerter", 5-8 = "Mon compte", 9+ = "Infos pratiques")

**Header Titles** (`src/navigation/RootStack.js`):
- `getHeaderTitle(route)` switch-case maps route names to display titles

**Bottom Tab Navigator Pattern** (reference: `src/scenes/AlertAgg/index.js`):
- `createBottomTabNavigator()` with `Tab.Screen` entries
- `screenOptions={{ headerShown: false, tabBarLabelStyle: { fontSize: 13 }, lazy: true, unmountOnBlur: true }}`
- Tab icons use MaterialCommunityIcons

**Action Buttons Pattern** (reference: `src/scenes/AlertCurOverview/index.js`):
- `<Button mode="contained" icon={() => <MaterialCommunityIcons ... />} style={[styles.actionButton, ...]} onPress={handler}>`

**Alert Map Navigation Pattern** (reference: `src/scenes/AlertCurMap/index.js`):
- Uses route calculation via OSRM, user location tracking, polyline rendering
- Camera management via `useMapInit()`, features via `useFeatures()`
- Control buttons, routing steps drawer, profile selection (car/bike/pedestrian)

**Navigation to nested screens:**
```js
navigation.navigate("Main", {
  screen: "AlertCur",
  params: { screen: "AlertCurTab", params: { screen: "AlertCurMap" } },
});

DAE Data Layer (already implemented)

  • src/data/getNearbyDefibs.js - Main API: getNearbyDefibs({ lat, lon, radiusMeters, limit, disponible24hOnly })
  • src/db/defibsRepo.js - SQLite queries using H3 spatial index
  • src/db/openDb.js - SQLite database initialization
  • src/assets/db/geodae.db - Pre-built SQLite database with defibrillator data

DefibResult type:

{ id, latitude, longitude, nom, adresse, horaires, acces, disponible_24h, distanceMeters }

Files You Should Reference for Patterns

  • Drawer registration: src/navigation/Drawer.js
  • Menu sections: src/navigation/DrawerNav/DrawerItemList.js
  • Header titles: src/navigation/RootStack.js (getHeaderTitle)
  • Bottom tabs: src/scenes/AlertAgg/index.js
  • List view: src/scenes/AlertAggList/index.js
  • Map view: src/scenes/AlertAggMap/index.js
  • Alert navigation map: src/scenes/AlertCurMap/index.js
  • Alert situation view: src/scenes/AlertCurOverview/index.js
  • Alert posting: src/scenes/SendAlertConfirm/useOnSubmit.js
  • Store definition: src/stores/index.js
  • Store atom example: src/stores/alert.js
  • Modal pattern: uses Portal + Modal from react-native-paper

---

## Task 1: Create the DAE Zustand Store

**Goal:** Create a Zustand store atom for DAE state management.

**Create `src/stores/dae.js`:**
- State:
  - `daeList: []` — array of DefibResult objects currently loaded
  - `daeDisplayEnabled: false` — whether DAE markers should show on alert map
  - `daeAlertFilter: null` — when set to `{ userCoords, alertCoords }`, filters DAE within 10km of the axis between user and alert
  - `selectedDae: null` — currently selected DAE item (for DAEItem view)
- Actions:
  - `setDaeList(list)`
  - `setDaeDisplayEnabled(enabled)`
  - `setDaeAlertFilter(filter)`
  - `setSelectedDae(dae)`
  - `reset()`

**Update `src/stores/index.js`:**
- Import the new `dae` atom
- Add it to `createStore()` call
- Export `useDaeState`, `getDaeState`, `subscribeDaeState`, `daeActions`

**Reference:** Follow the exact pattern of `src/stores/alert.js` for atom structure.

---

## Task 2: Create DAEItem Scene (Single DAE Detail View)

**Goal:** Create the DAEItem view that shows details of a single defibrillator with bottom tabs (Infos + Map).

**Create `src/scenes/DAEItem/index.js`:**
- Bottom Tab Navigator with 2 tabs:
  - `DAEItemInfos` (left, default) — Info tab
  - `DAEItemMap` (right) — Map tab with navigation to DAE
- Follow the `AlertAgg/index.js` tab pattern exactly
- Tab icons: `information-outline` for Infos, `map-marker-outline` for Map

**Create `src/scenes/DAEItemInfos/index.js`:**
- Display selected DAE info from `useDaeState(["selectedDae"])`
- Show: nom, adresse, horaires, acces, disponible_24h, distance
- Use a ScrollView with info lines
- Style following the app's pattern (createStyles, useStyles)
- Include a "Itinéraire" button that switches to the Map tab

**Create `src/scenes/DAEItemMap/index.js`:**
- Map with route to the DAE location (the DAE is the destination)
- Mimic `src/scenes/AlertCurMap/index.js` implementation:
  - Same OSRM route calculation
  - Same camera management, polyline rendering
  - Same control buttons, routing steps
  - Same profile selection (car/bike/pedestrian)
  - The destination is `{ latitude: dae.latitude, longitude: dae.longitude }` instead of alert location
- Use `useDaeState(["selectedDae"])` to get the DAE coordinates
- Reuse as much as possible from the existing map containers (`~/containers/Map/*`)

---

## Task 3: Create DAEList Scene (List + Map of Nearby DAEs)

**Goal:** Create the DAEList view with bottom tabs showing a list and map of nearby defibrillators.

**Create `src/scenes/DAEList/index.js`:**
- Bottom Tab Navigator with 2 tabs:
  - `DAEListList` (left, default) — List of DAEs
  - `DAEListMap` (right) — Map of DAEs
- Follow `AlertAgg/index.js` pattern
- Tab icons: `format-list-bulleted` for List, `map-marker-outline` for Map

**Create `src/scenes/DAEListList/index.js`:**
- Load DAEs using `getNearbyDefibs()` with user location from `useLocationState`
- Parameters: `radiusMeters: 10000` (10km), `limit: 100`
- Filter out DAEs not available for current day and hour:
  - Parse the `horaires` field to determine availability
  - Keep DAEs where `disponible_24h === 1` (always available)
  - For others, parse opening hours and check against current day/time
  - If `horaires` is empty or unparseable, keep the DAE (err on the side of showing)
- Sort by `distanceMeters` ascending (nearest first)
- Display as a ScrollView/FlatList with rows showing:
  - DAE name (nom), address (adresse), distance (formatted), availability indicator
- On press → set `daeActions.setSelectedDae(dae)` and navigate to `DAEItem`

**Create `src/scenes/DAEListMap/index.js`:**
- Map showing all nearby DAEs as markers
- Mimic `src/scenes/AlertAggMap/index.js` pattern for marker clustering and display
- Use DAE-specific marker icon (defibrillator icon)
- On marker press → set `daeActions.setSelectedDae(dae)` and navigate to `DAEItem`
- Camera bounds should fit all visible DAE markers

---

## Task 4: Register DAEList and DAEItem in Navigation

**Goal:** Wire up DAEList and DAEItem screens into the app navigation.

**Update `src/navigation/Drawer.js`:**
- Import DAEList and DAEItem scenes
- Add `<Drawer.Screen name="DAEList">` with:
  - `drawerLabel: "Défibrillateurs"`
  - `drawerIcon`: use `MaterialCommunityIcons` with name `"heart-pulse"` (or `"medical-bag"`)
  - `unmountOnBlur: true`
- Add `<Drawer.Screen name="DAEItem">` with:
  - `hidden: true` (not shown in drawer menu, navigated to programmatically)
  - `unmountOnBlur: true`
- Place `DAEList` in the "Infos pratiques" section (after existing items, before About or Sheets)

**Update `src/navigation/DrawerNav/DrawerItemList.js`:**
- Adjust the index constants (`index1`, `index2`) if needed to account for the new drawer screen
- Ensure DAEList appears in the "Infos pratiques" section

**Update `src/navigation/RootStack.js`:**
- Add cases to `getHeaderTitle()`:
  - `case "DAEList": return "Défibrillateurs";`
  - `case "DAEItem": return "Défibrillateur";`

---

## Task 5: Add "Afficher les défibrillateurs" Button in AlertCurOverview

**Goal:** Add a button in the Alert Situation view to enable DAE display and navigate to the alert map.

**Update `src/scenes/AlertCurOverview/index.js`:**
- Import `daeActions` from `~/stores`
- Import `getNearbyDefibs` from `~/data/getNearbyDefibs`
- Import `useLocationState` from `~/stores` (or use `getCurrentLocation`)
- Add a new action button "Afficher les défibrillateurs" in the `containerActions` section:
  - Position it after the "Je viens vous aider" / "Coming help" button area (visible for both sender and receiver, when alert is open)
  - Icon: `MaterialCommunityIcons` name `"heart-pulse"`
  - On press:
    1. Get user coords (from store or `getCurrentLocation()`)
    2. Get alert coords from `alert.location.coordinates`
    3. Call `daeActions.setDaeDisplayEnabled(true)`
    4. Call `daeActions.setDaeAlertFilter({ userCoords: { latitude, longitude }, alertCoords: { latitude: alertLat, longitude: alertLon } })`
    5. Load DAEs: call `getNearbyDefibs()` with a radius covering the 10km corridor along the axis between user and alert positions, store results via `daeActions.setDaeList(results)`
    6. Navigate to AlertCurMap: `navigation.navigate("Main", { screen: "AlertCur", params: { screen: "AlertCurTab", params: { screen: "AlertCurMap" } } })`
- Show this button for all users (sender and receiver) when the alert is open
- Follow the exact same Button pattern as the other action buttons in this file

---

## Task 6: Add DAE Markers to AlertCurMap

**Goal:** Display defibrillator markers on the alert navigation map when enabled.

**Update `src/scenes/AlertCurMap/index.js`:**
- Import `useDaeState` from `~/stores`
- Read `daeDisplayEnabled` and `daeList` from the DAE store
- When `daeDisplayEnabled` is true, render DAE markers on the map:
  - Use `Maplibre.PointAnnotation` or `Maplibre.MarkerView` for each DAE
  - Use a distinct icon/color for DAE markers (different from alert markers) — green with heart-pulse or defibrillator symbol
  - Show DAE name in a callout/tooltip on marker press
  - On marker press → set `daeActions.setSelectedDae(dae)` and navigate to `DAEItem`

**Alternative approach (if using the existing FeatureImages/clustering pattern):**
- Create DAE-specific features array from `daeList`
- Convert each DAE to a GeoJSON Feature with `Point` geometry
- Add a separate ShapeSource + SymbolLayer for DAE features
- Use a defibrillator icon image (add to `FeatureImages` or create a DAE-specific one)

**Important:** DAE markers must coexist with the existing alert marker and route display without interfering.

---

## Task 7: Cardiac Keyword Detection Modal on Alert Posting

**Goal:** When posting an alert, detect cardiac-related keywords in the subject and show a modal suggesting to search for a defibrillator. The modal must work offline and persist even during navigation.

**Create `src/components/DAEModal/index.js`:**
- A modal component using `Portal` + `Modal` from `react-native-paper`
- Props: `visible`, `onDismiss`, `onSearchDAE`
- Content:
  - Title: "Défibrillateur" or relevant header
  - Brief message about defibrillators nearby
  - Two buttons:
    - "Chercher un défibrillateur" (primary, calls `onSearchDAE`)
    - "Non merci" (secondary, calls `onDismiss`)
- The modal must render at a high level in the component tree so it persists across navigations

**Create `src/utils/string/detectCardiacKeywords.js`:**
- Export a function `detectCardiacKeywords(text)` that returns `true` if the text contains cardiac-related keywords
- Keywords to match (case-insensitive, with common typos):
  - "cardiaque", "cardiac", "cardique" (typo)
  - "coeur", "cœur"
  - "malaise", "mailaise" (typo), "mallaise" (typo)
  - "inconscient", "inconsciente"
  - "évanoui", "évanouie", "evanouie", "evanoui", "évanouis" (typo variant)
  - "arrêt", "arret" (in context of "arrêt cardiaque")
  - "défibrillateur", "defibrillateur"
  - "réanimation", "reanimation"
  - "massage cardiaque"
  - "ne respire plus", "respire plus"
- Use a regex approach for fuzzy matching

**Update `src/scenes/SendAlertConfirm/useOnSubmit.js`:**
- The keyword detection needs to happen at submission time
- However, the modal must show *during* the posting flow (even offline)
- Approach:
  - Instead of modifying `useOnSubmit` directly (since the modal must persist across navigation), manage the modal state via the DAE store or a dedicated state
  - Add `showDaeModal: false` to the DAE store, and `daeActions.setShowDaeModal(bool)`
  - In `useOnSubmit.js`, after creating `alertSendAlertInput` but before or right after the mutation call, check `detectCardiacKeywords(subject)`. If true, call `daeActions.setShowDaeModal(true)`
  - The modal keeps showing even as navigation happens (because it's rendered at app root level via Portal)

**Mount the DAEModal at app level:**
- Update `src/app/AppRoot.js` or `src/layout/Layout.js` to include the `DAEModal` component
- The modal reads `showDaeModal` from `useDaeState`
- "Chercher un défibrillateur" → `daeActions.setShowDaeModal(false)` + navigate to `DAEList`
- "Non merci" → `daeActions.setShowDaeModal(false)`

---

## Task Dependency Order

Task 1 (Store) → no dependencies, do first Task 2 (DAEItem) → depends on Task 1 Task 3 (DAEList) → depends on Task 1 Task 4 (Navigation) → depends on Tasks 2, 3 Task 5 (Button in AlertCurOverview) → depends on Tasks 1, 4 Task 6 (DAE Markers on AlertCurMap) → depends on Tasks 1, 2, 4 Task 7 (Cardiac Modal) → depends on Tasks 1, 3, 4


Parallelizable: Tasks 2 and 3 can run in parallel after Task 1.
Task 4 should be done after Tasks 2 and 3.
Tasks 5, 6, 7 can run in parallel after Task 4.