@burdenoff/microfe-movethewheels 2026.1101.1 → 2026.1101.2

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.
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Turn-by-turn navigation hand-off for the driver day view (BOFF-3991).
3
+ *
4
+ * The driver app does not embed its own routing engine: it hands the stop
5
+ * (or the remaining stops, in sequence) to the device's maps app through the
6
+ * universal Google Maps URL scheme, which opens the native Google Maps / Apple
7
+ * Maps-compatible handler on Android and iOS and a browser tab on desktop.
8
+ */
9
+ import type { Address } from '../types';
10
+ import { formatAddress } from './formatters';
11
+
12
+ const MAPS_DIRECTIONS_BASE = 'https://www.google.com/maps/dir/?api=1';
13
+
14
+ /**
15
+ * Google's universal URL accepts at most 9 intermediate waypoints (plus the
16
+ * destination). Longer days are handed off in chunks: the driver restarts
17
+ * navigation from the next remaining stop once these are done.
18
+ */
19
+ export const MAX_NAVIGATION_WAYPOINTS = 9;
20
+
21
+ /** The best destination string for an address: coordinates when present, else the formatted address. */
22
+ export function navigationDestination(address: Partial<Address> | null | undefined): string | null {
23
+ if (!address) return null;
24
+ if (
25
+ typeof address.latitude === 'number' &&
26
+ typeof address.longitude === 'number' &&
27
+ Number.isFinite(address.latitude) &&
28
+ Number.isFinite(address.longitude)
29
+ ) {
30
+ return `${address.latitude},${address.longitude}`;
31
+ }
32
+ const formatted = formatAddress(address).trim();
33
+ return formatted.length > 0 ? formatted : null;
34
+ }
35
+
36
+ /** Directions from the device's current position to one stop. */
37
+ export function stopNavigationUrl(address: Partial<Address> | null | undefined): string | null {
38
+ const destination = navigationDestination(address);
39
+ if (!destination) return null;
40
+ const params = new URLSearchParams({ destination, travelmode: 'driving' });
41
+ return `${MAPS_DIRECTIONS_BASE}&${params.toString()}`;
42
+ }
43
+
44
+ /**
45
+ * Directions through the remaining stops in sequence: the last stop in the
46
+ * chunk is the destination, the ones before it are waypoints. Stops without a
47
+ * usable address are skipped. Returns null when no stop is navigable.
48
+ */
49
+ export function routeNavigationUrl(
50
+ addresses: Array<Partial<Address> | null | undefined>
51
+ ): string | null {
52
+ const destinations = addresses
53
+ .map((address) => navigationDestination(address))
54
+ .filter((value): value is string => value !== null)
55
+ .slice(0, MAX_NAVIGATION_WAYPOINTS + 1);
56
+ if (destinations.length === 0) return null;
57
+ const destination = destinations[destinations.length - 1];
58
+ const waypoints = destinations.slice(0, -1);
59
+ const params = new URLSearchParams({ destination, travelmode: 'driving' });
60
+ if (waypoints.length > 0) params.set('waypoints', waypoints.join('|'));
61
+ return `${MAPS_DIRECTIONS_BASE}&${params.toString()}`;
62
+ }
63
+
64
+ /** A `tel:` link for a phone number, keeping only dialable characters. */
65
+ export function phoneHref(phone: string | null | undefined): string | null {
66
+ if (!phone) return null;
67
+ const dialable = phone.replace(/[^\d+]/g, '');
68
+ return dialable.length >= 3 ? `tel:${dialable}` : null;
69
+ }
70
+
71
+ /** The device-local calendar date as YYYY-MM-DD (what `myDeliveryDay` expects). */
72
+ export function localDateString(date: Date): string {
73
+ const year = date.getFullYear();
74
+ const month = String(date.getMonth() + 1).padStart(2, '0');
75
+ const day = String(date.getDate()).padStart(2, '0');
76
+ return `${year}-${month}-${day}`;
77
+ }
78
+
79
+ /** Shift a YYYY-MM-DD date by whole days, staying in local calendar terms. */
80
+ export function shiftLocalDate(date: string, days: number): string {
81
+ const [year, month, day] = date.split('-').map(Number);
82
+ return localDateString(new Date(year, month - 1, day + days));
83
+ }
84
+
85
+ /** The `Date#getTimezoneOffset()` value for local midnight of a YYYY-MM-DD date (DST-correct). */
86
+ export function timezoneOffsetFor(date: string): number {
87
+ const [year, month, day] = date.split('-').map(Number);
88
+ return new Date(year, month - 1, day).getTimezoneOffset();
89
+ }
90
+
91
+ /**
92
+ * The device's IANA time zone (e.g. `Asia/Kolkata`), so the server can build
93
+ * the day window from real local midnights, including on DST transition
94
+ * days. Null when the runtime cannot report one; the fixed offset is then used.
95
+ */
96
+ export function deviceTimeZone(): string | null {
97
+ try {
98
+ const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
99
+ return typeof zone === 'string' && zone.length > 0 ? zone : null;
100
+ } catch {
101
+ return null;
102
+ }
103
+ }