@wasto/cms-components 1.5.11 → 1.5.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wasto/cms-components",
3
- "version": "1.5.11",
3
+ "version": "1.5.14",
4
4
  "description": "Wast'O CMS React Native components",
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -30,10 +30,12 @@
30
30
  "@react-navigation/bottom-tabs": "*",
31
31
  "@sentry/react-native": "*",
32
32
  "@wasto/sdk": "*",
33
+ "expo-constants": "*",
33
34
  "expo-haptics": "*",
34
35
  "expo-image": "*",
35
36
  "expo-image-picker": "*",
36
37
  "expo-linear-gradient": "*",
38
+ "expo-notifications": "*",
37
39
  "expo-router": "*",
38
40
  "lucide-react-native": "*",
39
41
  "maplibre-gl": "*",
@@ -42,21 +44,31 @@
42
44
  "react-native-gesture-handler": "*",
43
45
  "react-native-markdown-display": "*",
44
46
  "react-native-reanimated": "*",
45
- "react-native-safe-area-context": "*"
47
+ "react-native-safe-area-context": "*",
48
+ "@react-navigation/elements": "*"
46
49
  },
47
50
  "peerDependenciesMeta": {
48
51
  "@maplibre/maplibre-react-native": {
49
52
  "optional": true
50
53
  },
54
+ "@react-navigation/elements": {
55
+ "optional": true
56
+ },
51
57
  "@sentry/react-native": {
52
58
  "optional": true
53
59
  },
60
+ "expo-constants": {
61
+ "optional": true
62
+ },
54
63
  "expo-image-picker": {
55
64
  "optional": true
56
65
  },
57
66
  "expo-linear-gradient": {
58
67
  "optional": true
59
68
  },
69
+ "expo-notifications": {
70
+ "optional": true
71
+ },
60
72
  "expo-router": {
61
73
  "optional": true
62
74
  },
@@ -67,17 +79,20 @@
67
79
  "devDependencies": {
68
80
  "@maplibre/maplibre-react-native": "11.3.6",
69
81
  "@react-navigation/bottom-tabs": "^7.4.0",
82
+ "@react-navigation/elements": "^2.9.10",
70
83
  "@sentry/react-native": "~6.22.0",
71
84
  "@types/react": "~19.2.2",
85
+ "expo-constants": "~18.0.9",
72
86
  "expo-haptics": "~15.0.7",
73
87
  "expo-linear-gradient": "~15.0.8",
88
+ "expo-notifications": "~0.32.17",
74
89
  "maplibre-gl": "^4.7.0",
75
90
  "react-native-gesture-handler": "~2.28.0",
76
91
  "react-native-markdown-display": "^7.0.2",
77
92
  "react-native-reanimated": "~4.1.1",
78
93
  "tsup": "^8.5.1",
79
94
  "typescript": "^6.0.2",
80
- "@wasto/sdk": "1.0.4"
95
+ "@wasto/sdk": "1.0.5"
81
96
  },
82
97
  "keywords": [],
83
98
  "author": "",
@@ -8,6 +8,7 @@ import {
8
8
  } from "react-native";
9
9
  import { useTheme } from "../../contexts/ThemeContext";
10
10
  import { useWasto } from "../../contexts/WastoContext";
11
+ import { useOptionalAppData } from "../../contexts/AppDataContext";
11
12
  import { useSafeAreaPadding } from "../../helpers/useSafeAreaPadding";
12
13
  import { Title } from "../title/Title";
13
14
  import { Theme } from "../../types/Theme";
@@ -58,6 +59,26 @@ export type AppHeaderConfig = {
58
59
  elements: AppHeaderElement[];
59
60
  };
60
61
 
62
+ /** Actions standard (notifications + profil) pour navbar sans titre. */
63
+ export const DEFAULT_HEADER_ACTIONS: readonly AppHeaderActionElement[] = [
64
+ {
65
+ id: "notifications",
66
+ type: "action",
67
+ slot: "right",
68
+ icon: "bell",
69
+ action: "navigate",
70
+ href: SYSTEM_ROUTES.notifications,
71
+ },
72
+ {
73
+ id: "profile",
74
+ type: "action",
75
+ slot: "right",
76
+ icon: "person",
77
+ action: "navigate",
78
+ href: SYSTEM_ROUTES.profile,
79
+ },
80
+ ];
81
+
61
82
  export type HeaderProps = {
62
83
  readonly options: { title?: string; headerBackVisible?: boolean };
63
84
  readonly route: { name: string };
@@ -67,7 +88,10 @@ export type HeaderProps = {
67
88
  };
68
89
  /** Flotte au-dessus du contenu (ex. carte) : fond transparent. */
69
90
  readonly overlay?: boolean;
70
- /** Config layout (`header.elements`) — logo / titre / actions. */
91
+ /**
92
+ * Config layout (`header.elements`) — logo / actions.
93
+ * Si fournie (même vide), le fallback titre d’écran ne s’applique pas.
94
+ */
71
95
  readonly headerConfig?: AppHeaderConfig | null;
72
96
  };
73
97
 
@@ -75,21 +99,16 @@ function resolveLogoUri(
75
99
  apiUrl: string,
76
100
  el: AppHeaderLogoElement,
77
101
  ): string | null {
78
- if (el.sourceType === "media") {
102
+ const url = (el.url || "").trim();
103
+ if (url) return url;
104
+ // Fallback média (export parfois laisse mediaId si rewrite manqué)
105
+ if (el.sourceType === "media" || el.mediaId) {
79
106
  return publicMediaUrl(apiUrl, el.mediaId);
80
107
  }
81
- const url = (el.url || "").trim();
82
- return url || null;
108
+ return null;
83
109
  }
84
110
 
85
- function HeaderLogo({
86
- element,
87
- }: {
88
- readonly element: AppHeaderLogoElement;
89
- }): ReactNode {
90
- const { apiUrl } = useWasto();
91
- const uri = resolveLogoUri(apiUrl, element);
92
- if (!uri) return null;
111
+ function HeaderLogoImage({ uri }: { readonly uri: string }): ReactNode {
93
112
  return (
94
113
  <Image
95
114
  source={{ uri }}
@@ -100,6 +119,29 @@ function HeaderLogo({
100
119
  );
101
120
  }
102
121
 
122
+ function HeaderLogo({
123
+ element,
124
+ }: {
125
+ readonly element: AppHeaderLogoElement;
126
+ }): ReactNode {
127
+ const { apiUrl } = useWasto();
128
+ const uri = resolveLogoUri(apiUrl, element);
129
+ if (!uri) return null;
130
+ return <HeaderLogoImage uri={uri} />;
131
+ }
132
+
133
+ function CityLogoFallback(): ReactNode {
134
+ const { apiUrl } = useWasto();
135
+ const appData = useOptionalAppData();
136
+ const logoMediaId =
137
+ typeof appData?.city?.logoMediaId === "string"
138
+ ? appData.city.logoMediaId
139
+ : null;
140
+ const uri = publicMediaUrl(apiUrl, logoMediaId);
141
+ if (!uri) return null;
142
+ return <HeaderLogoImage uri={uri} />;
143
+ }
144
+
103
145
  function HeaderActionButton({
104
146
  onPress,
105
147
  accessibilityLabel,
@@ -143,9 +185,15 @@ export function Header({
143
185
  [options.title, route.name],
144
186
  );
145
187
 
146
- const elements = headerConfig?.elements ?? [];
147
- const hasCustom = elements.length > 0;
188
+ /** Config explicite → navbar (logo/actions), jamais le fallback titre d’écran. */
189
+ const hasExplicitConfig = headerConfig != null;
190
+ const elements = useMemo(() => {
191
+ const raw = headerConfig?.elements ?? [];
192
+ // `type: "title"` est déprécié (navbar uniquement).
193
+ return raw.filter((el) => el.type !== "title");
194
+ }, [headerConfig]);
148
195
  const hasCustomActions = elements.some((el) => el.type === "action");
196
+ const hasLogo = elements.some((el) => el.type === "logo");
149
197
 
150
198
  const bySlot = useMemo(() => {
151
199
  const left: AppHeaderElement[] = [];
@@ -174,11 +222,7 @@ export function Header({
174
222
  return <HeaderLogo key={el.id} element={el} />;
175
223
  }
176
224
  if (el.type === "title") {
177
- const text =
178
- el.useScreenTitle !== false
179
- ? screenTitle
180
- : (el.text || "").trim() || screenTitle;
181
- return <Title key={el.id} text={text} level="1" />;
225
+ return null;
182
226
  }
183
227
  // action
184
228
  const onPress = async () => {
@@ -259,7 +303,7 @@ export function Header({
259
303
  pointerEvents="box-none"
260
304
  >
261
305
  <View style={styles.header} pointerEvents="box-none">
262
- {hasCustom ? (
306
+ {hasExplicitConfig ? (
263
307
  <>
264
308
  <View style={styles.slotLeft} pointerEvents="box-none">
265
309
  {showBack ? (
@@ -276,6 +320,7 @@ export function Header({
276
320
  </HeaderActionButton>
277
321
  ) : null}
278
322
  {bySlot.left.map(renderElement)}
323
+ {!hasLogo ? <CityLogoFallback /> : null}
279
324
  </View>
280
325
  <View
281
326
  style={styles.slotCenter}
@@ -304,6 +349,7 @@ export function Header({
304
349
  />
305
350
  </HeaderActionButton>
306
351
  ) : null}
352
+ <CityLogoFallback />
307
353
  <Title text={screenTitle} level="1" />
308
354
  </View>
309
355
  <View style={styles.actions}>{defaultActions}</View>
@@ -333,9 +379,9 @@ const styles = StyleSheet.create({
333
379
  alignItems: "center",
334
380
  justifyContent: "space-between",
335
381
  paddingHorizontal: SPACE.pageX,
336
- paddingVertical: 12,
382
+ paddingVertical: 6,
337
383
  gap: 10,
338
- minHeight: 56,
384
+ minHeight: 44,
339
385
  },
340
386
  left: {
341
387
  flexDirection: "row",
@@ -27,15 +27,22 @@ import { LocationSheet } from "./LocationSheet";
27
27
  import type { LocationsMapViewProps } from "./LocationsMapView";
28
28
  import { cardStyle, resolveTheme } from "../../helpers/themeStyles";
29
29
  import { getContrastColor } from "../../helpers/getContrastColor";
30
- import { Header } from "../header/Header";
30
+ import { Header, DEFAULT_HEADER_ACTIONS, type AppHeaderConfig } from "../header/Header";
31
+ import { useHeaderConfig } from "../../contexts/HeaderConfigContext";
31
32
  import { SPACE } from "../../helpers/layout";
32
33
 
33
34
  export type LocationsMapProps = {
34
35
  fullScreen?: boolean;
35
36
  showFilters?: boolean;
36
- /** Header flottant au-dessus de la carte (titre + cloche + profil). */
37
+ /** Header flottant au-dessus de la carte (même navbar que le layout, sans titre). */
37
38
  overlayHeader?: boolean;
39
+ /**
40
+ * @deprecated Ignoré — la navbar n’affiche plus de titre d’écran.
41
+ * Conservé pour compatibilité des configs CMS existantes.
42
+ */
38
43
  overlayTitle?: string;
44
+ /** Override optionnel ; sinon config du layout via `HeaderConfigProvider`. */
45
+ headerConfig?: AppHeaderConfig | null;
39
46
  };
40
47
 
41
48
  const CATEGORIES = Object.keys(LOCATION_CATEGORY_META) as LocationCategory[];
@@ -168,17 +175,28 @@ export function LocationsMapShell({
168
175
  fullScreen = false,
169
176
  showFilters = true,
170
177
  overlayHeader = false,
171
- overlayTitle = "Carte",
178
+ headerConfig: headerConfigProp,
172
179
  MapCanvas,
173
180
  }: LocationsMapShellProps): ReactNode {
174
181
  const { theme } = useTheme();
175
182
  const t = resolveTheme(theme);
176
183
  const themedStyles = createThemedStyles(theme);
184
+ const layoutHeaderConfig = useHeaderConfig();
177
185
  const { locations, isLoading, error, center } = useLocations();
178
186
  const [filters, setFilters] = useState<Set<LocationCategory>>(new Set());
179
187
  const [selected, setSelected] = useState<MapLocation | null>(null);
180
188
  const [filtersOpen, setFiltersOpen] = useState(false);
181
189
 
190
+ const overlayHeaderConfig: AppHeaderConfig = useMemo(() => {
191
+ const raw = headerConfigProp ?? layoutHeaderConfig;
192
+ if (raw?.elements?.length) {
193
+ return {
194
+ elements: raw.elements.filter((el) => el.type !== "title"),
195
+ };
196
+ }
197
+ return { elements: DEFAULT_HEADER_ACTIONS.map((a) => ({ ...a })) };
198
+ }, [headerConfigProp, layoutHeaderConfig]);
199
+
182
200
  const toggle = (category: LocationCategory) => {
183
201
  setFilters((prev) => {
184
202
  const next = new Set(prev);
@@ -252,9 +270,10 @@ export function LocationsMapShell({
252
270
  {overlayHeader ? (
253
271
  <Header
254
272
  overlay
255
- options={{ title: overlayTitle, headerBackVisible: false }}
256
- route={{ name: overlayTitle }}
273
+ options={{ headerBackVisible: false }}
274
+ route={{ name: "map" }}
257
275
  navigation={NOOP_NAV}
276
+ headerConfig={overlayHeaderConfig}
258
277
  />
259
278
  ) : null}
260
279
 
@@ -1,5 +1,12 @@
1
- import { PropsWithChildren, useCallback, useState, type ReactNode } from "react";
1
+ import {
2
+ PropsWithChildren,
3
+ useCallback,
4
+ useContext,
5
+ useState,
6
+ type ReactNode,
7
+ } from "react";
2
8
  import { RefreshControl, ScrollView, StyleSheet, View } from "react-native";
9
+ import { HeaderHeightContext } from "@react-navigation/elements";
3
10
  import { useTheme } from "../../contexts/ThemeContext";
4
11
  import { useOptionalAppData } from "../../contexts/AppDataContext";
5
12
  import { useSafeAreaPadding } from "../../helpers/useSafeAreaPadding";
@@ -18,7 +25,8 @@ export type PageProps = PropsWithChildren & {
18
25
  };
19
26
 
20
27
  /**
21
- * Page shell: top safe area + optional vertical scroll.
28
+ * Page shell: optional vertical scroll.
29
+ * Top safe area only when no navigation header (avoids a double inset band).
22
30
  * No global horizontal padding — each block uses ContentInset or is full-bleed
23
31
  * (map, calendar grid). Bottom inset is handled by BottomTab when using tabs.
24
32
  */
@@ -30,7 +38,9 @@ export function Page({
30
38
  const { theme } = useTheme();
31
39
  const t = resolveTheme(theme);
32
40
  const themedStyles = createThemedStyles(theme);
33
- const topInset = useSafeAreaPadding(["top"]);
41
+ const headerHeight = useContext(HeaderHeightContext);
42
+ const hasNavHeader = typeof headerHeight === "number" && headerHeight > 0;
43
+ const topInset = useSafeAreaPadding(hasNavHeader ? [] : ["top"]);
34
44
  const appData = useOptionalAppData();
35
45
  const [refreshing, setRefreshing] = useState(false);
36
46
 
@@ -0,0 +1,77 @@
1
+ import { useEffect, useRef, type ReactNode } from "react";
2
+ import { AppState, Platform } from "react-native";
3
+ import { useLocal } from "../../contexts/LocalContext";
4
+ import { useWasto } from "../../contexts/WastoContext";
5
+ import { useWastoClient } from "../../hooks/useWastoClient";
6
+ import { registerExpoPushToken } from "../../push/registerExpoPushToken";
7
+
8
+ /**
9
+ * Registers the Expo push token once a deviceId exists.
10
+ * Retries when the app comes back to the foreground (permission granted in OS settings).
11
+ * Associates the token with the selected collection place (→ city → city group).
12
+ * Renders nothing — mount inside LocalProvider.
13
+ */
14
+ export function PushTokenRegistrar(): ReactNode {
15
+ const { deviceId, selectedCityId, selectedPlaceId } = useLocal();
16
+ const { previewMode, reportError } = useWasto();
17
+ const client = useWastoClient();
18
+ const lastRegistered = useRef<string | null>(null);
19
+ const inFlight = useRef(false);
20
+
21
+ useEffect(() => {
22
+ if (previewMode || Platform.OS === "web" || !deviceId) return;
23
+
24
+ let cancelled = false;
25
+
26
+ const tryRegister = async () => {
27
+ if (cancelled || inFlight.current) return;
28
+ const registrationKey = `${deviceId}:${selectedPlaceId ?? ""}:${selectedCityId ?? ""}`;
29
+ if (lastRegistered.current === registrationKey) return;
30
+
31
+ inFlight.current = true;
32
+ try {
33
+ const token = await registerExpoPushToken({
34
+ deviceId,
35
+ registerPushToken: (id: string, pushToken: string) =>
36
+ client.public.devices.registerPushToken(id, {
37
+ token: pushToken,
38
+ cityId: selectedCityId,
39
+ wasteCollectionPlaceId: selectedPlaceId,
40
+ }),
41
+ });
42
+ if (!cancelled && token) {
43
+ lastRegistered.current = registrationKey;
44
+ }
45
+ } catch (error) {
46
+ if (!cancelled) {
47
+ reportError(error, { source: "push", deviceId });
48
+ }
49
+ } finally {
50
+ inFlight.current = false;
51
+ }
52
+ };
53
+
54
+ void tryRegister();
55
+
56
+ const sub = AppState.addEventListener("change", (state) => {
57
+ if (state === "active") {
58
+ lastRegistered.current = null;
59
+ void tryRegister();
60
+ }
61
+ });
62
+
63
+ return () => {
64
+ cancelled = true;
65
+ sub.remove();
66
+ };
67
+ }, [
68
+ client.public.devices,
69
+ deviceId,
70
+ previewMode,
71
+ reportError,
72
+ selectedCityId,
73
+ selectedPlaceId,
74
+ ]);
75
+
76
+ return null;
77
+ }
@@ -0,0 +1,29 @@
1
+ import {
2
+ createContext,
3
+ useContext,
4
+ type ReactNode,
5
+ } from "react";
6
+ import type { AppHeaderConfig } from "../components/header/Header";
7
+
8
+ const HeaderConfigContext = createContext<AppHeaderConfig | null>(null);
9
+
10
+ export type HeaderConfigProviderProps = {
11
+ readonly value: AppHeaderConfig | null;
12
+ readonly children: ReactNode;
13
+ };
14
+
15
+ export function HeaderConfigProvider({
16
+ value,
17
+ children,
18
+ }: HeaderConfigProviderProps): ReactNode {
19
+ return (
20
+ <HeaderConfigContext.Provider value={value}>
21
+ {children}
22
+ </HeaderConfigContext.Provider>
23
+ );
24
+ }
25
+
26
+ /** Config header du layout parent (`null` si hors provider / non défini). */
27
+ export function useHeaderConfig(): AppHeaderConfig | null {
28
+ return useContext(HeaderConfigContext);
29
+ }
@@ -93,18 +93,22 @@ export function LocalProvider({
93
93
 
94
94
  let resolvedDeviceId: string | null = null;
95
95
  if (!previewMode) {
96
- const newDeviceId = generateDeviceId();
96
+ const existingDeviceId = await AsyncStorage.getItem(keys.device);
97
+ const nextDeviceId = existingDeviceId || generateDeviceId();
97
98
  await client.public.analytics.track({
98
- id: newDeviceId,
99
+ id: nextDeviceId,
99
100
  cityId: cityId,
101
+ wasteCollectionPlaceId: placeId,
100
102
  platform: Platform.OS,
101
103
  osVersion: null,
102
104
  appVersion: null,
103
105
  deviceModel: null,
104
106
  });
105
- await AsyncStorage.setItem(keys.device, newDeviceId);
106
- setDeviceId(newDeviceId);
107
- resolvedDeviceId = newDeviceId;
107
+ if (!existingDeviceId) {
108
+ await AsyncStorage.setItem(keys.device, nextDeviceId);
109
+ }
110
+ setDeviceId(nextDeviceId);
111
+ resolvedDeviceId = nextDeviceId;
108
112
  } else {
109
113
  // Studio preview: never create analytics devices / persist device id.
110
114
  setDeviceId(null);
@@ -190,6 +194,29 @@ export function LocalProvider({
190
194
  void loadLocalData();
191
195
  }, [keys, previewMode]);
192
196
 
197
+ useEffect(() => {
198
+ if (previewMode || !deviceId || !selectedCityId) return;
199
+ void client.public.analytics
200
+ .track({
201
+ id: deviceId,
202
+ cityId: selectedCityId,
203
+ wasteCollectionPlaceId: selectedPlaceId,
204
+ platform: Platform.OS,
205
+ osVersion: null,
206
+ appVersion: null,
207
+ deviceModel: null,
208
+ })
209
+ .catch(() => {
210
+ // Heartbeat best-effort: the sector is already stored locally.
211
+ });
212
+ }, [
213
+ client.public.analytics,
214
+ deviceId,
215
+ previewMode,
216
+ selectedCityId,
217
+ selectedPlaceId,
218
+ ]);
219
+
193
220
  return (
194
221
  <LocalContext
195
222
  value={{
@@ -15,6 +15,7 @@ import { ChallengesProvider } from "./ChallengesContext";
15
15
  import { AnnouncementsProvider } from "./AnnouncementsContext";
16
16
  import SelectStreetModal from "../components/select-street-modal/SelectStreetModal";
17
17
  import { AnnouncementModal } from "../components/announcement-modal/AnnouncementModal";
18
+ import { PushTokenRegistrar } from "../components/push/PushTokenRegistrar";
18
19
  import { OptionalSafeAreaProvider } from "../components/safe-area/OptionalSafeAreaProvider";
19
20
  import {
20
21
  initWastoSentry,
@@ -104,6 +105,7 @@ export function WastoProvider(props: WastoProviderProps): ReactNode {
104
105
  <WastoContext value={value}>
105
106
  <OptionalSafeAreaProvider>
106
107
  <LocalProvider previewMode={previewMode}>
108
+ <PushTokenRegistrar />
107
109
  <ChallengesProvider>
108
110
  <AnnouncementsProvider apiUrl={apiUrl}>
109
111
  <ThemeProvider theme={props.theme}>
@@ -23,7 +23,7 @@ export type WasteCollectionPlace = {
23
23
  /** Ex: "Lundi", "Mardi", ... (mapping lundi=0 … dimanche=6) */
24
24
  name: string;
25
25
  wasteTypeId: string;
26
- biweekly?: boolean;
26
+ everyWeeks?: number;
27
27
  startDate?: string;
28
28
  }>;
29
29
  /** Périodes d’exception (pas de collecte) */
@@ -90,13 +90,16 @@ export function getCollectionsForDay(
90
90
  });
91
91
  if (hasException) continue;
92
92
 
93
- if (day.biweekly && day.startDate) {
93
+ if (day.startDate) {
94
94
  const start = new Date(day.startDate);
95
+ start.setHours(0, 0, 0, 0);
96
+ const everyWeeks = Math.max(1, Number(day.everyWeeks) || 1);
95
97
  const diffDays = Math.floor(
96
98
  (targetDate.getTime() - start.getTime()) / (1000 * 60 * 60 * 24),
97
99
  );
98
100
  const weeks = Math.floor(diffDays / 7);
99
- if (weeks < 0 || weeks % 2 !== 0) continue;
101
+ if (weeks < 0) continue;
102
+ if (everyWeeks > 1 && weeks % everyWeeks !== 0) continue;
100
103
  }
101
104
 
102
105
  const wt = wasteTypes.find((w) => w.id === day.wasteTypeId);
@@ -31,7 +31,7 @@ export type CollectionScheduleEntry = {
31
31
  dayOfWeek: number; // lundi=0 ... dimanche=6
32
32
  wasteTypeId: string;
33
33
  color: string;
34
- biweekly?: boolean;
34
+ everyWeeks?: number;
35
35
  startDate?: string;
36
36
  };
37
37
 
@@ -255,7 +255,7 @@ function buildSchedule(
255
255
  const rawDays = (selected.collectionDays || []) as Array<{
256
256
  name: string;
257
257
  wasteTypeId: string;
258
- biweekly?: boolean;
258
+ everyWeeks?: number;
259
259
  startDate?: string;
260
260
  }>;
261
261
 
@@ -268,7 +268,7 @@ function buildSchedule(
268
268
  dayOfWeek: dayIndex,
269
269
  wasteTypeId: day.wasteTypeId,
270
270
  color: typeof color === "string" && color.trim().length > 0 ? color : "#7C7C7C",
271
- biweekly: day.biweekly,
271
+ everyWeeks: Math.max(1, Number(day.everyWeeks) || 1),
272
272
  startDate: day.startDate,
273
273
  });
274
274
  }
@@ -293,14 +293,13 @@ function computeWasteTypesForDay(
293
293
 
294
294
  const wasteTypes: WasteTypeInfoForDay[] = [];
295
295
  for (const s of daySchedules) {
296
- if (s.biweekly && s.startDate) {
296
+ const everyWeeks = Math.max(1, Number(s.everyWeeks) || 1);
297
+ if (s.startDate) {
297
298
  const weeksSinceStart = getWeeksSinceStart(s.startDate, date);
298
- if (weeksSinceStart >= 0 && weeksSinceStart % 2 === 0) {
299
- wasteTypes.push({ wasteTypeId: s.wasteTypeId, color: s.color });
300
- }
301
- } else if (!s.biweekly) {
302
- wasteTypes.push({ wasteTypeId: s.wasteTypeId, color: s.color });
299
+ if (weeksSinceStart < 0) continue;
300
+ if (everyWeeks > 1 && weeksSinceStart % everyWeeks !== 0) continue;
303
301
  }
302
+ wasteTypes.push({ wasteTypeId: s.wasteTypeId, color: s.color });
304
303
  }
305
304
 
306
305
  // Filtre des exceptions: si l’exception cible un wasteTypeId précis, on ne garde pas ce type pendant la période.
package/src/index.ts CHANGED
@@ -63,6 +63,7 @@ export {
63
63
  } from "./components/title/Title"
64
64
  export {
65
65
  Header,
66
+ DEFAULT_HEADER_ACTIONS,
66
67
  type HeaderProps,
67
68
  type AppHeaderConfig,
68
69
  type AppHeaderElement,
@@ -98,6 +99,12 @@ export {
98
99
  type WastoContextType,
99
100
  } from "./contexts/WastoContext"
100
101
 
102
+ export {
103
+ HeaderConfigProvider,
104
+ useHeaderConfig,
105
+ type HeaderConfigProviderProps,
106
+ } from "./contexts/HeaderConfigContext"
107
+
101
108
  export {
102
109
  useAppData,
103
110
  useOptionalAppData,
@@ -114,6 +121,11 @@ export {
114
121
  type WastoErrorContext,
115
122
  } from "./sentry/initSentry"
116
123
 
124
+ export {
125
+ registerExpoPushToken,
126
+ type RegisterExpoPushTokenOptions,
127
+ } from "./push/registerExpoPushToken"
128
+
117
129
  export { useWastoClient } from "./hooks/useWastoClient"
118
130
 
119
131
  export type {