@wasto/cms-components 1.5.12 → 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/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +102 -34
- package/dist/index.mjs +108 -36
- package/package.json +8 -3
- package/src/components/header/Header.tsx +35 -13
- package/src/components/page/Page.tsx +13 -3
- package/src/components/push/PushTokenRegistrar.tsx +38 -8
- package/src/contexts/LocalContext.tsx +32 -5
- package/src/helpers/nextCollection.ts +6 -3
- package/src/hooks/useCollectionCalendar.ts +8 -9
package/dist/index.d.mts
CHANGED
|
@@ -185,7 +185,8 @@ type PageProps = PropsWithChildren & {
|
|
|
185
185
|
pullToRefresh?: boolean;
|
|
186
186
|
};
|
|
187
187
|
/**
|
|
188
|
-
* Page shell:
|
|
188
|
+
* Page shell: optional vertical scroll.
|
|
189
|
+
* Top safe area only when no navigation header (avoids a double inset band).
|
|
189
190
|
* No global horizontal padding — each block uses ContentInset or is full-bleed
|
|
190
191
|
* (map, calendar grid). Bottom inset is handled by BottomTab when using tabs.
|
|
191
192
|
*/
|
|
@@ -430,7 +431,7 @@ type WasteCollectionPlace = {
|
|
|
430
431
|
/** Ex: "Lundi", "Mardi", ... (mapping lundi=0 … dimanche=6) */
|
|
431
432
|
name: string;
|
|
432
433
|
wasteTypeId: string;
|
|
433
|
-
|
|
434
|
+
everyWeeks?: number;
|
|
434
435
|
startDate?: string;
|
|
435
436
|
}>;
|
|
436
437
|
/** Périodes d’exception (pas de collecte) */
|
package/dist/index.d.ts
CHANGED
|
@@ -185,7 +185,8 @@ type PageProps = PropsWithChildren & {
|
|
|
185
185
|
pullToRefresh?: boolean;
|
|
186
186
|
};
|
|
187
187
|
/**
|
|
188
|
-
* Page shell:
|
|
188
|
+
* Page shell: optional vertical scroll.
|
|
189
|
+
* Top safe area only when no navigation header (avoids a double inset band).
|
|
189
190
|
* No global horizontal padding — each block uses ContentInset or is full-bleed
|
|
190
191
|
* (map, calendar grid). Bottom inset is handled by BottomTab when using tabs.
|
|
191
192
|
*/
|
|
@@ -430,7 +431,7 @@ type WasteCollectionPlace = {
|
|
|
430
431
|
/** Ex: "Lundi", "Mardi", ... (mapping lundi=0 … dimanche=6) */
|
|
431
432
|
name: string;
|
|
432
433
|
wasteTypeId: string;
|
|
433
|
-
|
|
434
|
+
everyWeeks?: number;
|
|
434
435
|
startDate?: string;
|
|
435
436
|
}>;
|
|
436
437
|
/** Périodes d’exception (pas de collecte) */
|
package/dist/index.js
CHANGED
|
@@ -271,18 +271,22 @@ function LocalProvider({
|
|
|
271
271
|
};
|
|
272
272
|
let resolvedDeviceId = null;
|
|
273
273
|
if (!previewMode) {
|
|
274
|
-
const
|
|
274
|
+
const existingDeviceId = await import_async_storage.default.getItem(keys.device);
|
|
275
|
+
const nextDeviceId = existingDeviceId || generateDeviceId();
|
|
275
276
|
await client.public.analytics.track({
|
|
276
|
-
id:
|
|
277
|
+
id: nextDeviceId,
|
|
277
278
|
cityId,
|
|
279
|
+
wasteCollectionPlaceId: placeId,
|
|
278
280
|
platform: import_react_native.Platform.OS,
|
|
279
281
|
osVersion: null,
|
|
280
282
|
appVersion: null,
|
|
281
283
|
deviceModel: null
|
|
282
284
|
});
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
285
|
+
if (!existingDeviceId) {
|
|
286
|
+
await import_async_storage.default.setItem(keys.device, nextDeviceId);
|
|
287
|
+
}
|
|
288
|
+
setDeviceId(nextDeviceId);
|
|
289
|
+
resolvedDeviceId = nextDeviceId;
|
|
286
290
|
} else {
|
|
287
291
|
setDeviceId(null);
|
|
288
292
|
}
|
|
@@ -356,6 +360,25 @@ function LocalProvider({
|
|
|
356
360
|
};
|
|
357
361
|
void loadLocalData();
|
|
358
362
|
}, [keys, previewMode]);
|
|
363
|
+
(0, import_react3.useEffect)(() => {
|
|
364
|
+
if (previewMode || !deviceId || !selectedCityId) return;
|
|
365
|
+
void client.public.analytics.track({
|
|
366
|
+
id: deviceId,
|
|
367
|
+
cityId: selectedCityId,
|
|
368
|
+
wasteCollectionPlaceId: selectedPlaceId,
|
|
369
|
+
platform: import_react_native.Platform.OS,
|
|
370
|
+
osVersion: null,
|
|
371
|
+
appVersion: null,
|
|
372
|
+
deviceModel: null
|
|
373
|
+
}).catch(() => {
|
|
374
|
+
});
|
|
375
|
+
}, [
|
|
376
|
+
client.public.analytics,
|
|
377
|
+
deviceId,
|
|
378
|
+
previewMode,
|
|
379
|
+
selectedCityId,
|
|
380
|
+
selectedPlaceId
|
|
381
|
+
]);
|
|
359
382
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
360
383
|
LocalContext,
|
|
361
384
|
{
|
|
@@ -746,13 +769,16 @@ function getCollectionsForDay(wasteTypes, wasteCollectionPlaces, dateOffset, sel
|
|
|
746
769
|
return targetDate.getTime() >= from.getTime() && targetDate.getTime() < to.getTime();
|
|
747
770
|
});
|
|
748
771
|
if (hasException) continue;
|
|
749
|
-
if (day.
|
|
772
|
+
if (day.startDate) {
|
|
750
773
|
const start = new Date(day.startDate);
|
|
774
|
+
start.setHours(0, 0, 0, 0);
|
|
775
|
+
const everyWeeks = Math.max(1, Number(day.everyWeeks) || 1);
|
|
751
776
|
const diffDays = Math.floor(
|
|
752
777
|
(targetDate.getTime() - start.getTime()) / (1e3 * 60 * 60 * 24)
|
|
753
778
|
);
|
|
754
779
|
const weeks = Math.floor(diffDays / 7);
|
|
755
|
-
if (weeks < 0
|
|
780
|
+
if (weeks < 0) continue;
|
|
781
|
+
if (everyWeeks > 1 && weeks % everyWeeks !== 0) continue;
|
|
756
782
|
}
|
|
757
783
|
const wt = wasteTypes.find((w) => w.id === day.wasteTypeId);
|
|
758
784
|
if (!wt) continue;
|
|
@@ -2210,33 +2236,58 @@ async function registerExpoPushToken(options) {
|
|
|
2210
2236
|
|
|
2211
2237
|
// src/components/push/PushTokenRegistrar.tsx
|
|
2212
2238
|
function PushTokenRegistrar() {
|
|
2213
|
-
const { deviceId } = useLocal();
|
|
2239
|
+
const { deviceId, selectedCityId, selectedPlaceId } = useLocal();
|
|
2214
2240
|
const { previewMode, reportError: reportError2 } = useWasto();
|
|
2215
2241
|
const client = useWastoClient();
|
|
2216
2242
|
const lastRegistered = (0, import_react8.useRef)(null);
|
|
2243
|
+
const inFlight = (0, import_react8.useRef)(false);
|
|
2217
2244
|
(0, import_react8.useEffect)(() => {
|
|
2218
2245
|
if (previewMode || import_react_native8.Platform.OS === "web" || !deviceId) return;
|
|
2219
|
-
if (lastRegistered.current === deviceId) return;
|
|
2220
2246
|
let cancelled = false;
|
|
2221
|
-
|
|
2247
|
+
const tryRegister = async () => {
|
|
2248
|
+
if (cancelled || inFlight.current) return;
|
|
2249
|
+
const registrationKey = `${deviceId}:${selectedPlaceId ?? ""}:${selectedCityId ?? ""}`;
|
|
2250
|
+
if (lastRegistered.current === registrationKey) return;
|
|
2251
|
+
inFlight.current = true;
|
|
2222
2252
|
try {
|
|
2223
2253
|
const token = await registerExpoPushToken({
|
|
2224
2254
|
deviceId,
|
|
2225
|
-
registerPushToken: (id, pushToken) => client.public.devices.registerPushToken(id, {
|
|
2255
|
+
registerPushToken: (id, pushToken) => client.public.devices.registerPushToken(id, {
|
|
2256
|
+
token: pushToken,
|
|
2257
|
+
cityId: selectedCityId,
|
|
2258
|
+
wasteCollectionPlaceId: selectedPlaceId
|
|
2259
|
+
})
|
|
2226
2260
|
});
|
|
2227
2261
|
if (!cancelled && token) {
|
|
2228
|
-
lastRegistered.current =
|
|
2262
|
+
lastRegistered.current = registrationKey;
|
|
2229
2263
|
}
|
|
2230
2264
|
} catch (error) {
|
|
2231
2265
|
if (!cancelled) {
|
|
2232
2266
|
reportError2(error, { source: "push", deviceId });
|
|
2233
2267
|
}
|
|
2268
|
+
} finally {
|
|
2269
|
+
inFlight.current = false;
|
|
2234
2270
|
}
|
|
2235
|
-
}
|
|
2271
|
+
};
|
|
2272
|
+
void tryRegister();
|
|
2273
|
+
const sub = import_react_native8.AppState.addEventListener("change", (state) => {
|
|
2274
|
+
if (state === "active") {
|
|
2275
|
+
lastRegistered.current = null;
|
|
2276
|
+
void tryRegister();
|
|
2277
|
+
}
|
|
2278
|
+
});
|
|
2236
2279
|
return () => {
|
|
2237
2280
|
cancelled = true;
|
|
2281
|
+
sub.remove();
|
|
2238
2282
|
};
|
|
2239
|
-
}, [
|
|
2283
|
+
}, [
|
|
2284
|
+
client.public.devices,
|
|
2285
|
+
deviceId,
|
|
2286
|
+
previewMode,
|
|
2287
|
+
reportError2,
|
|
2288
|
+
selectedCityId,
|
|
2289
|
+
selectedPlaceId
|
|
2290
|
+
]);
|
|
2240
2291
|
return null;
|
|
2241
2292
|
}
|
|
2242
2293
|
|
|
@@ -6369,7 +6420,7 @@ function buildSchedule(wasteTypes, places, selectedPlaceId) {
|
|
|
6369
6420
|
dayOfWeek: dayIndex,
|
|
6370
6421
|
wasteTypeId: day.wasteTypeId,
|
|
6371
6422
|
color: typeof color === "string" && color.trim().length > 0 ? color : "#7C7C7C",
|
|
6372
|
-
|
|
6423
|
+
everyWeeks: Math.max(1, Number(day.everyWeeks) || 1),
|
|
6373
6424
|
startDate: day.startDate
|
|
6374
6425
|
});
|
|
6375
6426
|
}
|
|
@@ -6383,14 +6434,13 @@ function computeWasteTypesForDay(day, year, month, schedule, exceptions) {
|
|
|
6383
6434
|
const daySchedules = schedule.filter((s) => s.dayOfWeek === mondayBasedDay);
|
|
6384
6435
|
const wasteTypes = [];
|
|
6385
6436
|
for (const s of daySchedules) {
|
|
6386
|
-
|
|
6437
|
+
const everyWeeks = Math.max(1, Number(s.everyWeeks) || 1);
|
|
6438
|
+
if (s.startDate) {
|
|
6387
6439
|
const weeksSinceStart = getWeeksSinceStart(s.startDate, date);
|
|
6388
|
-
if (weeksSinceStart
|
|
6389
|
-
|
|
6390
|
-
}
|
|
6391
|
-
} else if (!s.biweekly) {
|
|
6392
|
-
wasteTypes.push({ wasteTypeId: s.wasteTypeId, color: s.color });
|
|
6440
|
+
if (weeksSinceStart < 0) continue;
|
|
6441
|
+
if (everyWeeks > 1 && weeksSinceStart % everyWeeks !== 0) continue;
|
|
6393
6442
|
}
|
|
6443
|
+
wasteTypes.push({ wasteTypeId: s.wasteTypeId, color: s.color });
|
|
6394
6444
|
}
|
|
6395
6445
|
const filtered = [];
|
|
6396
6446
|
for (const wt of wasteTypes) {
|
|
@@ -8186,18 +8236,14 @@ var DEFAULT_HEADER_ACTIONS = [
|
|
|
8186
8236
|
}
|
|
8187
8237
|
];
|
|
8188
8238
|
function resolveLogoUri(apiUrl, el) {
|
|
8189
|
-
|
|
8239
|
+
const url = (el.url || "").trim();
|
|
8240
|
+
if (url) return url;
|
|
8241
|
+
if (el.sourceType === "media" || el.mediaId) {
|
|
8190
8242
|
return publicMediaUrl(apiUrl, el.mediaId);
|
|
8191
8243
|
}
|
|
8192
|
-
|
|
8193
|
-
return url || null;
|
|
8244
|
+
return null;
|
|
8194
8245
|
}
|
|
8195
|
-
function
|
|
8196
|
-
element
|
|
8197
|
-
}) {
|
|
8198
|
-
const { apiUrl } = useWasto();
|
|
8199
|
-
const uri = resolveLogoUri(apiUrl, element);
|
|
8200
|
-
if (!uri) return null;
|
|
8246
|
+
function HeaderLogoImage({ uri }) {
|
|
8201
8247
|
return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
|
|
8202
8248
|
import_react_native34.Image,
|
|
8203
8249
|
{
|
|
@@ -8208,6 +8254,22 @@ function HeaderLogo({
|
|
|
8208
8254
|
}
|
|
8209
8255
|
);
|
|
8210
8256
|
}
|
|
8257
|
+
function HeaderLogo({
|
|
8258
|
+
element
|
|
8259
|
+
}) {
|
|
8260
|
+
const { apiUrl } = useWasto();
|
|
8261
|
+
const uri = resolveLogoUri(apiUrl, element);
|
|
8262
|
+
if (!uri) return null;
|
|
8263
|
+
return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(HeaderLogoImage, { uri });
|
|
8264
|
+
}
|
|
8265
|
+
function CityLogoFallback() {
|
|
8266
|
+
const { apiUrl } = useWasto();
|
|
8267
|
+
const appData = useOptionalAppData();
|
|
8268
|
+
const logoMediaId = typeof appData?.city?.logoMediaId === "string" ? appData.city.logoMediaId : null;
|
|
8269
|
+
const uri = publicMediaUrl(apiUrl, logoMediaId);
|
|
8270
|
+
if (!uri) return null;
|
|
8271
|
+
return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(HeaderLogoImage, { uri });
|
|
8272
|
+
}
|
|
8211
8273
|
function HeaderActionButton({
|
|
8212
8274
|
onPress,
|
|
8213
8275
|
accessibilityLabel,
|
|
@@ -8248,6 +8310,7 @@ function Header({
|
|
|
8248
8310
|
return raw.filter((el) => el.type !== "title");
|
|
8249
8311
|
}, [headerConfig]);
|
|
8250
8312
|
const hasCustomActions = elements.some((el) => el.type === "action");
|
|
8313
|
+
const hasLogo = elements.some((el) => el.type === "logo");
|
|
8251
8314
|
const bySlot = (0, import_react35.useMemo)(() => {
|
|
8252
8315
|
const left = [];
|
|
8253
8316
|
const center = [];
|
|
@@ -8369,7 +8432,8 @@ function Header({
|
|
|
8369
8432
|
)
|
|
8370
8433
|
}
|
|
8371
8434
|
) : null,
|
|
8372
|
-
bySlot.left.map(renderElement)
|
|
8435
|
+
bySlot.left.map(renderElement),
|
|
8436
|
+
!hasLogo ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(CityLogoFallback, {}) : null
|
|
8373
8437
|
] }),
|
|
8374
8438
|
/* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
|
|
8375
8439
|
import_react_native34.View,
|
|
@@ -8401,6 +8465,7 @@ function Header({
|
|
|
8401
8465
|
)
|
|
8402
8466
|
}
|
|
8403
8467
|
) : null,
|
|
8468
|
+
/* @__PURE__ */ (0, import_jsx_runtime41.jsx)(CityLogoFallback, {}),
|
|
8404
8469
|
/* @__PURE__ */ (0, import_jsx_runtime41.jsx)(Title, { text: screenTitle, level: "1" })
|
|
8405
8470
|
] }),
|
|
8406
8471
|
/* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_react_native34.View, { style: styles29.actions, children: defaultActions })
|
|
@@ -8425,9 +8490,9 @@ var styles29 = import_react_native34.StyleSheet.create({
|
|
|
8425
8490
|
alignItems: "center",
|
|
8426
8491
|
justifyContent: "space-between",
|
|
8427
8492
|
paddingHorizontal: SPACE.pageX,
|
|
8428
|
-
paddingVertical:
|
|
8493
|
+
paddingVertical: 6,
|
|
8429
8494
|
gap: 10,
|
|
8430
|
-
minHeight:
|
|
8495
|
+
minHeight: 44
|
|
8431
8496
|
},
|
|
8432
8497
|
left: {
|
|
8433
8498
|
flexDirection: "row",
|
|
@@ -8952,6 +9017,7 @@ function LocationsMap(props) {
|
|
|
8952
9017
|
// src/components/page/Page.tsx
|
|
8953
9018
|
var import_react38 = require("react");
|
|
8954
9019
|
var import_react_native36 = require("react-native");
|
|
9020
|
+
var import_elements = require("@react-navigation/elements");
|
|
8955
9021
|
var import_jsx_runtime45 = require("react/jsx-runtime");
|
|
8956
9022
|
function Page({
|
|
8957
9023
|
children,
|
|
@@ -8961,7 +9027,9 @@ function Page({
|
|
|
8961
9027
|
const { theme } = useTheme();
|
|
8962
9028
|
const t = resolveTheme(theme);
|
|
8963
9029
|
const themedStyles = createThemedStyles16(theme);
|
|
8964
|
-
const
|
|
9030
|
+
const headerHeight = (0, import_react38.useContext)(import_elements.HeaderHeightContext);
|
|
9031
|
+
const hasNavHeader = typeof headerHeight === "number" && headerHeight > 0;
|
|
9032
|
+
const topInset = useSafeAreaPadding(hasNavHeader ? [] : ["top"]);
|
|
8965
9033
|
const appData = useOptionalAppData();
|
|
8966
9034
|
const [refreshing, setRefreshing] = (0, import_react38.useState)(false);
|
|
8967
9035
|
const enableRefresh = pullToRefresh ?? scroll;
|
package/dist/index.mjs
CHANGED
|
@@ -212,18 +212,22 @@ function LocalProvider({
|
|
|
212
212
|
};
|
|
213
213
|
let resolvedDeviceId = null;
|
|
214
214
|
if (!previewMode) {
|
|
215
|
-
const
|
|
215
|
+
const existingDeviceId = await AsyncStorage.getItem(keys.device);
|
|
216
|
+
const nextDeviceId = existingDeviceId || generateDeviceId();
|
|
216
217
|
await client.public.analytics.track({
|
|
217
|
-
id:
|
|
218
|
+
id: nextDeviceId,
|
|
218
219
|
cityId,
|
|
220
|
+
wasteCollectionPlaceId: placeId,
|
|
219
221
|
platform: Platform.OS,
|
|
220
222
|
osVersion: null,
|
|
221
223
|
appVersion: null,
|
|
222
224
|
deviceModel: null
|
|
223
225
|
});
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
226
|
+
if (!existingDeviceId) {
|
|
227
|
+
await AsyncStorage.setItem(keys.device, nextDeviceId);
|
|
228
|
+
}
|
|
229
|
+
setDeviceId(nextDeviceId);
|
|
230
|
+
resolvedDeviceId = nextDeviceId;
|
|
227
231
|
} else {
|
|
228
232
|
setDeviceId(null);
|
|
229
233
|
}
|
|
@@ -297,6 +301,25 @@ function LocalProvider({
|
|
|
297
301
|
};
|
|
298
302
|
void loadLocalData();
|
|
299
303
|
}, [keys, previewMode]);
|
|
304
|
+
useEffect(() => {
|
|
305
|
+
if (previewMode || !deviceId || !selectedCityId) return;
|
|
306
|
+
void client.public.analytics.track({
|
|
307
|
+
id: deviceId,
|
|
308
|
+
cityId: selectedCityId,
|
|
309
|
+
wasteCollectionPlaceId: selectedPlaceId,
|
|
310
|
+
platform: Platform.OS,
|
|
311
|
+
osVersion: null,
|
|
312
|
+
appVersion: null,
|
|
313
|
+
deviceModel: null
|
|
314
|
+
}).catch(() => {
|
|
315
|
+
});
|
|
316
|
+
}, [
|
|
317
|
+
client.public.analytics,
|
|
318
|
+
deviceId,
|
|
319
|
+
previewMode,
|
|
320
|
+
selectedCityId,
|
|
321
|
+
selectedPlaceId
|
|
322
|
+
]);
|
|
300
323
|
return /* @__PURE__ */ jsx2(
|
|
301
324
|
LocalContext,
|
|
302
325
|
{
|
|
@@ -705,13 +728,16 @@ function getCollectionsForDay(wasteTypes, wasteCollectionPlaces, dateOffset, sel
|
|
|
705
728
|
return targetDate.getTime() >= from.getTime() && targetDate.getTime() < to.getTime();
|
|
706
729
|
});
|
|
707
730
|
if (hasException) continue;
|
|
708
|
-
if (day.
|
|
731
|
+
if (day.startDate) {
|
|
709
732
|
const start = new Date(day.startDate);
|
|
733
|
+
start.setHours(0, 0, 0, 0);
|
|
734
|
+
const everyWeeks = Math.max(1, Number(day.everyWeeks) || 1);
|
|
710
735
|
const diffDays = Math.floor(
|
|
711
736
|
(targetDate.getTime() - start.getTime()) / (1e3 * 60 * 60 * 24)
|
|
712
737
|
);
|
|
713
738
|
const weeks = Math.floor(diffDays / 7);
|
|
714
|
-
if (weeks < 0
|
|
739
|
+
if (weeks < 0) continue;
|
|
740
|
+
if (everyWeeks > 1 && weeks % everyWeeks !== 0) continue;
|
|
715
741
|
}
|
|
716
742
|
const wt = wasteTypes.find((w) => w.id === day.wasteTypeId);
|
|
717
743
|
if (!wt) continue;
|
|
@@ -2108,7 +2134,7 @@ var styles4 = StyleSheet4.create({
|
|
|
2108
2134
|
|
|
2109
2135
|
// src/components/push/PushTokenRegistrar.tsx
|
|
2110
2136
|
import { useEffect as useEffect6, useRef as useRef3 } from "react";
|
|
2111
|
-
import { Platform as Platform5 } from "react-native";
|
|
2137
|
+
import { AppState, Platform as Platform5 } from "react-native";
|
|
2112
2138
|
|
|
2113
2139
|
// src/push/registerExpoPushToken.ts
|
|
2114
2140
|
import { Platform as Platform4 } from "react-native";
|
|
@@ -2200,33 +2226,58 @@ async function registerExpoPushToken(options) {
|
|
|
2200
2226
|
|
|
2201
2227
|
// src/components/push/PushTokenRegistrar.tsx
|
|
2202
2228
|
function PushTokenRegistrar() {
|
|
2203
|
-
const { deviceId } = useLocal();
|
|
2229
|
+
const { deviceId, selectedCityId, selectedPlaceId } = useLocal();
|
|
2204
2230
|
const { previewMode, reportError: reportError2 } = useWasto();
|
|
2205
2231
|
const client = useWastoClient();
|
|
2206
2232
|
const lastRegistered = useRef3(null);
|
|
2233
|
+
const inFlight = useRef3(false);
|
|
2207
2234
|
useEffect6(() => {
|
|
2208
2235
|
if (previewMode || Platform5.OS === "web" || !deviceId) return;
|
|
2209
|
-
if (lastRegistered.current === deviceId) return;
|
|
2210
2236
|
let cancelled = false;
|
|
2211
|
-
|
|
2237
|
+
const tryRegister = async () => {
|
|
2238
|
+
if (cancelled || inFlight.current) return;
|
|
2239
|
+
const registrationKey = `${deviceId}:${selectedPlaceId ?? ""}:${selectedCityId ?? ""}`;
|
|
2240
|
+
if (lastRegistered.current === registrationKey) return;
|
|
2241
|
+
inFlight.current = true;
|
|
2212
2242
|
try {
|
|
2213
2243
|
const token = await registerExpoPushToken({
|
|
2214
2244
|
deviceId,
|
|
2215
|
-
registerPushToken: (id, pushToken) => client.public.devices.registerPushToken(id, {
|
|
2245
|
+
registerPushToken: (id, pushToken) => client.public.devices.registerPushToken(id, {
|
|
2246
|
+
token: pushToken,
|
|
2247
|
+
cityId: selectedCityId,
|
|
2248
|
+
wasteCollectionPlaceId: selectedPlaceId
|
|
2249
|
+
})
|
|
2216
2250
|
});
|
|
2217
2251
|
if (!cancelled && token) {
|
|
2218
|
-
lastRegistered.current =
|
|
2252
|
+
lastRegistered.current = registrationKey;
|
|
2219
2253
|
}
|
|
2220
2254
|
} catch (error) {
|
|
2221
2255
|
if (!cancelled) {
|
|
2222
2256
|
reportError2(error, { source: "push", deviceId });
|
|
2223
2257
|
}
|
|
2258
|
+
} finally {
|
|
2259
|
+
inFlight.current = false;
|
|
2224
2260
|
}
|
|
2225
|
-
}
|
|
2261
|
+
};
|
|
2262
|
+
void tryRegister();
|
|
2263
|
+
const sub = AppState.addEventListener("change", (state) => {
|
|
2264
|
+
if (state === "active") {
|
|
2265
|
+
lastRegistered.current = null;
|
|
2266
|
+
void tryRegister();
|
|
2267
|
+
}
|
|
2268
|
+
});
|
|
2226
2269
|
return () => {
|
|
2227
2270
|
cancelled = true;
|
|
2271
|
+
sub.remove();
|
|
2228
2272
|
};
|
|
2229
|
-
}, [
|
|
2273
|
+
}, [
|
|
2274
|
+
client.public.devices,
|
|
2275
|
+
deviceId,
|
|
2276
|
+
previewMode,
|
|
2277
|
+
reportError2,
|
|
2278
|
+
selectedCityId,
|
|
2279
|
+
selectedPlaceId
|
|
2280
|
+
]);
|
|
2230
2281
|
return null;
|
|
2231
2282
|
}
|
|
2232
2283
|
|
|
@@ -6387,7 +6438,7 @@ function buildSchedule(wasteTypes, places, selectedPlaceId) {
|
|
|
6387
6438
|
dayOfWeek: dayIndex,
|
|
6388
6439
|
wasteTypeId: day.wasteTypeId,
|
|
6389
6440
|
color: typeof color === "string" && color.trim().length > 0 ? color : "#7C7C7C",
|
|
6390
|
-
|
|
6441
|
+
everyWeeks: Math.max(1, Number(day.everyWeeks) || 1),
|
|
6391
6442
|
startDate: day.startDate
|
|
6392
6443
|
});
|
|
6393
6444
|
}
|
|
@@ -6401,14 +6452,13 @@ function computeWasteTypesForDay(day, year, month, schedule, exceptions) {
|
|
|
6401
6452
|
const daySchedules = schedule.filter((s) => s.dayOfWeek === mondayBasedDay);
|
|
6402
6453
|
const wasteTypes = [];
|
|
6403
6454
|
for (const s of daySchedules) {
|
|
6404
|
-
|
|
6455
|
+
const everyWeeks = Math.max(1, Number(s.everyWeeks) || 1);
|
|
6456
|
+
if (s.startDate) {
|
|
6405
6457
|
const weeksSinceStart = getWeeksSinceStart(s.startDate, date);
|
|
6406
|
-
if (weeksSinceStart
|
|
6407
|
-
|
|
6408
|
-
}
|
|
6409
|
-
} else if (!s.biweekly) {
|
|
6410
|
-
wasteTypes.push({ wasteTypeId: s.wasteTypeId, color: s.color });
|
|
6458
|
+
if (weeksSinceStart < 0) continue;
|
|
6459
|
+
if (everyWeeks > 1 && weeksSinceStart % everyWeeks !== 0) continue;
|
|
6411
6460
|
}
|
|
6461
|
+
wasteTypes.push({ wasteTypeId: s.wasteTypeId, color: s.color });
|
|
6412
6462
|
}
|
|
6413
6463
|
const filtered = [];
|
|
6414
6464
|
for (const wt of wasteTypes) {
|
|
@@ -8264,18 +8314,14 @@ var DEFAULT_HEADER_ACTIONS = [
|
|
|
8264
8314
|
}
|
|
8265
8315
|
];
|
|
8266
8316
|
function resolveLogoUri(apiUrl, el) {
|
|
8267
|
-
|
|
8317
|
+
const url = (el.url || "").trim();
|
|
8318
|
+
if (url) return url;
|
|
8319
|
+
if (el.sourceType === "media" || el.mediaId) {
|
|
8268
8320
|
return publicMediaUrl(apiUrl, el.mediaId);
|
|
8269
8321
|
}
|
|
8270
|
-
|
|
8271
|
-
return url || null;
|
|
8322
|
+
return null;
|
|
8272
8323
|
}
|
|
8273
|
-
function
|
|
8274
|
-
element
|
|
8275
|
-
}) {
|
|
8276
|
-
const { apiUrl } = useWasto();
|
|
8277
|
-
const uri = resolveLogoUri(apiUrl, element);
|
|
8278
|
-
if (!uri) return null;
|
|
8324
|
+
function HeaderLogoImage({ uri }) {
|
|
8279
8325
|
return /* @__PURE__ */ jsx41(
|
|
8280
8326
|
Image11,
|
|
8281
8327
|
{
|
|
@@ -8286,6 +8332,22 @@ function HeaderLogo({
|
|
|
8286
8332
|
}
|
|
8287
8333
|
);
|
|
8288
8334
|
}
|
|
8335
|
+
function HeaderLogo({
|
|
8336
|
+
element
|
|
8337
|
+
}) {
|
|
8338
|
+
const { apiUrl } = useWasto();
|
|
8339
|
+
const uri = resolveLogoUri(apiUrl, element);
|
|
8340
|
+
if (!uri) return null;
|
|
8341
|
+
return /* @__PURE__ */ jsx41(HeaderLogoImage, { uri });
|
|
8342
|
+
}
|
|
8343
|
+
function CityLogoFallback() {
|
|
8344
|
+
const { apiUrl } = useWasto();
|
|
8345
|
+
const appData = useOptionalAppData();
|
|
8346
|
+
const logoMediaId = typeof appData?.city?.logoMediaId === "string" ? appData.city.logoMediaId : null;
|
|
8347
|
+
const uri = publicMediaUrl(apiUrl, logoMediaId);
|
|
8348
|
+
if (!uri) return null;
|
|
8349
|
+
return /* @__PURE__ */ jsx41(HeaderLogoImage, { uri });
|
|
8350
|
+
}
|
|
8289
8351
|
function HeaderActionButton({
|
|
8290
8352
|
onPress,
|
|
8291
8353
|
accessibilityLabel,
|
|
@@ -8326,6 +8388,7 @@ function Header({
|
|
|
8326
8388
|
return raw.filter((el) => el.type !== "title");
|
|
8327
8389
|
}, [headerConfig]);
|
|
8328
8390
|
const hasCustomActions = elements.some((el) => el.type === "action");
|
|
8391
|
+
const hasLogo = elements.some((el) => el.type === "logo");
|
|
8329
8392
|
const bySlot = useMemo20(() => {
|
|
8330
8393
|
const left = [];
|
|
8331
8394
|
const center = [];
|
|
@@ -8447,7 +8510,8 @@ function Header({
|
|
|
8447
8510
|
)
|
|
8448
8511
|
}
|
|
8449
8512
|
) : null,
|
|
8450
|
-
bySlot.left.map(renderElement)
|
|
8513
|
+
bySlot.left.map(renderElement),
|
|
8514
|
+
!hasLogo ? /* @__PURE__ */ jsx41(CityLogoFallback, {}) : null
|
|
8451
8515
|
] }),
|
|
8452
8516
|
/* @__PURE__ */ jsx41(
|
|
8453
8517
|
View29,
|
|
@@ -8479,6 +8543,7 @@ function Header({
|
|
|
8479
8543
|
)
|
|
8480
8544
|
}
|
|
8481
8545
|
) : null,
|
|
8546
|
+
/* @__PURE__ */ jsx41(CityLogoFallback, {}),
|
|
8482
8547
|
/* @__PURE__ */ jsx41(Title, { text: screenTitle, level: "1" })
|
|
8483
8548
|
] }),
|
|
8484
8549
|
/* @__PURE__ */ jsx41(View29, { style: styles29.actions, children: defaultActions })
|
|
@@ -8503,9 +8568,9 @@ var styles29 = StyleSheet30.create({
|
|
|
8503
8568
|
alignItems: "center",
|
|
8504
8569
|
justifyContent: "space-between",
|
|
8505
8570
|
paddingHorizontal: SPACE.pageX,
|
|
8506
|
-
paddingVertical:
|
|
8571
|
+
paddingVertical: 6,
|
|
8507
8572
|
gap: 10,
|
|
8508
|
-
minHeight:
|
|
8573
|
+
minHeight: 44
|
|
8509
8574
|
},
|
|
8510
8575
|
left: {
|
|
8511
8576
|
flexDirection: "row",
|
|
@@ -9031,8 +9096,13 @@ function LocationsMap(props) {
|
|
|
9031
9096
|
}
|
|
9032
9097
|
|
|
9033
9098
|
// src/components/page/Page.tsx
|
|
9034
|
-
import {
|
|
9099
|
+
import {
|
|
9100
|
+
useCallback as useCallback13,
|
|
9101
|
+
useContext as useContext10,
|
|
9102
|
+
useState as useState26
|
|
9103
|
+
} from "react";
|
|
9035
9104
|
import { RefreshControl, ScrollView as ScrollView11, StyleSheet as StyleSheet32, View as View31 } from "react-native";
|
|
9105
|
+
import { HeaderHeightContext } from "@react-navigation/elements";
|
|
9036
9106
|
import { jsx as jsx45 } from "react/jsx-runtime";
|
|
9037
9107
|
function Page({
|
|
9038
9108
|
children,
|
|
@@ -9042,7 +9112,9 @@ function Page({
|
|
|
9042
9112
|
const { theme } = useTheme();
|
|
9043
9113
|
const t = resolveTheme(theme);
|
|
9044
9114
|
const themedStyles = createThemedStyles16(theme);
|
|
9045
|
-
const
|
|
9115
|
+
const headerHeight = useContext10(HeaderHeightContext);
|
|
9116
|
+
const hasNavHeader = typeof headerHeight === "number" && headerHeight > 0;
|
|
9117
|
+
const topInset = useSafeAreaPadding(hasNavHeader ? [] : ["top"]);
|
|
9046
9118
|
const appData = useOptionalAppData();
|
|
9047
9119
|
const [refreshing, setRefreshing] = useState26(false);
|
|
9048
9120
|
const enableRefresh = pullToRefresh ?? scroll;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wasto/cms-components",
|
|
3
|
-
"version": "1.5.
|
|
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",
|
|
@@ -44,12 +44,16 @@
|
|
|
44
44
|
"react-native-gesture-handler": "*",
|
|
45
45
|
"react-native-markdown-display": "*",
|
|
46
46
|
"react-native-reanimated": "*",
|
|
47
|
-
"react-native-safe-area-context": "*"
|
|
47
|
+
"react-native-safe-area-context": "*",
|
|
48
|
+
"@react-navigation/elements": "*"
|
|
48
49
|
},
|
|
49
50
|
"peerDependenciesMeta": {
|
|
50
51
|
"@maplibre/maplibre-react-native": {
|
|
51
52
|
"optional": true
|
|
52
53
|
},
|
|
54
|
+
"@react-navigation/elements": {
|
|
55
|
+
"optional": true
|
|
56
|
+
},
|
|
53
57
|
"@sentry/react-native": {
|
|
54
58
|
"optional": true
|
|
55
59
|
},
|
|
@@ -75,6 +79,7 @@
|
|
|
75
79
|
"devDependencies": {
|
|
76
80
|
"@maplibre/maplibre-react-native": "11.3.6",
|
|
77
81
|
"@react-navigation/bottom-tabs": "^7.4.0",
|
|
82
|
+
"@react-navigation/elements": "^2.9.10",
|
|
78
83
|
"@sentry/react-native": "~6.22.0",
|
|
79
84
|
"@types/react": "~19.2.2",
|
|
80
85
|
"expo-constants": "~18.0.9",
|
|
@@ -87,7 +92,7 @@
|
|
|
87
92
|
"react-native-reanimated": "~4.1.1",
|
|
88
93
|
"tsup": "^8.5.1",
|
|
89
94
|
"typescript": "^6.0.2",
|
|
90
|
-
"@wasto/sdk": "1.0.
|
|
95
|
+
"@wasto/sdk": "1.0.5"
|
|
91
96
|
},
|
|
92
97
|
"keywords": [],
|
|
93
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";
|
|
@@ -98,21 +99,16 @@ function resolveLogoUri(
|
|
|
98
99
|
apiUrl: string,
|
|
99
100
|
el: AppHeaderLogoElement,
|
|
100
101
|
): string | null {
|
|
101
|
-
|
|
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) {
|
|
102
106
|
return publicMediaUrl(apiUrl, el.mediaId);
|
|
103
107
|
}
|
|
104
|
-
|
|
105
|
-
return url || null;
|
|
108
|
+
return null;
|
|
106
109
|
}
|
|
107
110
|
|
|
108
|
-
function
|
|
109
|
-
element,
|
|
110
|
-
}: {
|
|
111
|
-
readonly element: AppHeaderLogoElement;
|
|
112
|
-
}): ReactNode {
|
|
113
|
-
const { apiUrl } = useWasto();
|
|
114
|
-
const uri = resolveLogoUri(apiUrl, element);
|
|
115
|
-
if (!uri) return null;
|
|
111
|
+
function HeaderLogoImage({ uri }: { readonly uri: string }): ReactNode {
|
|
116
112
|
return (
|
|
117
113
|
<Image
|
|
118
114
|
source={{ uri }}
|
|
@@ -123,6 +119,29 @@ function HeaderLogo({
|
|
|
123
119
|
);
|
|
124
120
|
}
|
|
125
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
|
+
|
|
126
145
|
function HeaderActionButton({
|
|
127
146
|
onPress,
|
|
128
147
|
accessibilityLabel,
|
|
@@ -174,6 +193,7 @@ export function Header({
|
|
|
174
193
|
return raw.filter((el) => el.type !== "title");
|
|
175
194
|
}, [headerConfig]);
|
|
176
195
|
const hasCustomActions = elements.some((el) => el.type === "action");
|
|
196
|
+
const hasLogo = elements.some((el) => el.type === "logo");
|
|
177
197
|
|
|
178
198
|
const bySlot = useMemo(() => {
|
|
179
199
|
const left: AppHeaderElement[] = [];
|
|
@@ -300,6 +320,7 @@ export function Header({
|
|
|
300
320
|
</HeaderActionButton>
|
|
301
321
|
) : null}
|
|
302
322
|
{bySlot.left.map(renderElement)}
|
|
323
|
+
{!hasLogo ? <CityLogoFallback /> : null}
|
|
303
324
|
</View>
|
|
304
325
|
<View
|
|
305
326
|
style={styles.slotCenter}
|
|
@@ -328,6 +349,7 @@ export function Header({
|
|
|
328
349
|
/>
|
|
329
350
|
</HeaderActionButton>
|
|
330
351
|
) : null}
|
|
352
|
+
<CityLogoFallback />
|
|
331
353
|
<Title text={screenTitle} level="1" />
|
|
332
354
|
</View>
|
|
333
355
|
<View style={styles.actions}>{defaultActions}</View>
|
|
@@ -357,9 +379,9 @@ const styles = StyleSheet.create({
|
|
|
357
379
|
alignItems: "center",
|
|
358
380
|
justifyContent: "space-between",
|
|
359
381
|
paddingHorizontal: SPACE.pageX,
|
|
360
|
-
paddingVertical:
|
|
382
|
+
paddingVertical: 6,
|
|
361
383
|
gap: 10,
|
|
362
|
-
minHeight:
|
|
384
|
+
minHeight: 44,
|
|
363
385
|
},
|
|
364
386
|
left: {
|
|
365
387
|
flexDirection: "row",
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import {
|
|
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:
|
|
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
|
|
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
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef, type ReactNode } from "react";
|
|
2
|
-
import { Platform } from "react-native";
|
|
2
|
+
import { AppState, Platform } from "react-native";
|
|
3
3
|
import { useLocal } from "../../contexts/LocalContext";
|
|
4
4
|
import { useWasto } from "../../contexts/WastoContext";
|
|
5
5
|
import { useWastoClient } from "../../hooks/useWastoClient";
|
|
@@ -7,41 +7,71 @@ import { registerExpoPushToken } from "../../push/registerExpoPushToken";
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
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).
|
|
10
12
|
* Renders nothing — mount inside LocalProvider.
|
|
11
13
|
*/
|
|
12
14
|
export function PushTokenRegistrar(): ReactNode {
|
|
13
|
-
const { deviceId } = useLocal();
|
|
15
|
+
const { deviceId, selectedCityId, selectedPlaceId } = useLocal();
|
|
14
16
|
const { previewMode, reportError } = useWasto();
|
|
15
17
|
const client = useWastoClient();
|
|
16
18
|
const lastRegistered = useRef<string | null>(null);
|
|
19
|
+
const inFlight = useRef(false);
|
|
17
20
|
|
|
18
21
|
useEffect(() => {
|
|
19
22
|
if (previewMode || Platform.OS === "web" || !deviceId) return;
|
|
20
|
-
if (lastRegistered.current === deviceId) return;
|
|
21
23
|
|
|
22
24
|
let cancelled = false;
|
|
23
25
|
|
|
24
|
-
|
|
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;
|
|
25
32
|
try {
|
|
26
33
|
const token = await registerExpoPushToken({
|
|
27
34
|
deviceId,
|
|
28
35
|
registerPushToken: (id: string, pushToken: string) =>
|
|
29
|
-
client.public.devices.registerPushToken(id, {
|
|
36
|
+
client.public.devices.registerPushToken(id, {
|
|
37
|
+
token: pushToken,
|
|
38
|
+
cityId: selectedCityId,
|
|
39
|
+
wasteCollectionPlaceId: selectedPlaceId,
|
|
40
|
+
}),
|
|
30
41
|
});
|
|
31
42
|
if (!cancelled && token) {
|
|
32
|
-
lastRegistered.current =
|
|
43
|
+
lastRegistered.current = registrationKey;
|
|
33
44
|
}
|
|
34
45
|
} catch (error) {
|
|
35
46
|
if (!cancelled) {
|
|
36
47
|
reportError(error, { source: "push", deviceId });
|
|
37
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();
|
|
38
60
|
}
|
|
39
|
-
})
|
|
61
|
+
});
|
|
40
62
|
|
|
41
63
|
return () => {
|
|
42
64
|
cancelled = true;
|
|
65
|
+
sub.remove();
|
|
43
66
|
};
|
|
44
|
-
}, [
|
|
67
|
+
}, [
|
|
68
|
+
client.public.devices,
|
|
69
|
+
deviceId,
|
|
70
|
+
previewMode,
|
|
71
|
+
reportError,
|
|
72
|
+
selectedCityId,
|
|
73
|
+
selectedPlaceId,
|
|
74
|
+
]);
|
|
45
75
|
|
|
46
76
|
return null;
|
|
47
77
|
}
|
|
@@ -93,18 +93,22 @@ export function LocalProvider({
|
|
|
93
93
|
|
|
94
94
|
let resolvedDeviceId: string | null = null;
|
|
95
95
|
if (!previewMode) {
|
|
96
|
-
const
|
|
96
|
+
const existingDeviceId = await AsyncStorage.getItem(keys.device);
|
|
97
|
+
const nextDeviceId = existingDeviceId || generateDeviceId();
|
|
97
98
|
await client.public.analytics.track({
|
|
98
|
-
id:
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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={{
|
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
299
|
-
|
|
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.
|