@agent-native/core 0.159.5 → 0.160.1

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 (81) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/assets/server/lib/generation.ts +10 -6
  3. package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +18 -4
  4. package/corpus/templates/calendar/actions/create-event.ts +4 -2
  5. package/corpus/templates/calendar/actions/event-action-helpers.ts +56 -9
  6. package/corpus/templates/calendar/actions/update-event.ts +1 -0
  7. package/corpus/templates/calendar/agent-native.config.ts +1 -0
  8. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +8 -2
  9. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +102 -68
  10. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +150 -49
  11. package/corpus/templates/calendar/app/components/calendar/MonthView.tsx +57 -23
  12. package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +4 -1
  13. package/corpus/templates/calendar/app/components/calendar/TimezoneSwitchDialog.tsx +70 -0
  14. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +127 -88
  15. package/corpus/templates/calendar/app/components/calendar/WorkingLocationEditor.tsx +59 -25
  16. package/corpus/templates/calendar/app/hooks/use-event-drag.ts +55 -18
  17. package/corpus/templates/calendar/app/i18n-data.ts +60 -0
  18. package/corpus/templates/calendar/app/lib/all-day-layout.ts +16 -8
  19. package/corpus/templates/calendar/app/lib/calendar-drafts.ts +56 -0
  20. package/corpus/templates/calendar/app/lib/calendar-timezone.ts +351 -0
  21. package/corpus/templates/calendar/app/lib/event-layout.ts +14 -8
  22. package/corpus/templates/calendar/app/lib/out-of-office.ts +29 -61
  23. package/corpus/templates/calendar/app/lib/working-location.ts +34 -2
  24. package/corpus/templates/calendar/app/pages/CalendarView.tsx +317 -139
  25. package/corpus/templates/calendar/changelog/2026-08-13-calendar-stays-pinned-to-the-saved-timezone-and-asks-before-.md +6 -0
  26. package/corpus/templates/calendar/changelog/2026-08-13-working-locations-can-be-added-from-calendar-days-with-a-ful.md +6 -0
  27. package/corpus/templates/calendar/changelog/2026-08-14-choosing-office-or-other-when-adding-a-working-location-creates.md +6 -0
  28. package/corpus/templates/calendar/changelog/2026-08-14-creating-an-other-working-location-now-keeps-the-custom-name.md +6 -0
  29. package/corpus/templates/calendar/changelog/2026-08-14-timed-working-locations-keep-home-office-or-custom-titles.md +6 -0
  30. package/corpus/templates/calendar/changelog/2026-08-14-timed-working-locations-now-keep-a-home-office-or-custom-title.md +6 -0
  31. package/corpus/templates/calendar/changelog/2026-08-14-turning-a-midnight-ending-working-location-back-to-all-day.md +6 -0
  32. package/corpus/templates/calendar/server/lib/google-calendar.ts +22 -1
  33. package/dist/agent/engine/builder-engine.d.ts +16 -2
  34. package/dist/agent/engine/builder-engine.js +178 -115
  35. package/dist/agent/engine/builtin.js +13 -4
  36. package/dist/agent/engine/credential-errors.d.ts +23 -0
  37. package/dist/agent/engine/credential-errors.js +19 -0
  38. package/dist/agent/engine/error-detail.d.ts +15 -0
  39. package/dist/agent/engine/error-detail.js +29 -0
  40. package/dist/agent/engine/registry.d.ts +14 -3
  41. package/dist/agent/engine/registry.js +106 -61
  42. package/dist/agent/engine/types.d.ts +16 -0
  43. package/dist/agent/engine/types.js +9 -0
  44. package/dist/agent/production-agent.d.ts +7 -0
  45. package/dist/agent/production-agent.js +68 -31
  46. package/dist/agent/run-manager.js +30 -0
  47. package/dist/agent/thread-data-builder.js +4 -0
  48. package/dist/agent/tool-approval-migrations.d.ts +1 -0
  49. package/dist/agent/tool-approval-migrations.js +10 -0
  50. package/dist/agent/tool-approval-store.d.ts +5 -3
  51. package/dist/agent/tool-approval-store.js +11 -6
  52. package/dist/agent/types.d.ts +9 -0
  53. package/dist/cli/code-agent-connector.js +79 -7
  54. package/dist/cli/portal-transfer.d.ts +43 -0
  55. package/dist/cli/portal-transfer.js +201 -0
  56. package/dist/client/agent-chat-adapter.js +46 -15
  57. package/dist/client/error-format.js +25 -2
  58. package/dist/client/sse-event-processor.d.ts +2 -0
  59. package/dist/client/sse-event-processor.js +12 -1
  60. package/dist/collab/awareness.d.ts +2 -2
  61. package/dist/collab/struct-routes.d.ts +1 -1
  62. package/dist/jobs/background-automation-runner.js +9 -15
  63. package/dist/notifications/routes.d.ts +6 -6
  64. package/dist/observability/routes.d.ts +5 -5
  65. package/dist/resources/handlers.d.ts +1 -1
  66. package/dist/secrets/register-framework-secrets.js +25 -0
  67. package/dist/secrets/routes.d.ts +9 -9
  68. package/dist/server/agent-chat-plugin.js +9 -2
  69. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  70. package/dist/server/credential-provider.d.ts +80 -0
  71. package/dist/server/credential-provider.js +168 -1
  72. package/dist/server/google-realtime-session.d.ts +1 -1
  73. package/dist/server/google-realtime-session.js +6 -6
  74. package/dist/server/index.d.ts +1 -1
  75. package/dist/server/index.js +6 -1
  76. package/dist/server/realtime-token.d.ts +1 -1
  77. package/dist/server/realtime-voice.js +12 -6
  78. package/dist/server/transcribe-voice.d.ts +1 -1
  79. package/dist/server/transcribe-voice.js +18 -16
  80. package/dist/transcription/builder-transcription.js +8 -4
  81. package/package.json +3 -3
@@ -1,7 +1,17 @@
1
1
  import type { CalendarEvent } from "@shared/api";
2
- import { parseISO, startOfDay, set, addMinutes } from "date-fns";
2
+ import { parseISO, startOfDay } from "date-fns";
3
3
  import { useState, useRef, useCallback, useEffect } from "react";
4
4
 
5
+ import {
6
+ addCalendarDays,
7
+ dateKeyToDate,
8
+ dateKeyToTimezoneIso,
9
+ dateToCalendarDateKey,
10
+ getBrowserTimezone,
11
+ getEventDateKey,
12
+ getEventSegmentForCalendarDay,
13
+ } from "@/lib/calendar-timezone";
14
+
5
15
  const SNAP_MINUTES = 15;
6
16
 
7
17
  interface DragState {
@@ -48,6 +58,8 @@ export interface UseEventDragOptions {
48
58
  onEventTimeChange: (eventId: string, newStart: Date, newEnd: Date) => void;
49
59
  /** All events (to find the event being dragged) */
50
60
  events: CalendarEvent[];
61
+ /** IANA timezone used by the visible calendar grid */
62
+ timezone?: string;
51
63
  }
52
64
 
53
65
  export function useEventDrag({
@@ -57,6 +69,7 @@ export function useEventDrag({
57
69
  days,
58
70
  onEventTimeChange,
59
71
  events,
72
+ timezone = getBrowserTimezone(),
60
73
  }: UseEventDragOptions) {
61
74
  const [dragState, setDragState] = useState<DragState | null>(null);
62
75
  const dragStateRef = useRef<DragState | null>(null);
@@ -127,10 +140,13 @@ export function useEventDrag({
127
140
  // Compute current event position
128
141
  const evStart = parseISO(event.start);
129
142
  const evEnd = parseISO(event.end);
130
- const dayStart = set(startOfDay(evStart), {
131
- hours: startHour,
132
- });
133
- const startMinutes = (evStart.getTime() - dayStart.getTime()) / 60000;
143
+ const eventDateKey =
144
+ getEventDateKey(event, timezone) ?? dateToCalendarDateKey(evStart);
145
+ const eventDay = days?.[dayIndex] ?? dateKeyToDate(eventDateKey);
146
+ const segment = getEventSegmentForCalendarDay(event, eventDay, timezone);
147
+ const startMinutes =
148
+ segment?.startMinutes ??
149
+ (evStart.getTime() - startOfDay(evStart).getTime()) / 60000;
134
150
  const durationMinutes = (evEnd.getTime() - evStart.getTime()) / 60000;
135
151
 
136
152
  const originalTop = Math.max(0, (startMinutes / 60) * hourHeight);
@@ -172,6 +188,7 @@ export function useEventDrag({
172
188
  getScrollTop,
173
189
  startHour,
174
190
  hourHeight,
191
+ timezone,
175
192
  ],
176
193
  );
177
194
 
@@ -288,20 +305,39 @@ export function useEventDrag({
288
305
  pxToMinutes(state.currentHeight),
289
306
  );
290
307
 
291
- // Determine the base day
308
+ // Determine the base day in the visible calendar timezone.
292
309
  const originalStart = parseISO(state.event.start);
293
- let baseDay: Date;
294
- if (days && state.currentDayIndex !== state.startDayIndex) {
295
- baseDay = days[state.currentDayIndex];
296
- } else {
297
- baseDay = startOfDay(originalStart);
298
- }
299
-
300
- const newStart = addMinutes(
301
- set(baseDay, { hours: startHour, minutes: 0, seconds: 0 }),
302
- topMinutes,
303
- );
304
- const newEnd = addMinutes(newStart, heightMinutes);
310
+ const baseDay =
311
+ days && state.currentDayIndex !== state.startDayIndex
312
+ ? days[state.currentDayIndex]
313
+ : dateKeyToDate(
314
+ getEventDateKey(state.event, timezone) ??
315
+ dateToCalendarDateKey(originalStart),
316
+ );
317
+ const baseDate = dateToCalendarDateKey(baseDay);
318
+ const startTotalMinutes = startHour * 60 + topMinutes;
319
+ const endTotalMinutes = startTotalMinutes + heightMinutes;
320
+ const toZonedIso = (totalMinutes: number) => {
321
+ const dayOffset = Math.floor(totalMinutes / (24 * 60));
322
+ const minuteOfDay = totalMinutes - dayOffset * 24 * 60;
323
+ const hour = Math.floor(minuteOfDay / 60);
324
+ const minute = Math.round(minuteOfDay % 60);
325
+ const normalizedMinute = minute === 60 ? 0 : minute;
326
+ const normalizedHour = minute === 60 ? hour + 1 : hour;
327
+ const time =
328
+ normalizedHour >= 24
329
+ ? "00:00"
330
+ : `${String(normalizedHour).padStart(2, "0")}:${String(
331
+ normalizedMinute,
332
+ ).padStart(2, "0")}`;
333
+ return dateKeyToTimezoneIso(
334
+ addCalendarDays(baseDate, dayOffset),
335
+ time,
336
+ timezone,
337
+ );
338
+ };
339
+ const newStart = new Date(toZonedIso(startTotalMinutes));
340
+ const newEnd = new Date(toZonedIso(endTotalMinutes));
305
341
 
306
342
  onEventTimeChange(state.eventId, newStart, newEnd);
307
343
  }
@@ -314,6 +350,7 @@ export function useEventDrag({
314
350
  days,
315
351
  startHour,
316
352
  onEventTimeChange,
353
+ timezone,
317
354
  ]);
318
355
 
319
356
  const cancelDrag = useCallback(() => {
@@ -664,6 +664,7 @@ const enUS = {
664
664
  },
665
665
  },
666
666
  calendarView: {
667
+ addWorkingLocation: "Add working location",
667
668
  addTitleBeforeCreate: "Add a title before creating the event",
668
669
  calendarSettingsLoading:
669
670
  "Calendar settings are still loading. Try again in a moment.",
@@ -692,6 +693,11 @@ const enUS = {
692
693
  updatingEvent: "Updating event...",
693
694
  updatingRecurringEvent: "Updating recurring event...",
694
695
  week: "Week",
696
+ timezoneSwitchTitle: "Use your browser timezone?",
697
+ timezoneSwitchDescription:
698
+ "Calendar is pinned to {{savedTimezone}}, but your browser is in {{browserTimezone}}. Switch the calendar timezone?",
699
+ timezoneSwitchKeep: "Keep {{timezone}}",
700
+ timezoneSwitchSwitch: "Switch to {{timezone}}",
695
701
  },
696
702
  eventForm: {
697
703
  addAttachment: "Add attachment",
@@ -8014,6 +8020,7 @@ const translatedCalendarRawBurnDown = {
8014
8020
  yourInformation: "你的信息",
8015
8021
  },
8016
8022
  calendarView: {
8023
+ addWorkingLocation: "添加工作地点",
8017
8024
  addTitleBeforeCreate: "创建事件前请添加标题",
8018
8025
  calendarSettingsLoading: "日历设置仍在加载。请稍后再试。",
8019
8026
  day: "日",
@@ -8041,6 +8048,11 @@ const translatedCalendarRawBurnDown = {
8041
8048
  updatingEvent: "正在更新事件...",
8042
8049
  updatingRecurringEvent: "正在更新重复事件...",
8043
8050
  week: "周",
8051
+ timezoneSwitchTitle: "使用浏览器时区?",
8052
+ timezoneSwitchDescription:
8053
+ "日历固定为 {{savedTimezone}},但浏览器使用 {{browserTimezone}}。要切换日历时区吗?",
8054
+ timezoneSwitchKeep: "保留 {{timezone}}",
8055
+ timezoneSwitchSwitch: "切换到 {{timezone}}",
8044
8056
  },
8045
8057
  routeTitles: {
8046
8058
  bookingLinks: "预约链接 — Calendar",
@@ -8183,6 +8195,7 @@ const translatedCalendarRawBurnDown = {
8183
8195
  yourInformation: "Tu información",
8184
8196
  },
8185
8197
  calendarView: {
8198
+ addWorkingLocation: "Añadir ubicación de trabajo",
8186
8199
  addTitleBeforeCreate: "Añade un título antes de crear el evento",
8187
8200
  calendarSettingsLoading:
8188
8201
  "La configuración del calendario aún se está cargando. Inténtalo de nuevo en un momento.",
@@ -8213,6 +8226,11 @@ const translatedCalendarRawBurnDown = {
8213
8226
  updatingEvent: "Actualizando evento...",
8214
8227
  updatingRecurringEvent: "Actualizando evento recurrente...",
8215
8228
  week: "Semana",
8229
+ timezoneSwitchTitle: "¿Usar la zona horaria del navegador?",
8230
+ timezoneSwitchDescription:
8231
+ "El calendario está fijado en {{savedTimezone}}, pero tu navegador usa {{browserTimezone}}. ¿Cambiar la zona horaria del calendario?",
8232
+ timezoneSwitchKeep: "Mantener {{timezone}}",
8233
+ timezoneSwitchSwitch: "Cambiar a {{timezone}}",
8216
8234
  },
8217
8235
  routeTitles: {
8218
8236
  bookingLinks: "Enlaces de reserva — Calendar",
@@ -8361,6 +8379,7 @@ const translatedCalendarRawBurnDown = {
8361
8379
  yourInformation: "Vos informations",
8362
8380
  },
8363
8381
  calendarView: {
8382
+ addWorkingLocation: "Ajouter un lieu de travail",
8364
8383
  addTitleBeforeCreate: "Ajoutez un titre avant de créer l'événement",
8365
8384
  calendarSettingsLoading:
8366
8385
  "Les paramètres du calendrier sont encore en cours de chargement. Réessayez dans un instant.",
@@ -8389,6 +8408,11 @@ const translatedCalendarRawBurnDown = {
8389
8408
  updatingEvent: "Mise à jour de l'événement...",
8390
8409
  updatingRecurringEvent: "Mise à jour de l'événement récurrent...",
8391
8410
  week: "Semaine",
8411
+ timezoneSwitchTitle: "Utiliser le fuseau horaire du navigateur ?",
8412
+ timezoneSwitchDescription:
8413
+ "Le calendrier est fixé sur {{savedTimezone}}, mais votre navigateur utilise {{browserTimezone}}. Changer le fuseau horaire du calendrier ?",
8414
+ timezoneSwitchKeep: "Conserver {{timezone}}",
8415
+ timezoneSwitchSwitch: "Passer à {{timezone}}",
8392
8416
  },
8393
8417
  routeTitles: {
8394
8418
  bookingLinks: "Liens de réservation — Calendar",
@@ -8539,6 +8563,7 @@ const translatedCalendarRawBurnDown = {
8539
8563
  yourInformation: "Deine Informationen",
8540
8564
  },
8541
8565
  calendarView: {
8566
+ addWorkingLocation: "Arbeitsort hinzufügen",
8542
8567
  addTitleBeforeCreate:
8543
8568
  "Fügen Sie einen Titel hinzu, bevor Sie das Ereignis erstellen",
8544
8569
  calendarSettingsLoading:
@@ -8569,6 +8594,11 @@ const translatedCalendarRawBurnDown = {
8569
8594
  updatingEvent: "Ereignis wird aktualisiert...",
8570
8595
  updatingRecurringEvent: "Wiederkehrendes Ereignis wird aktualisiert...",
8571
8596
  week: "Woche",
8597
+ timezoneSwitchTitle: "Browser-Zeitzone verwenden?",
8598
+ timezoneSwitchDescription:
8599
+ "Der Kalender ist auf {{savedTimezone}} festgelegt, aber Ihr Browser verwendet {{browserTimezone}}. Kalenderzeitzone wechseln?",
8600
+ timezoneSwitchKeep: "{{timezone}} beibehalten",
8601
+ timezoneSwitchSwitch: "Zu {{timezone}} wechseln",
8572
8602
  },
8573
8603
  routeTitles: {
8574
8604
  bookingLinks: "Buchungslinks — Calendar",
@@ -8718,6 +8748,7 @@ const translatedCalendarRawBurnDown = {
8718
8748
  yourInformation: "あなたの情報",
8719
8749
  },
8720
8750
  calendarView: {
8751
+ addWorkingLocation: "勤務場所を追加",
8721
8752
  addTitleBeforeCreate: "イベントを作成する前にタイトルを追加してください",
8722
8753
  calendarSettingsLoading:
8723
8754
  "カレンダー設定を読み込み中です。少し待ってからもう一度お試しください。",
@@ -8746,6 +8777,11 @@ const translatedCalendarRawBurnDown = {
8746
8777
  updatingEvent: "イベントを更新中...",
8747
8778
  updatingRecurringEvent: "繰り返しイベントを更新中...",
8748
8779
  week: "週",
8780
+ timezoneSwitchTitle: "ブラウザのタイムゾーンを使用しますか?",
8781
+ timezoneSwitchDescription:
8782
+ "カレンダーは {{savedTimezone}} に固定されていますが、ブラウザは {{browserTimezone}} を使用しています。カレンダーのタイムゾーンを切り替えますか?",
8783
+ timezoneSwitchKeep: "{{timezone}} を維持",
8784
+ timezoneSwitchSwitch: "{{timezone}} に切り替え",
8749
8785
  },
8750
8786
  routeTitles: {
8751
8787
  bookingLinks: "予約リンク — Calendar",
@@ -8890,6 +8926,7 @@ const translatedCalendarRawBurnDown = {
8890
8926
  yourInformation: "내 정보",
8891
8927
  },
8892
8928
  calendarView: {
8929
+ addWorkingLocation: "근무 위치 추가",
8893
8930
  addTitleBeforeCreate: "이벤트를 만들기 전에 제목을 추가하세요",
8894
8931
  calendarSettingsLoading:
8895
8932
  "캘린더 설정을 아직 불러오는 중입니다. 잠시 후 다시 시도하세요.",
@@ -8919,6 +8956,11 @@ const translatedCalendarRawBurnDown = {
8919
8956
  updatingEvent: "이벤트 업데이트 중...",
8920
8957
  updatingRecurringEvent: "반복 이벤트 업데이트 중...",
8921
8958
  week: "주",
8959
+ timezoneSwitchTitle: "브라우저 시간대를 사용할까요?",
8960
+ timezoneSwitchDescription:
8961
+ "캘린더는 {{savedTimezone}}에 고정되어 있지만 브라우저는 {{browserTimezone}}을(를) 사용합니다. 캘린더 시간대를 전환할까요?",
8962
+ timezoneSwitchKeep: "{{timezone}} 유지",
8963
+ timezoneSwitchSwitch: "{{timezone}}(으)로 전환",
8922
8964
  },
8923
8965
  routeTitles: {
8924
8966
  bookingLinks: "예약 링크 — Calendar",
@@ -9065,6 +9107,7 @@ const translatedCalendarRawBurnDown = {
9065
9107
  yourInformation: "Suas informações",
9066
9108
  },
9067
9109
  calendarView: {
9110
+ addWorkingLocation: "Adicionar local de trabalho",
9068
9111
  addTitleBeforeCreate: "Adicione um título antes de criar o evento",
9069
9112
  calendarSettingsLoading:
9070
9113
  "As configurações do calendário ainda estão carregando. Tente novamente em instantes.",
@@ -9094,6 +9137,11 @@ const translatedCalendarRawBurnDown = {
9094
9137
  updatingEvent: "Atualizando evento...",
9095
9138
  updatingRecurringEvent: "Atualizando evento recorrente...",
9096
9139
  week: "Semana",
9140
+ timezoneSwitchTitle: "Usar o fuso horário do navegador?",
9141
+ timezoneSwitchDescription:
9142
+ "O calendário está fixado em {{savedTimezone}}, mas o navegador usa {{browserTimezone}}. Mudar o fuso horário do calendário?",
9143
+ timezoneSwitchKeep: "Manter {{timezone}}",
9144
+ timezoneSwitchSwitch: "Mudar para {{timezone}}",
9097
9145
  },
9098
9146
  routeTitles: {
9099
9147
  bookingLinks: "Links de reserva — Calendar",
@@ -9240,6 +9288,7 @@ const translatedCalendarRawBurnDown = {
9240
9288
  yourInformation: "आपकी जानकारी",
9241
9289
  },
9242
9290
  calendarView: {
9291
+ addWorkingLocation: "कार्य स्थान जोड़ें",
9243
9292
  addTitleBeforeCreate: "इवेंट बनाने से पहले शीर्षक जोड़ें",
9244
9293
  calendarSettingsLoading:
9245
9294
  "कैलेंडर सेटिंग अभी लोड हो रही हैं। कृपया थोड़ी देर में फिर कोशिश करें।",
@@ -9268,6 +9317,11 @@ const translatedCalendarRawBurnDown = {
9268
9317
  updatingEvent: "इवेंट अपडेट हो रहा है...",
9269
9318
  updatingRecurringEvent: "दोहराया जाने वाला इवेंट अपडेट हो रहा है...",
9270
9319
  week: "सप्ताह",
9320
+ timezoneSwitchTitle: "ब्राउज़र का टाइमज़ोन इस्तेमाल करें?",
9321
+ timezoneSwitchDescription:
9322
+ "कैलेंडर {{savedTimezone}} पर पिन है, लेकिन आपका ब्राउज़र {{browserTimezone}} पर है। कैलेंडर टाइमज़ोन बदलें?",
9323
+ timezoneSwitchKeep: "{{timezone}} रखें",
9324
+ timezoneSwitchSwitch: "{{timezone}} पर बदलें",
9271
9325
  },
9272
9326
  routeTitles: {
9273
9327
  bookingLinks: "बुकिंग लिंक — Calendar",
@@ -9413,6 +9467,7 @@ const translatedCalendarRawBurnDown = {
9413
9467
  yourInformation: "معلوماتك",
9414
9468
  },
9415
9469
  calendarView: {
9470
+ addWorkingLocation: "إضافة موقع العمل",
9416
9471
  addTitleBeforeCreate: "أضف عنوانًا قبل إنشاء الحدث",
9417
9472
  calendarSettingsLoading:
9418
9473
  "لا تزال إعدادات التقويم قيد التحميل. حاول مرة أخرى بعد قليل.",
@@ -9441,6 +9496,11 @@ const translatedCalendarRawBurnDown = {
9441
9496
  updatingEvent: "جارٍ تحديث الحدث...",
9442
9497
  updatingRecurringEvent: "جارٍ تحديث الحدث المتكرر...",
9443
9498
  week: "الأسبوع",
9499
+ timezoneSwitchTitle: "استخدام المنطقة الزمنية للمتصفح؟",
9500
+ timezoneSwitchDescription:
9501
+ "التقويم مثبت على {{savedTimezone}}، لكن متصفحك يستخدم {{browserTimezone}}. هل تريد تبديل المنطقة الزمنية للتقويم؟",
9502
+ timezoneSwitchKeep: "الاحتفاظ بـ {{timezone}}",
9503
+ timezoneSwitchSwitch: "التبديل إلى {{timezone}}",
9444
9504
  },
9445
9505
  routeTitles: {
9446
9506
  bookingLinks: "روابط الحجز — Calendar",
@@ -1,6 +1,11 @@
1
1
  import type { CalendarEvent } from "@shared/api";
2
- import { addDays, format, parseISO, startOfDay } from "date-fns";
3
2
 
3
+ import {
4
+ addCalendarDays,
5
+ dateToCalendarDateKey,
6
+ eventOverlapsCalendarDay,
7
+ getBrowserTimezone,
8
+ } from "@/lib/calendar-timezone";
4
9
  import { getFullDayOutOfOfficeDateRange } from "@/lib/out-of-office";
5
10
  import { isWorkingLocationEvent } from "@/lib/working-location";
6
11
 
@@ -37,22 +42,24 @@ export function partitionAllDayEvents(events: CalendarEvent[]) {
37
42
  export function getAllDaySpan(
38
43
  event: CalendarEvent,
39
44
  days: Date[],
45
+ timezone: string = getBrowserTimezone(),
40
46
  ): Omit<AllDaySpan, "event"> | null {
41
- const eventStart = parseISO(event.start);
42
- const eventEnd = event.end ? parseISO(event.end) : addDays(eventStart, 1);
43
47
  const outOfOfficeRange = getFullDayOutOfOfficeDateRange(event);
48
+ const eventStartDate = event.start.slice(0, 10);
49
+ const eventEndDate =
50
+ event.end?.slice(0, 10) ?? addCalendarDays(eventStartDate, 1);
44
51
 
45
52
  let startCol = -1;
46
53
  let endCol = -1;
47
54
 
48
55
  for (let index = 0; index < days.length; index++) {
49
- const dayStart = startOfDay(days[index]);
50
- const dayEnd = addDays(dayStart, 1);
51
- const dayDate = format(days[index], "yyyy-MM-dd");
56
+ const dayDate = dateToCalendarDateKey(days[index]);
52
57
  const overlaps = outOfOfficeRange
53
58
  ? dayDate >= outOfOfficeRange.startDate &&
54
59
  dayDate < outOfOfficeRange.endDateExclusive
55
- : eventStart < dayEnd && eventEnd > dayStart;
60
+ : event.allDay
61
+ ? eventStartDate <= dayDate && eventEndDate > dayDate
62
+ : eventOverlapsCalendarDay(event, days[index], timezone);
56
63
  if (overlaps) {
57
64
  if (startCol === -1) startCol = index;
58
65
  endCol = index;
@@ -71,10 +78,11 @@ function compareSpans(a: AllDaySpan, b: AllDaySpan): number {
71
78
  export function layoutAllDayEvents(
72
79
  events: CalendarEvent[],
73
80
  days: Date[],
81
+ timezone: string = getBrowserTimezone(),
74
82
  ): AllDayLayout {
75
83
  const spans = events
76
84
  .map((event) => {
77
- const span = getAllDaySpan(event, days);
85
+ const span = getAllDaySpan(event, days, timezone);
78
86
  return span ? { event, ...span } : null;
79
87
  })
80
88
  .filter((span): span is AllDaySpan => span !== null)
@@ -0,0 +1,56 @@
1
+ import type { CalendarEventDraft } from "@shared/api";
2
+
3
+ import { addCalendarDays, dateToCalendarDateKey } from "./calendar-timezone";
4
+
5
+ export function resolveDraftWorkingLocation(
6
+ draft: Pick<
7
+ CalendarEventDraft,
8
+ "workingLocationType" | "workingLocationLabel" | "location"
9
+ >,
10
+ ): {
11
+ workingLocationType: "homeOffice" | "officeLocation" | "customLocation";
12
+ workingLocationLabel: string;
13
+ } {
14
+ return {
15
+ workingLocationType: draft.workingLocationType ?? "homeOffice",
16
+ // Drafts initialize `location` to "", so `??` would drop the Other name.
17
+ workingLocationLabel: (
18
+ draft.workingLocationLabel ||
19
+ draft.location ||
20
+ ""
21
+ ).trim(),
22
+ };
23
+ }
24
+
25
+ export function buildWorkingLocationDraft({
26
+ id,
27
+ date,
28
+ accountEmail,
29
+ now = new Date().toISOString(),
30
+ }: {
31
+ id: string;
32
+ date: Date;
33
+ accountEmail?: string;
34
+ now?: string;
35
+ }): CalendarEventDraft {
36
+ const dateKey = dateToCalendarDateKey(date);
37
+ return {
38
+ id,
39
+ title: "",
40
+ description: "",
41
+ location: "",
42
+ start: dateKey,
43
+ end: addCalendarDays(dateKey, 1),
44
+ startTimeZone: undefined,
45
+ endTimeZone: undefined,
46
+ allDay: true,
47
+ eventType: "workingLocation",
48
+ workingLocationType: "homeOffice",
49
+ workingLocationLabel: "",
50
+ transparency: "transparent",
51
+ visibility: "public",
52
+ accountEmail,
53
+ createdAt: now,
54
+ updatedAt: now,
55
+ };
56
+ }