@wasto/sdk 1.0.3 → 1.0.4

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.
Files changed (56) hide show
  1. package/dist/index.d.mts +17 -1
  2. package/dist/index.d.ts +17 -1
  3. package/dist/index.js +7 -0
  4. package/dist/index.mjs +7 -0
  5. package/package.json +1 -1
  6. package/src/client.ts +701 -692
  7. package/src/dashboard-apps.ts +350 -350
  8. package/src/dto/cms-app-settings.ts +91 -91
  9. package/src/enums.ts +298 -298
  10. package/src/free-tier.ts +56 -56
  11. package/src/index.ts +35 -35
  12. package/src/opening-hours/constants.ts +48 -48
  13. package/src/opening-hours/convert.ts +276 -276
  14. package/src/opening-hours/format.ts +99 -99
  15. package/src/opening-hours/index.ts +9 -9
  16. package/src/opening-hours/is-open-now.ts +38 -38
  17. package/src/opening-hours/resolve.ts +71 -71
  18. package/src/opening-hours/time.ts +74 -74
  19. package/src/opening-hours/timezone.ts +34 -34
  20. package/src/opening-hours/types.ts +53 -53
  21. package/src/opening-hours/validate.ts +182 -182
  22. package/src/roi/calculations.ts +732 -732
  23. package/src/roi/index.ts +1 -1
  24. package/src/types/app-preload.types.ts +10 -0
  25. package/src/types/asset-bookings.types.ts +19 -19
  26. package/src/types/asset-interventions.types.ts +24 -24
  27. package/src/types/assets.types.ts +68 -68
  28. package/src/types/auth.types.ts +37 -37
  29. package/src/types/blog-posts.types.ts +19 -19
  30. package/src/types/calendar-pdf.types.ts +24 -24
  31. package/src/types/calendo-events.types.ts +64 -64
  32. package/src/types/challenges.types.ts +30 -30
  33. package/src/types/chatbot.types.ts +82 -82
  34. package/src/types/cities.types.ts +27 -27
  35. package/src/types/city-groups.types.ts +46 -46
  36. package/src/types/cms.types.ts +87 -87
  37. package/src/types/field-reports.types.ts +66 -66
  38. package/src/types/free-tier.types.ts +54 -54
  39. package/src/types/index.ts +33 -32
  40. package/src/types/iot.types.ts +31 -31
  41. package/src/types/maintenance.types.ts +44 -44
  42. package/src/types/mapso.types.ts +146 -146
  43. package/src/types/notifications.types.ts +68 -68
  44. package/src/types/onboarding.types.ts +117 -117
  45. package/src/types/operational-zones.types.ts +70 -70
  46. package/src/types/pagination.types.ts +33 -33
  47. package/src/types/providers.types.ts +29 -29
  48. package/src/types/quiz-questions.types.ts +59 -59
  49. package/src/types/recycling-points.types.ts +47 -47
  50. package/src/types/roi.types.ts +60 -60
  51. package/src/types/surveys.types.ts +103 -103
  52. package/src/types/tickets.types.ts +65 -65
  53. package/src/types/users.types.ts +30 -30
  54. package/src/types/waste-collection-places-query.types.ts +4 -4
  55. package/src/types/waste-collection-places.types.ts +33 -33
  56. package/src/types/waste-types.types.ts +21 -21
@@ -1,99 +1,99 @@
1
- import type { OpeningHours, OpeningHoursRule, TimeSlot, Weekday, WeeklyHoursRow } from './types';
2
- import { WEEKDAYS } from './types';
3
- import { WEEKDAY_LABELS_FR, WEEKDAY_LABELS_FR_SHORT } from './constants';
4
- import { convertUtcToLocal } from './convert';
5
- import { formatTimeDisplay, parseTimeMinutes } from './time';
6
-
7
- function formatSlots(slots: TimeSlot[]): string {
8
- return slots
9
- .map((s) => `${formatTimeDisplay(s.open)}–${formatTimeDisplay(s.close)}`)
10
- .join(', ');
11
- }
12
-
13
- function compressDayRanges(days: Weekday[]): string[] {
14
- if (days.length === 0) return [];
15
- const order = new Map(WEEKDAYS.map((d, i) => [d, i]));
16
- const sorted = [...days].sort((a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0));
17
- const ranges: string[] = [];
18
- let start = sorted[0];
19
- let prev = sorted[0];
20
- let prevIdx = order.get(prev) ?? 0;
21
-
22
- const flush = (from: Weekday, to: Weekday) => {
23
- if (from === to) {
24
- ranges.push(WEEKDAY_LABELS_FR_SHORT[from]);
25
- } else {
26
- ranges.push(`${WEEKDAY_LABELS_FR_SHORT[from]}–${WEEKDAY_LABELS_FR_SHORT[to]}`);
27
- }
28
- };
29
-
30
- for (let i = 1; i < sorted.length; i++) {
31
- const day = sorted[i];
32
- const idx = order.get(day) ?? 0;
33
- if (idx === prevIdx + 1) {
34
- prev = day;
35
- prevIdx = idx;
36
- } else {
37
- flush(start, prev);
38
- start = day;
39
- prev = day;
40
- prevIdx = idx;
41
- }
42
- }
43
- flush(start, prev);
44
- return ranges;
45
- }
46
-
47
- function formatRulesSummary(rules: OpeningHoursRule[]): string {
48
- const parts: string[] = [];
49
- for (const rule of rules) {
50
- if (!rule.days.length || !rule.slots.length) continue;
51
- const dayPart = compressDayRanges(rule.days).join(', ');
52
- const slotPart = formatSlots(rule.slots);
53
- parts.push(`${dayPart} ${slotPart}`);
54
- }
55
- return parts.join(' · ');
56
- }
57
-
58
- /** Formate un résumé court à partir d'horaires (UTC ou locaux selon le contenu passé). */
59
- export function formatOpeningHoursSummary(
60
- hours: OpeningHours | { rules: OpeningHoursRule[] },
61
- timeZone?: string,
62
- ): string {
63
- const rules =
64
- 'storage' in hours && hours.storage === 'utc' && timeZone
65
- ? convertUtcToLocal(hours as OpeningHours, timeZone).rules
66
- : hours.rules;
67
- return formatRulesSummary(rules);
68
- }
69
-
70
- export function formatOpeningHoursWeekly(
71
- hours: OpeningHours,
72
- timeZone?: string,
73
- ): WeeklyHoursRow[] {
74
- const rules =
75
- timeZone != null ? convertUtcToLocal(hours, timeZone).rules : hours.rules;
76
-
77
- const byDay = new Map<Weekday, TimeSlot[]>();
78
- for (const rule of rules) {
79
- for (const day of rule.days) {
80
- const existing = byDay.get(day) ?? [];
81
- byDay.set(day, [...existing, ...rule.slots]);
82
- }
83
- }
84
-
85
- return WEEKDAYS.map((day) => {
86
- const slots = byDay.get(day) ?? [];
87
- return {
88
- day,
89
- label: WEEKDAY_LABELS_FR[day],
90
- closed: slots.length === 0,
91
- slots,
92
- };
93
- });
94
- }
95
-
96
- export function formatWeeklyRow(row: WeeklyHoursRow): string {
97
- if (row.closed) return 'Fermé';
98
- return formatSlots(row.slots);
99
- }
1
+ import type { OpeningHours, OpeningHoursRule, TimeSlot, Weekday, WeeklyHoursRow } from './types';
2
+ import { WEEKDAYS } from './types';
3
+ import { WEEKDAY_LABELS_FR, WEEKDAY_LABELS_FR_SHORT } from './constants';
4
+ import { convertUtcToLocal } from './convert';
5
+ import { formatTimeDisplay, parseTimeMinutes } from './time';
6
+
7
+ function formatSlots(slots: TimeSlot[]): string {
8
+ return slots
9
+ .map((s) => `${formatTimeDisplay(s.open)}–${formatTimeDisplay(s.close)}`)
10
+ .join(', ');
11
+ }
12
+
13
+ function compressDayRanges(days: Weekday[]): string[] {
14
+ if (days.length === 0) return [];
15
+ const order = new Map(WEEKDAYS.map((d, i) => [d, i]));
16
+ const sorted = [...days].sort((a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0));
17
+ const ranges: string[] = [];
18
+ let start = sorted[0];
19
+ let prev = sorted[0];
20
+ let prevIdx = order.get(prev) ?? 0;
21
+
22
+ const flush = (from: Weekday, to: Weekday) => {
23
+ if (from === to) {
24
+ ranges.push(WEEKDAY_LABELS_FR_SHORT[from]);
25
+ } else {
26
+ ranges.push(`${WEEKDAY_LABELS_FR_SHORT[from]}–${WEEKDAY_LABELS_FR_SHORT[to]}`);
27
+ }
28
+ };
29
+
30
+ for (let i = 1; i < sorted.length; i++) {
31
+ const day = sorted[i];
32
+ const idx = order.get(day) ?? 0;
33
+ if (idx === prevIdx + 1) {
34
+ prev = day;
35
+ prevIdx = idx;
36
+ } else {
37
+ flush(start, prev);
38
+ start = day;
39
+ prev = day;
40
+ prevIdx = idx;
41
+ }
42
+ }
43
+ flush(start, prev);
44
+ return ranges;
45
+ }
46
+
47
+ function formatRulesSummary(rules: OpeningHoursRule[]): string {
48
+ const parts: string[] = [];
49
+ for (const rule of rules) {
50
+ if (!rule.days.length || !rule.slots.length) continue;
51
+ const dayPart = compressDayRanges(rule.days).join(', ');
52
+ const slotPart = formatSlots(rule.slots);
53
+ parts.push(`${dayPart} ${slotPart}`);
54
+ }
55
+ return parts.join(' · ');
56
+ }
57
+
58
+ /** Formate un résumé court à partir d'horaires (UTC ou locaux selon le contenu passé). */
59
+ export function formatOpeningHoursSummary(
60
+ hours: OpeningHours | { rules: OpeningHoursRule[] },
61
+ timeZone?: string,
62
+ ): string {
63
+ const rules =
64
+ 'storage' in hours && hours.storage === 'utc' && timeZone
65
+ ? convertUtcToLocal(hours as OpeningHours, timeZone).rules
66
+ : hours.rules;
67
+ return formatRulesSummary(rules);
68
+ }
69
+
70
+ export function formatOpeningHoursWeekly(
71
+ hours: OpeningHours,
72
+ timeZone?: string,
73
+ ): WeeklyHoursRow[] {
74
+ const rules =
75
+ timeZone != null ? convertUtcToLocal(hours, timeZone).rules : hours.rules;
76
+
77
+ const byDay = new Map<Weekday, TimeSlot[]>();
78
+ for (const rule of rules) {
79
+ for (const day of rule.days) {
80
+ const existing = byDay.get(day) ?? [];
81
+ byDay.set(day, [...existing, ...rule.slots]);
82
+ }
83
+ }
84
+
85
+ return WEEKDAYS.map((day) => {
86
+ const slots = byDay.get(day) ?? [];
87
+ return {
88
+ day,
89
+ label: WEEKDAY_LABELS_FR[day],
90
+ closed: slots.length === 0,
91
+ slots,
92
+ };
93
+ });
94
+ }
95
+
96
+ export function formatWeeklyRow(row: WeeklyHoursRow): string {
97
+ if (row.closed) return 'Fermé';
98
+ return formatSlots(row.slots);
99
+ }
@@ -1,9 +1,9 @@
1
- export * from './types';
2
- export * from './constants';
3
- export * from './time';
4
- export * from './timezone';
5
- export * from './validate';
6
- export * from './convert';
7
- export * from './format';
8
- export * from './resolve';
9
- export * from './is-open-now';
1
+ export * from './types';
2
+ export * from './constants';
3
+ export * from './time';
4
+ export * from './timezone';
5
+ export * from './validate';
6
+ export * from './convert';
7
+ export * from './format';
8
+ export * from './resolve';
9
+ export * from './is-open-now';
@@ -1,38 +1,38 @@
1
- import type { OpeningHours } from './types';
2
- import { parseTimeMinutes, weekdayFromUtcDate } from './time';
3
- import { utcDateIso } from './timezone';
4
-
5
- export function isOpenNow(
6
- hours: OpeningHours,
7
- at: Date = new Date(),
8
- ): boolean {
9
- const dateIso = utcDateIso(at);
10
- const exceptions = hours.exceptions ?? [];
11
- for (const exc of exceptions) {
12
- if (exc.date !== dateIso) continue;
13
- if (exc.closed) return false;
14
- return isWithinSlots(exc.slots, at);
15
- }
16
-
17
- const weekday = weekdayFromUtcDate(at);
18
- const rules = hours.rules.filter((r) => r.days.includes(weekday));
19
- for (const rule of rules) {
20
- if (isWithinSlots(rule.slots, at)) return true;
21
- }
22
- return false;
23
- }
24
-
25
- function isWithinSlots(slots: { open: string; close: string }[], at: Date): boolean {
26
- const nowMin = at.getUTCHours() * 60 + at.getUTCMinutes();
27
- for (const slot of slots) {
28
- const open = parseTimeMinutes(slot.open);
29
- const close = parseTimeMinutes(slot.close);
30
- if (open == null || close == null) continue;
31
- if (nowMin >= open && nowMin < close) return true;
32
- }
33
- return false;
34
- }
35
-
36
- export function openNowLabel(hours: OpeningHours, at?: Date): 'Ouvert' | 'Fermé' {
37
- return isOpenNow(hours, at) ? 'Ouvert' : 'Fermé';
38
- }
1
+ import type { OpeningHours } from './types';
2
+ import { parseTimeMinutes, weekdayFromUtcDate } from './time';
3
+ import { utcDateIso } from './timezone';
4
+
5
+ export function isOpenNow(
6
+ hours: OpeningHours,
7
+ at: Date = new Date(),
8
+ ): boolean {
9
+ const dateIso = utcDateIso(at);
10
+ const exceptions = hours.exceptions ?? [];
11
+ for (const exc of exceptions) {
12
+ if (exc.date !== dateIso) continue;
13
+ if (exc.closed) return false;
14
+ return isWithinSlots(exc.slots, at);
15
+ }
16
+
17
+ const weekday = weekdayFromUtcDate(at);
18
+ const rules = hours.rules.filter((r) => r.days.includes(weekday));
19
+ for (const rule of rules) {
20
+ if (isWithinSlots(rule.slots, at)) return true;
21
+ }
22
+ return false;
23
+ }
24
+
25
+ function isWithinSlots(slots: { open: string; close: string }[], at: Date): boolean {
26
+ const nowMin = at.getUTCHours() * 60 + at.getUTCMinutes();
27
+ for (const slot of slots) {
28
+ const open = parseTimeMinutes(slot.open);
29
+ const close = parseTimeMinutes(slot.close);
30
+ if (open == null || close == null) continue;
31
+ if (nowMin >= open && nowMin < close) return true;
32
+ }
33
+ return false;
34
+ }
35
+
36
+ export function openNowLabel(hours: OpeningHours, at?: Date): 'Ouvert' | 'Fermé' {
37
+ return isOpenNow(hours, at) ? 'Ouvert' : 'Fermé';
38
+ }
@@ -1,71 +1,71 @@
1
- import type { HoursInputMode, OpeningHours } from './types';
2
- import { convertUtcToLocal } from './convert';
3
- import { formatOpeningHoursSummary } from './format';
4
- import { getBrowserTimeZone } from './timezone';
5
-
6
- export type RecyclingPointHoursSource = {
7
- hours?: string | null;
8
- details?: Record<string, unknown> | null;
9
- };
10
-
11
- export function resolveHoursMode(
12
- details?: Record<string, unknown> | null,
13
- ): HoursInputMode {
14
- if (!details) return 'text';
15
- const mode = details.hoursMode;
16
- if (mode === 'structured' || mode === 'text') return mode;
17
- if (details.openingHours != null) return 'structured';
18
- return 'text';
19
- }
20
-
21
- export function resolveOpeningHoursUtc(
22
- point: RecyclingPointHoursSource,
23
- ): OpeningHours | null {
24
- const details = point.details;
25
- if (!details || resolveHoursMode(details) !== 'structured') return null;
26
- const raw = details.openingHours;
27
- if (!raw || typeof raw !== 'object') return null;
28
- return raw as OpeningHours;
29
- }
30
-
31
- export function resolveOpeningHoursLocal(
32
- point: RecyclingPointHoursSource,
33
- timeZone?: string,
34
- ): { rules: OpeningHours['rules']; exceptions?: OpeningHours['exceptions'] } | null {
35
- const utc = resolveOpeningHoursUtc(point);
36
- if (!utc) return null;
37
- const tz = timeZone ?? getBrowserTimeZone();
38
- return convertUtcToLocal(utc, tz);
39
- }
40
-
41
- export function resolveHoursText(point: RecyclingPointHoursSource): string | null {
42
- const details = point.details;
43
- if (resolveHoursMode(details) === 'text') {
44
- const fromDetails = details?.hours;
45
- if (typeof fromDetails === 'string' && fromDetails.trim()) return fromDetails.trim();
46
- if (typeof point.hours === 'string' && point.hours.trim()) return point.hours.trim();
47
- return null;
48
- }
49
- return null;
50
- }
51
-
52
- /** Texte d'affichage des horaires (structuré converti ou texte libre). */
53
- export function resolveHoursDisplay(
54
- point: RecyclingPointHoursSource,
55
- timeZone?: string,
56
- ): string | null {
57
- const text = resolveHoursText(point);
58
- if (text) return text;
59
- const utc = resolveOpeningHoursUtc(point);
60
- if (!utc) {
61
- if (typeof point.hours === 'string' && point.hours.trim()) return point.hours.trim();
62
- return null;
63
- }
64
- const tz = timeZone ?? getBrowserTimeZone();
65
- const summary = formatOpeningHoursSummary(utc, tz);
66
- return summary || null;
67
- }
68
-
69
- export function resolveHoursModeFromPoint(point: RecyclingPointHoursSource): HoursInputMode {
70
- return resolveHoursMode(point.details ?? undefined);
71
- }
1
+ import type { HoursInputMode, OpeningHours } from './types';
2
+ import { convertUtcToLocal } from './convert';
3
+ import { formatOpeningHoursSummary } from './format';
4
+ import { getBrowserTimeZone } from './timezone';
5
+
6
+ export type RecyclingPointHoursSource = {
7
+ hours?: string | null;
8
+ details?: Record<string, unknown> | null;
9
+ };
10
+
11
+ export function resolveHoursMode(
12
+ details?: Record<string, unknown> | null,
13
+ ): HoursInputMode {
14
+ if (!details) return 'text';
15
+ const mode = details.hoursMode;
16
+ if (mode === 'structured' || mode === 'text') return mode;
17
+ if (details.openingHours != null) return 'structured';
18
+ return 'text';
19
+ }
20
+
21
+ export function resolveOpeningHoursUtc(
22
+ point: RecyclingPointHoursSource,
23
+ ): OpeningHours | null {
24
+ const details = point.details;
25
+ if (!details || resolveHoursMode(details) !== 'structured') return null;
26
+ const raw = details.openingHours;
27
+ if (!raw || typeof raw !== 'object') return null;
28
+ return raw as OpeningHours;
29
+ }
30
+
31
+ export function resolveOpeningHoursLocal(
32
+ point: RecyclingPointHoursSource,
33
+ timeZone?: string,
34
+ ): { rules: OpeningHours['rules']; exceptions?: OpeningHours['exceptions'] } | null {
35
+ const utc = resolveOpeningHoursUtc(point);
36
+ if (!utc) return null;
37
+ const tz = timeZone ?? getBrowserTimeZone();
38
+ return convertUtcToLocal(utc, tz);
39
+ }
40
+
41
+ export function resolveHoursText(point: RecyclingPointHoursSource): string | null {
42
+ const details = point.details;
43
+ if (resolveHoursMode(details) === 'text') {
44
+ const fromDetails = details?.hours;
45
+ if (typeof fromDetails === 'string' && fromDetails.trim()) return fromDetails.trim();
46
+ if (typeof point.hours === 'string' && point.hours.trim()) return point.hours.trim();
47
+ return null;
48
+ }
49
+ return null;
50
+ }
51
+
52
+ /** Texte d'affichage des horaires (structuré converti ou texte libre). */
53
+ export function resolveHoursDisplay(
54
+ point: RecyclingPointHoursSource,
55
+ timeZone?: string,
56
+ ): string | null {
57
+ const text = resolveHoursText(point);
58
+ if (text) return text;
59
+ const utc = resolveOpeningHoursUtc(point);
60
+ if (!utc) {
61
+ if (typeof point.hours === 'string' && point.hours.trim()) return point.hours.trim();
62
+ return null;
63
+ }
64
+ const tz = timeZone ?? getBrowserTimeZone();
65
+ const summary = formatOpeningHoursSummary(utc, tz);
66
+ return summary || null;
67
+ }
68
+
69
+ export function resolveHoursModeFromPoint(point: RecyclingPointHoursSource): HoursInputMode {
70
+ return resolveHoursMode(point.details ?? undefined);
71
+ }
@@ -1,74 +1,74 @@
1
- import { WEEKDAYS, type TimeSlot, type Weekday } from './types';
2
- import { WEEKDAY_INDEX, WEEKDAY_REF_DATES } from './constants';
3
-
4
- const TIME_RE = /^([01]\d|2[0-3]):([0-5]\d)$/;
5
-
6
- export function parseTimeMinutes(value: string): number | null {
7
- const m = TIME_RE.exec(value.trim());
8
- if (!m) return null;
9
- return Number(m[1]) * 60 + Number(m[2]);
10
- }
11
-
12
- export function formatTimeMinutes(totalMinutes: number): string {
13
- const normalized = ((totalMinutes % (24 * 60)) + 24 * 60) % (24 * 60);
14
- const h = Math.floor(normalized / 60);
15
- const min = normalized % 60;
16
- return `${String(h).padStart(2, '0')}:${String(min).padStart(2, '0')}`;
17
- }
18
-
19
- export function formatTimeDisplay(value: string): string {
20
- const minutes = parseTimeMinutes(value);
21
- if (minutes == null) return value;
22
- const h = Math.floor(minutes / 60);
23
- const min = minutes % 60;
24
- return min === 0 ? `${h}h` : `${h}h${String(min).padStart(2, '0')}`;
25
- }
26
-
27
- export function slotsOverlap(a: TimeSlot, b: TimeSlot): boolean {
28
- const aOpen = parseTimeMinutes(a.open);
29
- const aClose = parseTimeMinutes(a.close);
30
- const bOpen = parseTimeMinutes(b.open);
31
- const bClose = parseTimeMinutes(b.close);
32
- if (aOpen == null || aClose == null || bOpen == null || bClose == null) return false;
33
- if (aClose <= aOpen || bClose <= bOpen) return false;
34
- return aOpen < bClose && bOpen < aClose;
35
- }
36
-
37
- export function sortSlots(slots: TimeSlot[]): TimeSlot[] {
38
- return [...slots].sort(
39
- (a, b) => (parseTimeMinutes(a.open) ?? 0) - (parseTimeMinutes(b.open) ?? 0),
40
- );
41
- }
42
-
43
- export function nextWeekday(day: Weekday): Weekday {
44
- const idx = (WEEKDAY_INDEX[day] + 1) % 7;
45
- return WEEKDAYS[idx];
46
- }
47
-
48
- export function addDaysToIsoDate(isoDate: string, days: number): string {
49
- const [y, mo, d] = isoDate.split('-').map(Number);
50
- const dt = new Date(Date.UTC(y, mo - 1, d + days));
51
- return dt.toISOString().slice(0, 10);
52
- }
53
-
54
- export function weekdayFromUtcDate(date: Date): Weekday {
55
- const jsDay = date.getUTCDay();
56
- const map: Weekday[] = [
57
- 'sunday',
58
- 'monday',
59
- 'tuesday',
60
- 'wednesday',
61
- 'thursday',
62
- 'friday',
63
- 'saturday',
64
- ];
65
- return map[jsDay];
66
- }
67
-
68
- export function utcHhmmFromDate(date: Date): string {
69
- return formatTimeMinutes(date.getUTCHours() * 60 + date.getUTCMinutes());
70
- }
71
-
72
- export function refDateForWeekday(day: Weekday): string {
73
- return WEEKDAY_REF_DATES[day];
74
- }
1
+ import { WEEKDAYS, type TimeSlot, type Weekday } from './types';
2
+ import { WEEKDAY_INDEX, WEEKDAY_REF_DATES } from './constants';
3
+
4
+ const TIME_RE = /^([01]\d|2[0-3]):([0-5]\d)$/;
5
+
6
+ export function parseTimeMinutes(value: string): number | null {
7
+ const m = TIME_RE.exec(value.trim());
8
+ if (!m) return null;
9
+ return Number(m[1]) * 60 + Number(m[2]);
10
+ }
11
+
12
+ export function formatTimeMinutes(totalMinutes: number): string {
13
+ const normalized = ((totalMinutes % (24 * 60)) + 24 * 60) % (24 * 60);
14
+ const h = Math.floor(normalized / 60);
15
+ const min = normalized % 60;
16
+ return `${String(h).padStart(2, '0')}:${String(min).padStart(2, '0')}`;
17
+ }
18
+
19
+ export function formatTimeDisplay(value: string): string {
20
+ const minutes = parseTimeMinutes(value);
21
+ if (minutes == null) return value;
22
+ const h = Math.floor(minutes / 60);
23
+ const min = minutes % 60;
24
+ return min === 0 ? `${h}h` : `${h}h${String(min).padStart(2, '0')}`;
25
+ }
26
+
27
+ export function slotsOverlap(a: TimeSlot, b: TimeSlot): boolean {
28
+ const aOpen = parseTimeMinutes(a.open);
29
+ const aClose = parseTimeMinutes(a.close);
30
+ const bOpen = parseTimeMinutes(b.open);
31
+ const bClose = parseTimeMinutes(b.close);
32
+ if (aOpen == null || aClose == null || bOpen == null || bClose == null) return false;
33
+ if (aClose <= aOpen || bClose <= bOpen) return false;
34
+ return aOpen < bClose && bOpen < aClose;
35
+ }
36
+
37
+ export function sortSlots(slots: TimeSlot[]): TimeSlot[] {
38
+ return [...slots].sort(
39
+ (a, b) => (parseTimeMinutes(a.open) ?? 0) - (parseTimeMinutes(b.open) ?? 0),
40
+ );
41
+ }
42
+
43
+ export function nextWeekday(day: Weekday): Weekday {
44
+ const idx = (WEEKDAY_INDEX[day] + 1) % 7;
45
+ return WEEKDAYS[idx];
46
+ }
47
+
48
+ export function addDaysToIsoDate(isoDate: string, days: number): string {
49
+ const [y, mo, d] = isoDate.split('-').map(Number);
50
+ const dt = new Date(Date.UTC(y, mo - 1, d + days));
51
+ return dt.toISOString().slice(0, 10);
52
+ }
53
+
54
+ export function weekdayFromUtcDate(date: Date): Weekday {
55
+ const jsDay = date.getUTCDay();
56
+ const map: Weekday[] = [
57
+ 'sunday',
58
+ 'monday',
59
+ 'tuesday',
60
+ 'wednesday',
61
+ 'thursday',
62
+ 'friday',
63
+ 'saturday',
64
+ ];
65
+ return map[jsDay];
66
+ }
67
+
68
+ export function utcHhmmFromDate(date: Date): string {
69
+ return formatTimeMinutes(date.getUTCHours() * 60 + date.getUTCMinutes());
70
+ }
71
+
72
+ export function refDateForWeekday(day: Weekday): string {
73
+ return WEEKDAY_REF_DATES[day];
74
+ }
@@ -1,34 +1,34 @@
1
- /** Offset du fuseau IANA en minutes (positif = en avance sur UTC) à un instant donné. */
2
- export function getTimezoneOffsetMinutes(timeZone: string, at: Date): number {
3
- const utc = new Date(at.toLocaleString('en-US', { timeZone: 'UTC' }));
4
- const zoned = new Date(at.toLocaleString('en-US', { timeZone }));
5
- return (zoned.getTime() - utc.getTime()) / 60_000;
6
- }
7
-
8
- /** Convertit une date/heure locale (dans timeZone) en instant UTC. */
9
- export function localDateTimeToUtc(
10
- dateIso: string,
11
- time: string,
12
- timeZone: string,
13
- ): Date {
14
- const [y, mo, d] = dateIso.split('-').map(Number);
15
- const [h, mi] = time.split(':').map(Number);
16
- let utcMs = Date.UTC(y, mo - 1, d, h, mi, 0);
17
- for (let i = 0; i < 2; i++) {
18
- const offset = getTimezoneOffsetMinutes(timeZone, new Date(utcMs));
19
- utcMs = Date.UTC(y, mo - 1, d, h, mi, 0) - offset * 60_000;
20
- }
21
- return new Date(utcMs);
22
- }
23
-
24
- export function getBrowserTimeZone(): string {
25
- try {
26
- return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
27
- } catch {
28
- return 'UTC';
29
- }
30
- }
31
-
32
- export function utcDateIso(at: Date): string {
33
- return at.toISOString().slice(0, 10);
34
- }
1
+ /** Offset du fuseau IANA en minutes (positif = en avance sur UTC) à un instant donné. */
2
+ export function getTimezoneOffsetMinutes(timeZone: string, at: Date): number {
3
+ const utc = new Date(at.toLocaleString('en-US', { timeZone: 'UTC' }));
4
+ const zoned = new Date(at.toLocaleString('en-US', { timeZone }));
5
+ return (zoned.getTime() - utc.getTime()) / 60_000;
6
+ }
7
+
8
+ /** Convertit une date/heure locale (dans timeZone) en instant UTC. */
9
+ export function localDateTimeToUtc(
10
+ dateIso: string,
11
+ time: string,
12
+ timeZone: string,
13
+ ): Date {
14
+ const [y, mo, d] = dateIso.split('-').map(Number);
15
+ const [h, mi] = time.split(':').map(Number);
16
+ let utcMs = Date.UTC(y, mo - 1, d, h, mi, 0);
17
+ for (let i = 0; i < 2; i++) {
18
+ const offset = getTimezoneOffsetMinutes(timeZone, new Date(utcMs));
19
+ utcMs = Date.UTC(y, mo - 1, d, h, mi, 0) - offset * 60_000;
20
+ }
21
+ return new Date(utcMs);
22
+ }
23
+
24
+ export function getBrowserTimeZone(): string {
25
+ try {
26
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
27
+ } catch {
28
+ return 'UTC';
29
+ }
30
+ }
31
+
32
+ export function utcDateIso(at: Date): string {
33
+ return at.toISOString().slice(0, 10);
34
+ }