@tuturuuu/ui 0.1.0 → 0.3.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 (128) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/package.json +82 -70
  3. package/src/components/ui/__tests__/avatar.test.tsx +8 -5
  4. package/src/components/ui/calendar-app/components/calendar-connections-compact.tsx +414 -0
  5. package/src/components/ui/calendar-app/components/calendar-connections-manager.tsx +5 -1
  6. package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +529 -0
  7. package/src/components/ui/calendar-app/components/calendar-connections-unified.tsx +26 -1429
  8. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +711 -0
  9. package/src/components/ui/chart.test.tsx +29 -0
  10. package/src/components/ui/chart.tsx +12 -3
  11. package/src/components/ui/chat/chat-agent-details-external-thread-panel.test.tsx +43 -13
  12. package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +138 -74
  13. package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +70 -0
  14. package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +60 -1
  15. package/src/components/ui/chat/chat-agent-details-sidebar.tsx +13 -5
  16. package/src/components/ui/chat/chat-sidebar-panel.test.tsx +110 -0
  17. package/src/components/ui/chat/chat-sidebar-panel.tsx +13 -3
  18. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +24 -1
  19. package/src/components/ui/custom/__tests__/tuturuuu-logo.test.ts +12 -3
  20. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +39 -0
  21. package/src/components/ui/custom/common-footer.tsx +16 -1
  22. package/src/components/ui/custom/production-indicator.tsx +1 -1
  23. package/src/components/ui/custom/settings/sidebar-settings.tsx +1 -1
  24. package/src/components/ui/custom/settings/task-settings.tsx +18 -0
  25. package/src/components/ui/custom/settings-dialog-shell.tsx +38 -23
  26. package/src/components/ui/custom/sidebar-context-compile-graph.test.ts +60 -0
  27. package/src/components/ui/custom/sidebar-context.tsx +61 -61
  28. package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +123 -0
  29. package/src/components/ui/custom/tuturuuu-logo-urls.ts +6 -0
  30. package/src/components/ui/custom/tuturuuu-logo.tsx +25 -7
  31. package/src/components/ui/custom/workspace-select-helpers.ts +20 -0
  32. package/src/components/ui/custom/workspace-select.tsx +33 -12
  33. package/src/components/ui/finance/invoices/components/invoice-checkout-summary.tsx +7 -1
  34. package/src/components/ui/finance/invoices/components/invoice-payment-settings.tsx +3 -0
  35. package/src/components/ui/finance/invoices/components/invoice-products-permission-warning.tsx +58 -0
  36. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +12 -20
  37. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +10 -9
  38. package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-content.ts +10 -5
  39. package/src/components/ui/finance/invoices/hooks.ts +75 -20
  40. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +137 -0
  41. package/src/components/ui/finance/invoices/new-invoice-page.tsx +86 -37
  42. package/src/components/ui/finance/invoices/product-selection.test.tsx +8 -26
  43. package/src/components/ui/finance/invoices/product-selection.tsx +2 -10
  44. package/src/components/ui/finance/invoices/standard-invoice.tsx +88 -26
  45. package/src/components/ui/finance/invoices/subscription-invoice.tsx +154 -46
  46. package/src/components/ui/finance/invoices/utils.test.ts +50 -0
  47. package/src/components/ui/finance/invoices/utils.ts +75 -17
  48. package/src/components/ui/finance/shared/finance-display-amount.tsx +3 -1
  49. package/src/components/ui/finance/shared/finance-permission-warning-dialog.test.tsx +34 -0
  50. package/src/components/ui/finance/shared/finance-permission-warning-dialog.tsx +157 -0
  51. package/src/components/ui/finance/transactions/form-basic-tab.tsx +8 -0
  52. package/src/components/ui/finance/transactions/form-more-tab.tsx +8 -0
  53. package/src/components/ui/finance/transactions/form-types.ts +2 -0
  54. package/src/components/ui/finance/transactions/form.test.tsx +43 -0
  55. package/src/components/ui/finance/transactions/form.tsx +60 -0
  56. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +27 -0
  57. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +13 -1
  58. package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +4 -0
  59. package/src/components/ui/finance/transactions/transactions-page.tsx +23 -1
  60. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  61. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +5 -0
  62. package/src/components/ui/legacy/calendar/calendar-content.tsx +9 -1
  63. package/src/components/ui/legacy/calendar/event-modal.tsx +146 -2
  64. package/src/components/ui/legacy/calendar/event-preview-popover.tsx +200 -0
  65. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +76 -0
  66. package/src/components/ui/legacy/calendar/smart-calendar.tsx +13 -1
  67. package/src/components/ui/legacy/meet/page.test.ts +180 -0
  68. package/src/components/ui/legacy/meet/page.tsx +87 -39
  69. package/src/components/ui/legacy/meet/planId/page.tsx +10 -4
  70. package/src/components/ui/text-editor/__tests__/task-mention-chip.test.tsx +203 -6
  71. package/src/components/ui/text-editor/task-mention-chip.tsx +29 -7
  72. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +79 -25
  73. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-external-workspaces.test.tsx +392 -0
  74. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.test.tsx +57 -0
  75. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.tsx +106 -0
  76. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +106 -161
  77. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-assignees.ts +96 -150
  78. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-labels.ts +63 -79
  79. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-projects.ts +64 -83
  80. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +115 -155
  81. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-utils.ts +319 -2
  82. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +8 -1
  83. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +63 -37
  84. package/src/components/ui/tu-do/boards/boardId/kanban/kanban-column-collapse.ts +16 -0
  85. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +46 -0
  86. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +5 -3
  87. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +19 -7
  88. package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-menus.test.tsx +181 -2
  89. package/src/components/ui/tu-do/boards/boardId/menus/index.ts +1 -0
  90. package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-menu.tsx +463 -0
  91. package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-utils.ts +109 -0
  92. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +4 -0
  93. package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardCheckbox.tsx +6 -3
  94. package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardDates.tsx +26 -9
  95. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-checkbox-style.ts +39 -0
  96. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.test.ts +43 -0
  97. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +33 -0
  98. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.test.ts +31 -0
  99. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.ts +9 -0
  100. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.test.tsx +124 -0
  101. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.tsx +88 -0
  102. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +151 -76
  103. package/src/components/ui/tu-do/boards/boardId/task-card/task-scheduling-badge.tsx +174 -0
  104. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +34 -13
  105. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +54 -1
  106. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +158 -0
  107. package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +5 -2
  108. package/src/components/ui/tu-do/shared/board-client.tsx +12 -2
  109. package/src/components/ui/tu-do/shared/board-views.tsx +195 -328
  110. package/src/components/ui/tu-do/shared/list-view.tsx +18 -8
  111. package/src/components/ui/tu-do/shared/task-due-date-visibility.test.ts +72 -0
  112. package/src/components/ui/tu-do/shared/task-due-date-visibility.ts +38 -0
  113. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/__tests__/use-task-realtime-sync.test.tsx +37 -9
  114. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +6 -3
  115. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-realtime-sync.ts +89 -70
  116. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +2 -2
  117. package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +33 -0
  118. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +74 -3
  119. package/src/hooks/__tests__/use-task-actions.test.tsx +118 -0
  120. package/src/hooks/__tests__/use-user-config.test.tsx +65 -0
  121. package/src/hooks/__tests__/use-workspace-presence.test.tsx +1 -1
  122. package/src/hooks/use-calendar-sync.tsx +22 -277
  123. package/src/hooks/use-calendar.tsx +95 -525
  124. package/src/hooks/use-task-actions.ts +43 -117
  125. package/src/hooks/use-user-config.ts +1 -1
  126. package/src/hooks/use-workspace-config.ts +6 -2
  127. package/src/hooks/use-workspace-presence.ts +1 -1
  128. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-bar.tsx +0 -94
@@ -23,7 +23,6 @@ import {
23
23
  useRef,
24
24
  useState,
25
25
  } from 'react';
26
- import { toast } from '../components/ui/sonner';
27
26
  import { useCalendarSync } from './use-calendar-sync';
28
27
 
29
28
  // Utility function to round time to nearest 15-minute interval
@@ -107,11 +106,18 @@ const CalendarContext = createContext<{
107
106
  deleteEvent: (eventId: string) => Promise<void>;
108
107
  isModalOpen: boolean;
109
108
  activeEvent: CalendarEvent | undefined;
109
+ isPreviewOpen: boolean;
110
+ previewEvent: CalendarEvent | undefined;
110
111
  openModal: (
111
112
  eventId?: string,
112
113
  modalType?: 'all-day' | 'event',
113
114
  options?: { defaultNewEventTab?: 'manual' | 'ai' }
114
115
  ) => void;
116
+ openEventEditor: (
117
+ eventId?: string,
118
+ options?: { defaultNewEventTab?: 'manual' | 'ai' }
119
+ ) => void;
120
+ closePreview: () => void;
115
121
  closeModal: () => void;
116
122
  isEditing: () => boolean;
117
123
  hideModal: () => void;
@@ -174,7 +180,11 @@ const CalendarContext = createContext<{
174
180
  deleteEvent: () => Promise.resolve(),
175
181
  isModalOpen: false,
176
182
  activeEvent: undefined,
183
+ isPreviewOpen: false,
184
+ previewEvent: undefined,
177
185
  openModal: () => undefined,
186
+ openEventEditor: () => undefined,
187
+ closePreview: () => undefined,
178
188
  closeModal: () => undefined,
179
189
  isEditing: () => false,
180
190
  hideModal: () => undefined,
@@ -362,10 +372,10 @@ async function syncTaskDurationAfterEventChange(
362
372
 
363
373
  export const CalendarProvider = ({
364
374
  ws,
365
- useQuery,
375
+ useQuery: _useQuery,
366
376
  useQueryClient,
367
377
  children,
368
- experimentalGoogleToken,
378
+ experimentalGoogleToken: _experimentalGoogleToken,
369
379
  readOnly = false,
370
380
  }: {
371
381
  ws?: Workspace;
@@ -391,6 +401,7 @@ export const CalendarProvider = ({
391
401
 
392
402
  // Modal state
393
403
  const [activeEventId, setActiveEventId] = useState<string | null>(null);
404
+ const [previewEventId, setPreviewEventId] = useState<string | null>(null);
394
405
  const [isModalHidden, setModalHidden] = useState(false);
395
406
  const [pendingNewEvent, setPendingNewEvent] =
396
407
  useState<Partial<CalendarEvent> | null>(null);
@@ -615,6 +626,7 @@ export const CalendarProvider = ({
615
626
  color: eventColor as SupportedColor,
616
627
  location: event.location || '',
617
628
  locked: true,
629
+ source: event.source,
618
630
  }),
619
631
  }
620
632
  );
@@ -804,6 +816,7 @@ export const CalendarProvider = ({
804
816
  location: updateData.location,
805
817
  }),
806
818
  ...(updateData.locked !== undefined && { locked: updateData.locked }),
819
+ ...(updateData.source !== undefined && { source: updateData.source }),
807
820
  };
808
821
 
809
822
  // ws is guaranteed to be defined here (validated above at line 732)
@@ -904,6 +917,7 @@ export const CalendarProvider = ({
904
917
  'color',
905
918
  'location',
906
919
  'locked',
920
+ 'source',
907
921
  ];
908
922
 
909
923
  const cleanedUpdates: Partial<CalendarEvent> = {};
@@ -1021,68 +1035,15 @@ export const CalendarProvider = ({
1021
1035
  // Just clear the pending event
1022
1036
  setPendingNewEvent(null);
1023
1037
  setActiveEventId(null);
1038
+ setPreviewEventId(null);
1024
1039
  return;
1025
1040
  }
1026
1041
 
1027
1042
  if (!ws) throw new Error('No workspace selected');
1028
1043
 
1029
- // Find the event first to get the Google Calendar ID
1030
1044
  const eventToDelete = events.find(
1031
1045
  (e: CalendarEvent) => e.id === eventId
1032
1046
  ) as (CalendarEvent & { task_id?: string | null }) | undefined;
1033
- const googleCalendarEventId = eventToDelete?.google_event_id;
1034
-
1035
- // --- Google Calendar Sync (Delete) ---
1036
- if (googleCalendarEventId && experimentalGoogleToken) {
1037
- // Check if ID exists and feature enabled
1038
- try {
1039
- const syncResponse = await fetch('/api/v1/calendar/auth/sync', {
1040
- method: 'DELETE',
1041
- headers: { 'Content-Type': 'application/json' },
1042
- body: JSON.stringify({ googleCalendarEventId }),
1043
- });
1044
-
1045
- if (!syncResponse.ok) {
1046
- const errorData = await syncResponse.json();
1047
- if (errorData.eventNotFound) {
1048
- console.warn(
1049
- `Google event ${googleCalendarEventId} not found during delete sync. Proceeding with local delete.`
1050
- );
1051
- // Don't throw, just log. The event is gone from Google anyway.
1052
- } else if (errorData.needsReAuth) {
1053
- // Notify user to re-authenticate with Google Calendar
1054
- toast.error('Google Calendar authentication expired', {
1055
- description:
1056
- 'Please re-authenticate your Google Calendar connection to continue syncing events.',
1057
- action: {
1058
- label: 'Re-authenticate',
1059
- onClick: () => {
1060
- // Redirect to Google Calendar auth page or open auth modal
1061
- // This could be enhanced to open a specific auth flow
1062
- window.open(
1063
- `/api/v1/calendar/auth?wsId=${ws?.id}`,
1064
- '_blank'
1065
- );
1066
- },
1067
- },
1068
- });
1069
- // Continue with local delete - don't block user action
1070
- console.warn(
1071
- 'Google Calendar re-authentication required, proceeding with local delete'
1072
- );
1073
- } else {
1074
- // Throw an error to potentially stop the local delete or notify user
1075
- throw new Error(
1076
- `Google Calendar sync (DELETE) failed: ${syncResponse.statusText} - ${JSON.stringify(errorData)}`
1077
- );
1078
- }
1079
- }
1080
- } catch (_) {
1081
- // Failed to sync delete with Google Calendar
1082
- }
1083
- } else if (experimentalGoogleToken && !googleCalendarEventId) {
1084
- // Event has no Google Calendar ID, skipping delete sync
1085
- }
1086
1047
 
1087
1048
  if (!ws?.id) {
1088
1049
  throw new Error('No workspace selected');
@@ -1116,6 +1077,7 @@ export const CalendarProvider = ({
1116
1077
  // Refresh the query cache after deleting an event
1117
1078
  refresh();
1118
1079
  setActiveEventId(null);
1080
+ setPreviewEventId(null);
1119
1081
 
1120
1082
  // If this was a task-linked event, refresh task queries
1121
1083
  if (hasLinkedTask || hasLinkedHabit) {
@@ -1128,357 +1090,49 @@ export const CalendarProvider = ({
1128
1090
  refresh,
1129
1091
  pendingNewEvent,
1130
1092
  events,
1131
- experimentalGoogleToken,
1132
1093
  queryClient,
1133
1094
  onTaskScheduled,
1134
1095
  readOnly,
1135
1096
  ]
1136
1097
  );
1137
1098
 
1138
- // Automatically fetch Google Calendar events
1139
- const fetchGoogleCalendarEvents = async () => {
1140
- if (!ws?.id) {
1141
- throw new Error('No workspace selected');
1142
- }
1143
- const response = await fetch(`/api/v1/calendar/auth/fetch?wsId=${ws.id}`);
1144
- if (!response.ok) {
1145
- throw new Error('Failed to fetch Google Calendar events');
1146
- }
1147
- return await response.json();
1148
- };
1149
-
1150
- // Query to fetch Google Calendar events every 1 hour
1151
- const { data: googleData } = useQuery({
1152
- queryKey: ['googleCalendarEvents', ws?.id],
1153
- queryFn: fetchGoogleCalendarEvents,
1154
- enabled: !!ws?.id && !!experimentalGoogleToken?.id,
1155
- refetchInterval: 1000 * 60 * 60, // Fetch every 1 hour
1156
- staleTime: 1000 * 60 * 60, // Data is considered fresh for 1 hour
1157
- });
1158
-
1159
- const googleEvents = useMemo(() => googleData?.events || [], [googleData]);
1160
-
1099
+ const googleEvents = useMemo(() => [], []);
1161
1100
  const getGoogleEvents = useCallback(() => googleEvents, [googleEvents]);
1162
1101
 
1163
- // Function to synchronize local events with Google Calendar
1164
- const syncEvents = useCallback(
1165
- async (
1166
- progressCallback?: (progress: {
1167
- phase: 'delete' | 'update' | 'insert' | 'complete';
1168
- current: number;
1169
- total: number;
1170
- changesMade: boolean;
1171
- }) => void
1172
- ) => {
1173
- const workspaceId = ws?.id;
1174
- if (!workspaceId || !googleEvents.length || !experimentalGoogleToken)
1175
- return;
1176
-
1177
- // Get local events that are synced with Google Calendar
1178
- const localGoogleEvents: CalendarEvent[] = events.filter(
1179
- (e: CalendarEvent) => e.google_event_id
1180
- );
1181
-
1182
- // Create a map for faster lookups of local events
1183
- const localEventMap = new Map<string, CalendarEvent>();
1184
- localGoogleEvents.forEach((event) => {
1185
- if (event.google_event_id) {
1186
- localEventMap.set(event.google_event_id, event);
1187
- }
1188
- });
1189
-
1190
- // Create a set of google_event_id from Google Calendar events for quick lookup
1191
- const googleEventIds: Set<string | undefined> = new Set(
1192
- googleEvents.map((e: { google_event_id?: string }) => e.google_event_id)
1193
- );
1194
-
1195
- // Identify events to delete: local events not present in Google Calendar
1196
- const eventsToDelete = localGoogleEvents.filter(
1197
- (e) => e.google_event_id && !googleEventIds.has(e.google_event_id)
1198
- );
1199
-
1200
- // Initialize batch operations - we'll perform these in a more optimized way
1201
- const eventsToUpdate: Array<{ id: string; data: any }> = [];
1202
- const eventsToInsert: Array<any> = [];
1203
- let changesMade = false;
1204
-
1205
- // Report initial progress
1206
- if (progressCallback) {
1207
- progressCallback({
1208
- phase: 'delete',
1209
- current: 0,
1210
- total: eventsToDelete.length,
1211
- changesMade: false,
1212
- });
1213
- }
1214
-
1215
- // Handle events to delete
1216
- if (eventsToDelete.length > 0) {
1217
- changesMade = true;
1218
- // Delete events in batches for better performance
1219
- const batchSize = 10;
1220
- for (let i = 0; i < eventsToDelete.length; i += batchSize) {
1221
- const batch = eventsToDelete.slice(i, i + batchSize);
1222
- const eventIds = batch.map((e) => e.id);
1223
-
1224
- // Report progress
1225
- if (progressCallback) {
1226
- progressCallback({
1227
- phase: 'delete',
1228
- current: i + batch.length,
1229
- total: eventsToDelete.length,
1230
- changesMade: true,
1231
- });
1232
- }
1233
-
1234
- try {
1235
- await Promise.all(
1236
- eventIds.map(async (eventId) => {
1237
- const response = await fetch(
1238
- `/api/v1/workspaces/${workspaceId}/calendar/events/${eventId}`,
1239
- {
1240
- method: 'DELETE',
1241
- }
1242
- );
1243
-
1244
- if (!response.ok) {
1245
- const errorData = await response.json().catch(() => null);
1246
- throw new Error(
1247
- errorData?.error || 'Failed to delete calendar event'
1248
- );
1249
- }
1250
- })
1251
- );
1252
- } catch (_) {
1253
- // Failed to delete events batch
1254
- }
1255
- }
1256
- }
1257
-
1258
- // Gather events to update or insert
1259
- for (const gEvent of googleEvents) {
1260
- // Skip events without google_event_id
1261
- if (!gEvent.google_event_id) continue;
1262
-
1263
- const localEvent = localEventMap.get(gEvent.google_event_id);
1264
-
1265
- if (localEvent) {
1266
- // Check if there are any significant changes in the event details that require an update
1267
- // For encrypted events, we only check non-encrypted fields (dates, color)
1268
- // since we can't compare encrypted content with plaintext Google data
1269
- const isEncrypted = localEvent.is_encrypted === true;
1270
-
1271
- const hasNonEncryptedChanges =
1272
- localEvent.start_at !== gEvent.start_at ||
1273
- localEvent.end_at !== gEvent.end_at ||
1274
- localEvent.color !== gEvent.color;
1275
-
1276
- const hasContentChanges =
1277
- !isEncrypted &&
1278
- (localEvent.title !== gEvent.title ||
1279
- localEvent.description !== (gEvent.description || '') ||
1280
- localEvent.location !== (gEvent.location || ''));
1281
-
1282
- const hasChanges = hasNonEncryptedChanges || hasContentChanges;
1283
-
1284
- // Only update if there are actual changes
1285
- if (hasChanges) {
1286
- changesMade = true;
1287
-
1288
- // For encrypted events, only update non-encrypted fields (dates, color)
1289
- // to preserve the encrypted title/description/location
1290
- if (isEncrypted) {
1291
- eventsToUpdate.push({
1292
- id: localEvent.id,
1293
- data: {
1294
- start_at: gEvent.start_at,
1295
- end_at: gEvent.end_at,
1296
- color: gEvent.color || 'BLUE',
1297
- // Don't update title, description, location - they are encrypted
1298
- },
1299
- });
1300
- } else {
1301
- eventsToUpdate.push({
1302
- id: localEvent.id,
1303
- data: {
1304
- title: gEvent.title,
1305
- description: gEvent.description || '',
1306
- start_at: gEvent.start_at,
1307
- end_at: gEvent.end_at,
1308
- color: gEvent.color || 'BLUE',
1309
- location: gEvent.location || '',
1310
- },
1311
- });
1312
- }
1313
- }
1314
- } else {
1315
- // Check for content-based duplicates before adding
1316
- const potentialDuplicates = events.filter(
1317
- (localEvent: CalendarEvent) => {
1318
- return (
1319
- localEvent.title === gEvent.title &&
1320
- localEvent.description === (gEvent.description || '') &&
1321
- localEvent.start_at === gEvent.start_at &&
1322
- localEvent.end_at === gEvent.end_at
1323
- );
1324
- }
1325
- );
1326
-
1327
- if (potentialDuplicates.length > 0) {
1328
- if (potentialDuplicates[0]) {
1329
- changesMade = true;
1330
- // Update the existing event with the Google Event ID rather than creating a new one
1331
- eventsToUpdate.push({
1332
- id: potentialDuplicates[0].id,
1333
- data: {
1334
- google_event_id: gEvent.google_event_id,
1335
- },
1336
- });
1337
- continue;
1338
- }
1339
- }
1340
-
1341
- // No duplicates found, add to insert batch
1342
- changesMade = true;
1343
- eventsToInsert.push({
1344
- title: gEvent.title,
1345
- description: gEvent.description || '',
1346
- start_at: gEvent.start_at,
1347
- end_at: gEvent.end_at,
1348
- color: gEvent.color || 'BLUE',
1349
- location: gEvent.location || '',
1350
- ws_id: ws?.id ?? '',
1351
- google_event_id: gEvent.google_event_id,
1352
- locked: gEvent.locked || false,
1353
- created_at: new Date().toISOString(),
1354
- });
1355
- }
1356
- }
1357
-
1358
- // Process batch updates
1359
- if (eventsToUpdate.length > 0) {
1360
- // Report progress for update phase
1361
- if (progressCallback) {
1362
- progressCallback({
1363
- phase: 'update',
1364
- current: 0,
1365
- total: eventsToUpdate.length,
1366
- changesMade: changesMade,
1367
- });
1368
- }
1369
-
1370
- const batchSize = 5; // Smaller batch size for updates to be safer
1371
-
1372
- for (let i = 0; i < eventsToUpdate.length; i += batchSize) {
1373
- const batch = eventsToUpdate.slice(i, i + batchSize);
1374
-
1375
- // Report progress update
1376
- if (progressCallback) {
1377
- progressCallback({
1378
- phase: 'update',
1379
- current: i + batch.length,
1380
- total: eventsToUpdate.length,
1381
- changesMade: changesMade,
1382
- });
1383
- }
1384
-
1385
- // Process each update one by one to ensure reliability
1386
- for (const item of batch) {
1387
- try {
1388
- const response = await fetch(
1389
- `/api/v1/workspaces/${workspaceId}/calendar/events/${item.id}`,
1390
- {
1391
- method: 'PUT',
1392
- headers: { 'Content-Type': 'application/json' },
1393
- body: JSON.stringify(item.data),
1394
- }
1395
- );
1102
+ // Modal management
1103
+ const openEventEditor = useCallback(
1104
+ (eventId?: string, options?: { defaultNewEventTab?: 'manual' | 'ai' }) => {
1105
+ setPreviewEventId(null);
1396
1106
 
1397
- if (!response.ok) {
1398
- const errorData = await response.json().catch(() => null);
1399
- throw new Error(
1400
- errorData?.error || 'Failed to update calendar event'
1401
- );
1402
- }
1403
- } catch (_) {
1404
- // Failed to update event
1405
- }
1406
- }
1407
- }
1107
+ if (eventId) {
1108
+ setActiveEventId(eventId);
1109
+ setPendingNewEvent(null);
1110
+ setModalHidden(false);
1111
+ return;
1408
1112
  }
1409
1113
 
1410
- // Process batch inserts
1411
- if (eventsToInsert.length > 0) {
1412
- // Report progress for insert phase
1413
- if (progressCallback) {
1414
- progressCallback({
1415
- phase: 'insert',
1416
- current: 0,
1417
- total: eventsToInsert.length,
1418
- changesMade: changesMade,
1419
- });
1420
- }
1114
+ setDefaultNewEventTab(options?.defaultNewEventTab ?? 'manual');
1421
1115
 
1422
- const batchSize = 10;
1116
+ const now = roundToNearest15Minutes(new Date());
1117
+ const oneHourLater = new Date(now);
1118
+ oneHourLater.setHours(oneHourLater.getHours() + 1);
1423
1119
 
1424
- for (let i = 0; i < eventsToInsert.length; i += batchSize) {
1425
- const batch = eventsToInsert.slice(i, i + batchSize);
1426
-
1427
- // Report progress update
1428
- if (progressCallback) {
1429
- progressCallback({
1430
- phase: 'insert',
1431
- current: i + batch.length,
1432
- total: eventsToInsert.length,
1433
- changesMade: changesMade,
1434
- });
1435
- }
1436
-
1437
- try {
1438
- await Promise.all(
1439
- batch.map(async (event) => {
1440
- const response = await fetch(
1441
- `/api/v1/workspaces/${workspaceId}/calendar/events`,
1442
- {
1443
- method: 'POST',
1444
- headers: { 'Content-Type': 'application/json' },
1445
- body: JSON.stringify(event),
1446
- }
1447
- );
1448
-
1449
- if (!response.ok) {
1450
- const errorData = await response.json().catch(() => null);
1451
- throw new Error(
1452
- errorData?.error || 'Failed to insert calendar event'
1453
- );
1454
- }
1455
- })
1456
- );
1457
- } catch (_) {
1458
- // Failed to insert events batch
1459
- }
1460
- }
1461
- }
1462
-
1463
- // Report completion
1464
- if (progressCallback) {
1465
- progressCallback({
1466
- phase: 'complete',
1467
- current: 1,
1468
- total: 1,
1469
- changesMade: changesMade,
1470
- });
1471
- }
1120
+ const newEvent: Omit<CalendarEvent, 'id'> = {
1121
+ title: '',
1122
+ description: '',
1123
+ start_at: now.toISOString(),
1124
+ end_at: oneHourLater.toISOString(),
1125
+ color: 'BLUE',
1126
+ ws_id: ws?.id || '',
1127
+ };
1472
1128
 
1473
- // Only refresh local events if changes were made
1474
- if (changesMade) {
1475
- queryClient.invalidateQueries(['calendarEvents', ws?.id]);
1476
- }
1129
+ setPendingNewEvent(newEvent);
1130
+ setActiveEventId('new');
1131
+ setModalHidden(false);
1477
1132
  },
1478
- [googleEvents, events, ws?.id, queryClient, experimentalGoogleToken]
1133
+ [ws?.id]
1479
1134
  );
1480
1135
 
1481
- // Modal management
1482
1136
  const openModal = useCallback(
1483
1137
  (
1484
1138
  eventId?: string,
@@ -1486,32 +1140,15 @@ export const CalendarProvider = ({
1486
1140
  options?: { defaultNewEventTab?: 'manual' | 'ai' }
1487
1141
  ) => {
1488
1142
  if (eventId) {
1489
- // Opening an existing event
1490
- setActiveEventId(eventId);
1491
1143
  setPendingNewEvent(null);
1492
- } else {
1493
- // Creating a new event
1494
- setDefaultNewEventTab(options?.defaultNewEventTab ?? 'manual');
1495
-
1496
- const now = roundToNearest15Minutes(new Date());
1497
- const oneHourLater = new Date(now);
1498
- oneHourLater.setHours(oneHourLater.getHours() + 1);
1499
-
1500
- // Create a pending new event
1501
- const newEvent: Omit<CalendarEvent, 'id'> = {
1502
- title: '',
1503
- description: '',
1504
- start_at: now.toISOString(),
1505
- end_at: oneHourLater.toISOString(),
1506
- color: 'BLUE',
1507
- };
1508
-
1509
- setPendingNewEvent(newEvent);
1510
- setActiveEventId('new');
1144
+ setActiveEventId(null);
1145
+ setPreviewEventId(eventId);
1146
+ return;
1511
1147
  }
1512
- setModalHidden(false);
1148
+
1149
+ openEventEditor(undefined, options);
1513
1150
  },
1514
- []
1151
+ [openEventEditor]
1515
1152
  );
1516
1153
 
1517
1154
  const closeModal = useCallback(() => {
@@ -1519,6 +1156,10 @@ export const CalendarProvider = ({
1519
1156
  setPendingNewEvent(null);
1520
1157
  }, []);
1521
1158
 
1159
+ const closePreview = useCallback(() => {
1160
+ setPreviewEventId(null);
1161
+ }, []);
1162
+
1522
1163
  const activeEvent = useMemo(() => {
1523
1164
  // If it's a pending new event
1524
1165
  if (pendingNewEvent && activeEventId === 'new') {
@@ -1534,6 +1175,12 @@ export const CalendarProvider = ({
1534
1175
  : undefined;
1535
1176
  }, [activeEventId, events, pendingNewEvent]);
1536
1177
 
1178
+ const previewEvent = useMemo(() => {
1179
+ return previewEventId
1180
+ ? events.find((e: Partial<CalendarEvent>) => e.id === previewEventId)
1181
+ : undefined;
1182
+ }, [events, previewEventId]);
1183
+
1537
1184
  const isEditing = useCallback(() => !!activeEventId, [activeEventId]);
1538
1185
  const hideModal = useCallback(() => setModalHidden(true), []);
1539
1186
  const showModal = useCallback(() => setModalHidden(false), []);
@@ -1565,137 +1212,64 @@ export const CalendarProvider = ({
1565
1212
  statusMessage?: string;
1566
1213
  }) => void
1567
1214
  ) => {
1568
- if (!experimentalGoogleToken || !ws?.id) {
1215
+ if (!ws?.id) {
1569
1216
  return false;
1570
1217
  }
1571
1218
 
1572
1219
  try {
1573
- // First, capture current events count for comparison
1574
- const beforeCount = events.length;
1575
- const beforeGoogleCount = googleEvents.length;
1576
-
1577
- // Report fetch starting
1578
1220
  if (progressCallback) {
1579
1221
  progressCallback({
1580
1222
  phase: 'fetch',
1581
1223
  current: 0,
1582
1224
  total: 1,
1583
1225
  changesMade: false,
1584
- statusMessage: 'Fetching events from Google Calendar...',
1226
+ statusMessage: 'Syncing connected calendars...',
1585
1227
  });
1586
1228
  }
1587
1229
 
1588
- // Force fetch the latest events from Google with a cache-busting parameter
1589
- try {
1590
- const response = await fetch(
1591
- `/api/v1/calendar/auth/fetch?wsId=${ws.id}&force=true&t=${Date.now()}`
1592
- );
1593
- const data = await response.json();
1594
-
1595
- if (!response.ok) {
1596
- // Include the full error details from Google API
1597
- const errorDetails = data.error || response.statusText;
1598
- const statusCode = response.status;
1599
- const googleError = data.googleError || data.details || '';
1600
- throw new Error(
1601
- `Failed to fetch Google Calendar events: ${errorDetails} (${statusCode})${googleError ? ` - Google API: ${googleError}` : ''}`
1602
- );
1603
- }
1604
-
1605
- // Update googleEvents directly through queryClient for faster UI response
1606
- queryClient.setQueryData(['googleCalendarEvents', ws?.id], data);
1607
-
1608
- // Report fetch complete
1609
- if (progressCallback) {
1610
- progressCallback({
1611
- phase: 'fetch',
1612
- current: 1,
1613
- total: 1,
1614
- changesMade: data.events?.length !== beforeGoogleCount,
1615
- statusMessage: `Fetched ${data.events?.length || 0} events from Google Calendar`,
1616
- });
1617
- }
1618
- } catch (fetchError) {
1619
- if (progressCallback) {
1620
- progressCallback({
1621
- phase: 'fetch',
1622
- current: 0,
1623
- total: 1,
1624
- changesMade: false,
1625
- statusMessage:
1626
- fetchError instanceof Error
1627
- ? fetchError.message
1628
- : 'Failed to fetch events from Google Calendar',
1629
- });
1230
+ const response = await fetch(
1231
+ `/api/v1/workspaces/${ws.id}/calendar/sync`,
1232
+ {
1233
+ method: 'POST',
1234
+ headers: { 'Content-Type': 'application/json' },
1235
+ body: JSON.stringify({ direction: 'inbound', source: 'manual' }),
1630
1236
  }
1237
+ );
1631
1238
 
1632
- // Propagate the error instead of returning false
1633
- throw fetchError;
1239
+ const result = await response.json().catch(() => null);
1240
+ if (!response.ok) {
1241
+ throw new Error(result?.error || 'Calendar sync failed');
1634
1242
  }
1635
1243
 
1636
- // Manually run the sync process with progress tracking
1637
- let changesMade = false;
1638
- await syncEvents((progress) => {
1639
- if (progressCallback) {
1640
- // Forward the progress updates
1641
- progressCallback({
1642
- ...progress,
1643
- statusMessage:
1644
- progress.phase === 'delete'
1645
- ? `Removing ${progress.total} deleted events (${progress.current}/${progress.total})`
1646
- : progress.phase === 'update'
1647
- ? `Updating ${progress.total} events (${progress.current}/${progress.total})`
1648
- : progress.phase === 'insert'
1649
- ? `Adding ${progress.total} new events (${progress.current}/${progress.total})`
1650
- : progress.phase === 'complete'
1651
- ? 'Sync completed'
1652
- : undefined,
1653
- });
1654
- }
1655
-
1656
- // Track if any changes were made
1657
- if (progress.changesMade) {
1658
- changesMade = true;
1659
- }
1244
+ const inserted =
1245
+ (result?.summary?.google?.inserted ?? 0) +
1246
+ (result?.summary?.microsoft?.inserted ?? 0);
1247
+ const updated =
1248
+ (result?.summary?.google?.updated ?? 0) +
1249
+ (result?.summary?.microsoft?.updated ?? 0);
1250
+ const deleted =
1251
+ (result?.summary?.google?.deleted ?? 0) +
1252
+ (result?.summary?.microsoft?.deleted ?? 0);
1253
+ const changesMade = inserted + updated + deleted > 0;
1254
+
1255
+ await queryClient.invalidateQueries({
1256
+ queryKey: ['databaseCalendarEvents', ws.id],
1257
+ exact: false,
1660
1258
  });
1661
1259
 
1662
- // Force refresh of local events
1663
- if (changesMade) {
1664
- if (progressCallback) {
1665
- progressCallback({
1666
- phase: 'complete',
1667
- current: 1,
1668
- total: 1,
1669
- changesMade: true,
1670
- statusMessage: 'Refreshing your calendar...',
1671
- });
1672
- }
1673
-
1674
- await queryClient.invalidateQueries({
1675
- queryKey: ['calendarEvents', ws?.id],
1676
- refetchType: 'all',
1677
- });
1678
- }
1679
-
1680
- // Calculate changes
1681
- const afterCount = getEvents().length;
1682
- const countDifference = afterCount - beforeCount;
1683
-
1684
- // Final callback with summary
1685
1260
  if (progressCallback) {
1686
1261
  progressCallback({
1687
1262
  phase: 'complete',
1688
1263
  current: 1,
1689
1264
  total: 1,
1690
- changesMade: changesMade,
1265
+ changesMade,
1691
1266
  statusMessage: changesMade
1692
- ? `Sync complete. ${Math.abs(countDifference)} events ${countDifference >= 0 ? 'added' : 'removed'}.`
1267
+ ? `Sync complete. ${inserted} added, ${updated} updated, ${deleted} removed.`
1693
1268
  : 'Sync complete. No changes needed.',
1694
1269
  });
1695
1270
  }
1696
1271
 
1697
- // Return success with indication if changes were made
1698
- return changesMade || beforeGoogleCount !== googleEvents.length;
1272
+ return changesMade;
1699
1273
  } catch (_) {
1700
1274
  if (progressCallback) {
1701
1275
  progressCallback({
@@ -1710,15 +1284,7 @@ export const CalendarProvider = ({
1710
1284
  return false;
1711
1285
  }
1712
1286
  },
1713
- [
1714
- experimentalGoogleToken,
1715
- ws?.id,
1716
- events.length,
1717
- googleEvents.length,
1718
- queryClient,
1719
- syncEvents,
1720
- getEvents,
1721
- ]
1287
+ [queryClient, ws?.id]
1722
1288
  );
1723
1289
 
1724
1290
  const [isDragging, setIsDragging] = useState(false);
@@ -1905,7 +1471,11 @@ export const CalendarProvider = ({
1905
1471
  // New API
1906
1472
  isModalOpen: !isModalHidden && activeEventId !== null,
1907
1473
  activeEvent,
1474
+ isPreviewOpen: !!previewEvent,
1475
+ previewEvent,
1908
1476
  openModal,
1477
+ openEventEditor,
1478
+ closePreview,
1909
1479
  closeModal,
1910
1480
 
1911
1481
  // Legacy API