@tuturuuu/ui 0.2.0 → 0.3.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.
Files changed (129) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/package.json +79 -67
  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-operations-panel.test.tsx +396 -2
  12. package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +36 -8
  13. package/src/components/ui/chat/chat-agent-details-setup-panel.tsx +14 -0
  14. package/src/components/ui/chat/chat-agent-details-sidebar.test.tsx +5 -0
  15. package/src/components/ui/chat/chat-agent-details-sidebar.tsx +21 -7
  16. package/src/components/ui/chat/chat-agent-details-utils.test.ts +73 -0
  17. package/src/components/ui/chat/chat-agent-details-utils.tsx +100 -26
  18. package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +517 -0
  19. package/src/components/ui/chat/chat-workspace.tsx +31 -1
  20. package/src/components/ui/chat/hooks-messages.test.tsx +45 -1
  21. package/src/components/ui/chat/hooks-messages.ts +1 -1
  22. package/src/components/ui/chat/hooks-realtime.ts +13 -16
  23. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +24 -1
  24. package/src/components/ui/custom/__tests__/tuturuuu-logo.test.ts +12 -3
  25. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +39 -0
  26. package/src/components/ui/custom/common-footer.tsx +16 -1
  27. package/src/components/ui/custom/production-indicator.tsx +1 -1
  28. package/src/components/ui/custom/settings/sidebar-settings.tsx +1 -1
  29. package/src/components/ui/custom/settings/task-settings.tsx +18 -0
  30. package/src/components/ui/custom/settings-dialog-shell.tsx +38 -23
  31. package/src/components/ui/custom/sidebar-context-compile-graph.test.ts +60 -0
  32. package/src/components/ui/custom/sidebar-context.tsx +61 -61
  33. package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +123 -0
  34. package/src/components/ui/custom/tuturuuu-logo-urls.ts +6 -0
  35. package/src/components/ui/custom/tuturuuu-logo.tsx +25 -7
  36. package/src/components/ui/custom/workspace-select-helpers.ts +20 -0
  37. package/src/components/ui/custom/workspace-select.tsx +33 -12
  38. package/src/components/ui/finance/invoices/components/invoice-checkout-summary.tsx +7 -1
  39. package/src/components/ui/finance/invoices/components/invoice-payment-settings.tsx +3 -0
  40. package/src/components/ui/finance/invoices/components/invoice-products-permission-warning.tsx +58 -0
  41. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +12 -20
  42. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +10 -9
  43. package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-content.ts +10 -5
  44. package/src/components/ui/finance/invoices/hooks.ts +75 -20
  45. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +137 -0
  46. package/src/components/ui/finance/invoices/new-invoice-page.tsx +86 -37
  47. package/src/components/ui/finance/invoices/product-selection.test.tsx +8 -26
  48. package/src/components/ui/finance/invoices/product-selection.tsx +2 -10
  49. package/src/components/ui/finance/invoices/standard-invoice.tsx +88 -26
  50. package/src/components/ui/finance/invoices/subscription-invoice.tsx +154 -46
  51. package/src/components/ui/finance/invoices/utils.test.ts +50 -0
  52. package/src/components/ui/finance/invoices/utils.ts +75 -17
  53. package/src/components/ui/finance/shared/finance-display-amount.tsx +3 -1
  54. package/src/components/ui/finance/shared/finance-permission-warning-dialog.test.tsx +34 -0
  55. package/src/components/ui/finance/shared/finance-permission-warning-dialog.tsx +157 -0
  56. package/src/components/ui/finance/transactions/form-basic-tab.tsx +8 -0
  57. package/src/components/ui/finance/transactions/form-more-tab.tsx +8 -0
  58. package/src/components/ui/finance/transactions/form-types.ts +2 -0
  59. package/src/components/ui/finance/transactions/form.test.tsx +43 -0
  60. package/src/components/ui/finance/transactions/form.tsx +60 -0
  61. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +27 -0
  62. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +13 -1
  63. package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +4 -0
  64. package/src/components/ui/finance/transactions/transactions-page.tsx +23 -1
  65. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  66. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +5 -0
  67. package/src/components/ui/legacy/calendar/calendar-content.tsx +9 -1
  68. package/src/components/ui/legacy/calendar/event-modal.tsx +146 -2
  69. package/src/components/ui/legacy/calendar/event-preview-popover.tsx +200 -0
  70. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +76 -0
  71. package/src/components/ui/legacy/calendar/smart-calendar.tsx +13 -1
  72. package/src/components/ui/legacy/meet/page.test.ts +180 -0
  73. package/src/components/ui/legacy/meet/page.tsx +87 -39
  74. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +79 -25
  75. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-external-workspaces.test.tsx +392 -0
  76. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.test.tsx +57 -0
  77. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.tsx +106 -0
  78. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +106 -161
  79. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-assignees.ts +96 -150
  80. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-labels.ts +63 -79
  81. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-projects.ts +64 -83
  82. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +115 -155
  83. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-utils.ts +319 -2
  84. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +8 -1
  85. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +63 -37
  86. package/src/components/ui/tu-do/boards/boardId/kanban/kanban-column-collapse.ts +16 -0
  87. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +46 -0
  88. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +5 -3
  89. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +19 -7
  90. package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-menus.test.tsx +181 -2
  91. package/src/components/ui/tu-do/boards/boardId/menus/index.ts +1 -0
  92. package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-menu.tsx +463 -0
  93. package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-utils.ts +109 -0
  94. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +4 -0
  95. package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardCheckbox.tsx +6 -3
  96. package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardDates.tsx +26 -9
  97. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-checkbox-style.ts +39 -0
  98. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.test.ts +43 -0
  99. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +33 -0
  100. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.test.ts +31 -0
  101. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.ts +9 -0
  102. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.test.tsx +124 -0
  103. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.tsx +88 -0
  104. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +151 -76
  105. package/src/components/ui/tu-do/boards/boardId/task-card/task-scheduling-badge.tsx +174 -0
  106. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +34 -13
  107. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +54 -1
  108. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +158 -0
  109. package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +5 -2
  110. package/src/components/ui/tu-do/shared/board-client.tsx +12 -2
  111. package/src/components/ui/tu-do/shared/board-views.tsx +195 -328
  112. package/src/components/ui/tu-do/shared/list-view.tsx +18 -8
  113. package/src/components/ui/tu-do/shared/task-due-date-visibility.test.ts +72 -0
  114. package/src/components/ui/tu-do/shared/task-due-date-visibility.ts +38 -0
  115. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +6 -3
  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-semantic-task-search.ts +10 -33
  125. package/src/hooks/use-task-actions.ts +43 -117
  126. package/src/hooks/use-user-config.ts +1 -1
  127. package/src/hooks/use-workspace-config.ts +6 -2
  128. package/src/hooks/use-workspace-presence.ts +1 -1
  129. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-bar.tsx +0 -94
@@ -0,0 +1,711 @@
1
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
2
+ import {
3
+ type CalendarSourceOption,
4
+ getWorkspaceCalendarDefaultSource,
5
+ updateWorkspaceCalendarDefaultSource,
6
+ } from '@tuturuuu/internal-api';
7
+ import { createClient } from '@tuturuuu/supabase/next/client';
8
+ import { useRouter } from 'next/navigation';
9
+ import { useTranslations } from 'next-intl';
10
+ import { useMemo, useState } from 'react';
11
+ import { useCalendarSync } from '../../../../hooks/use-calendar-sync';
12
+ import { toast } from '../../sonner';
13
+ import type {
14
+ AuthResponse,
15
+ CalendarSyncHealth,
16
+ ConnectedAccount,
17
+ ProviderCalendar,
18
+ } from './calendar-types';
19
+
20
+ interface AccountsResponse {
21
+ accounts: ConnectedAccount[];
22
+ grouped: {
23
+ google: ConnectedAccount[];
24
+ microsoft: ConnectedAccount[];
25
+ };
26
+ total: number;
27
+ }
28
+
29
+ interface WorkspaceCalendar {
30
+ id: string;
31
+ ws_id: string;
32
+ name: string;
33
+ description: string | null;
34
+ color: string | null;
35
+ calendar_type: 'primary' | 'tasks' | 'habits' | 'custom';
36
+ is_system: boolean;
37
+ is_enabled: boolean;
38
+ position: number;
39
+ }
40
+
41
+ interface WorkspaceCalendarsResponse {
42
+ calendars: WorkspaceCalendar[];
43
+ grouped: {
44
+ system: WorkspaceCalendar[];
45
+ custom: WorkspaceCalendar[];
46
+ };
47
+ total: number;
48
+ }
49
+
50
+ interface ManualSyncResponse {
51
+ ok: boolean;
52
+ alreadyRunning?: boolean;
53
+ code?: string;
54
+ error?: string;
55
+ retryAfterSeconds?: number | null;
56
+ }
57
+
58
+ export type CalendarConnectionsUnifiedVariant = 'compact' | 'settings';
59
+
60
+ function sourceInputFromOption(option: CalendarSourceOption) {
61
+ if (option.provider === 'tuturuuu') {
62
+ return {
63
+ provider: 'tuturuuu' as const,
64
+ workspaceCalendarId: option.workspaceCalendarId,
65
+ };
66
+ }
67
+
68
+ return {
69
+ provider: option.provider,
70
+ connectionId: option.connectionId,
71
+ };
72
+ }
73
+
74
+ export function useCalendarConnectionsManager(wsId: string) {
75
+ const t = useTranslations('calendar');
76
+ const router = useRouter();
77
+ const queryClient = useQueryClient();
78
+ const [disconnectingId, setDisconnectingId] = useState<string | null>(null);
79
+ const [togglingIds, setTogglingIds] = useState<Set<string>>(new Set());
80
+ const [togglingTuturuuuIds, setTogglingTuturuuuIds] = useState<Set<string>>(
81
+ new Set()
82
+ );
83
+ const [expandedAccounts, setExpandedAccounts] = useState<Set<string>>(
84
+ new Set()
85
+ );
86
+ const [expandedPopoverAccounts, setExpandedPopoverAccounts] = useState<
87
+ Set<string>
88
+ >(
89
+ new Set(['tuturuuu']) // Tuturuuu expanded by default
90
+ );
91
+ const [showCreateCalendarDialog, setShowCreateCalendarDialog] =
92
+ useState(false);
93
+ const [newCalendarName, setNewCalendarName] = useState('');
94
+
95
+ const {
96
+ calendarConnections,
97
+ updateCalendarConnection,
98
+ setCalendarConnections,
99
+ syncToTuturuuu,
100
+ isSyncing,
101
+ } = useCalendarSync();
102
+
103
+ const syncMutation = useMutation({
104
+ mutationFn: async () => {
105
+ const response = await fetch(`/api/v1/workspaces/${wsId}/calendar/sync`, {
106
+ method: 'POST',
107
+ headers: { 'Content-Type': 'application/json' },
108
+ body: JSON.stringify({ direction: 'inbound', source: 'manual' }),
109
+ });
110
+
111
+ const result = (await response.json()) as ManualSyncResponse &
112
+ Record<string, unknown>;
113
+ if (!response.ok) {
114
+ throw new Error(
115
+ typeof result.error === 'string'
116
+ ? result.error
117
+ : 'Failed to sync calendars'
118
+ );
119
+ }
120
+
121
+ return result;
122
+ },
123
+ onSuccess: async (result) => {
124
+ await queryClient.invalidateQueries({
125
+ queryKey: ['calendar-sync-status', wsId],
126
+ });
127
+ await queryClient.invalidateQueries({
128
+ queryKey: ['databaseCalendarEvents', wsId],
129
+ exact: false,
130
+ });
131
+
132
+ if (!result.alreadyRunning) {
133
+ toast.success(t('calendar_sync_started') || 'Calendar sync started');
134
+ }
135
+ },
136
+ onError: (error: Error) => {
137
+ toast.error(error.message);
138
+ },
139
+ });
140
+
141
+ // Fetch connected accounts
142
+ const { data: accountsData, isLoading: isLoadingAccounts } = useQuery({
143
+ queryKey: ['calendar-accounts', wsId],
144
+ queryFn: async () => {
145
+ const response = await fetch(
146
+ `/api/v1/calendar/auth/accounts?wsId=${wsId}`,
147
+ { cache: 'no-store' }
148
+ );
149
+ if (!response.ok)
150
+ return {
151
+ accounts: [],
152
+ grouped: { google: [], microsoft: [] },
153
+ total: 0,
154
+ };
155
+ return response.json() as Promise<AccountsResponse>;
156
+ },
157
+ staleTime: 30_000,
158
+ });
159
+
160
+ const accounts = accountsData?.accounts || [];
161
+ const hasConnectedAccounts = accounts.length > 0;
162
+
163
+ const { data: syncStatusData } = useQuery({
164
+ queryKey: ['calendar-sync-status', wsId],
165
+ queryFn: async () => {
166
+ const response = await fetch(
167
+ `/api/v1/workspaces/${wsId}/calendar/sync-status`,
168
+ { cache: 'no-store' }
169
+ );
170
+ if (!response.ok) return null;
171
+ return response.json() as Promise<{
172
+ health: CalendarSyncHealth;
173
+ accountsSummary: { total: number; google: number; microsoft: number };
174
+ connectionsSummary: { total: number; enabled: number };
175
+ }>;
176
+ },
177
+ staleTime: 15_000,
178
+ });
179
+
180
+ const { data: defaultSourceData } = useQuery({
181
+ queryKey: ['calendar-default-source', wsId],
182
+ queryFn: () => getWorkspaceCalendarDefaultSource(wsId),
183
+ staleTime: 30_000,
184
+ });
185
+
186
+ const defaultSourceMutation = useMutation({
187
+ mutationFn: async (sourceId: string) => {
188
+ const option = defaultSourceData?.options.find(
189
+ (candidate) => candidate.id === sourceId
190
+ );
191
+ if (!option) throw new Error('Calendar source is unavailable');
192
+
193
+ return updateWorkspaceCalendarDefaultSource(
194
+ wsId,
195
+ sourceInputFromOption(option)
196
+ );
197
+ },
198
+ onSuccess: () => {
199
+ queryClient.invalidateQueries({
200
+ queryKey: ['calendar-default-source', wsId],
201
+ });
202
+ toast.success(
203
+ t('default_calendar_updated') || 'Default calendar updated'
204
+ );
205
+ },
206
+ onError: (error: Error) => {
207
+ toast.error(error.message || 'Failed to update default calendar');
208
+ },
209
+ });
210
+
211
+ // Fetch current user's email
212
+ const { data: userEmail } = useQuery({
213
+ queryKey: ['current-user-email'],
214
+ queryFn: async () => {
215
+ const supabase = createClient();
216
+ const {
217
+ data: { user },
218
+ } = await supabase.auth.getUser();
219
+ return user?.email || null;
220
+ },
221
+ staleTime: Infinity, // User email rarely changes
222
+ });
223
+
224
+ // Fetch workspace calendars (Tuturuuu native calendars)
225
+ const {
226
+ data: workspaceCalendarsData,
227
+ isLoading: isLoadingWorkspaceCalendars,
228
+ } = useQuery({
229
+ queryKey: ['workspace-calendars', wsId],
230
+ queryFn: async () => {
231
+ const response = await fetch(`/api/v1/workspaces/${wsId}/calendars`, {
232
+ cache: 'no-store',
233
+ });
234
+ if (!response.ok)
235
+ return { calendars: [], grouped: { system: [], custom: [] }, total: 0 };
236
+ return response.json() as Promise<WorkspaceCalendarsResponse>;
237
+ },
238
+ staleTime: 30_000,
239
+ });
240
+
241
+ const isLoadingCalendars = isLoadingAccounts || isLoadingWorkspaceCalendars;
242
+
243
+ const workspaceCalendars = workspaceCalendarsData?.calendars || [];
244
+ const systemCalendars = workspaceCalendarsData?.grouped?.system || [];
245
+ const customCalendars = workspaceCalendarsData?.grouped?.custom || [];
246
+
247
+ // Calendar color mapping helper
248
+ const getCalendarColor = (color: string): string => {
249
+ const colorMap: Record<string, string> = {
250
+ BLUE: '#3b82f6',
251
+ RED: '#ef4444',
252
+ GREEN: '#22c55e',
253
+ YELLOW: '#eab308',
254
+ ORANGE: '#f97316',
255
+ PURPLE: '#a855f7',
256
+ PINK: '#ec4899',
257
+ CYAN: '#06b6d4',
258
+ GRAY: '#6b7280',
259
+ };
260
+ return colorMap[color.toUpperCase()] || color;
261
+ };
262
+
263
+ // Calculate total enabled calendars count
264
+ const tuturuuuEnabledCount = workspaceCalendars.filter(
265
+ (c) => c.is_enabled
266
+ ).length;
267
+
268
+ // Toggle workspace calendar visibility
269
+ const toggleWorkspaceCalendarMutation = useMutation({
270
+ mutationFn: async ({
271
+ id,
272
+ is_enabled,
273
+ }: {
274
+ id: string;
275
+ is_enabled: boolean;
276
+ }) => {
277
+ const response = await fetch(`/api/v1/workspaces/${wsId}/calendars`, {
278
+ method: 'PATCH',
279
+ headers: { 'Content-Type': 'application/json' },
280
+ body: JSON.stringify({ id, is_enabled }),
281
+ });
282
+ if (!response.ok) throw new Error('Failed to toggle calendar');
283
+ return response.json();
284
+ },
285
+ onMutate: ({ id }) => {
286
+ setTogglingTuturuuuIds((prev) => new Set(prev).add(id));
287
+ },
288
+ onSuccess: () => {
289
+ queryClient.invalidateQueries({
290
+ queryKey: ['workspace-calendars', wsId],
291
+ });
292
+ },
293
+ onError: () =>
294
+ toast.error(t('calendar_toggle_failed') || 'Failed to toggle calendar'),
295
+ onSettled: (_, __, { id }) => {
296
+ setTogglingTuturuuuIds((prev) => {
297
+ const next = new Set(prev);
298
+ next.delete(id);
299
+ return next;
300
+ });
301
+ },
302
+ });
303
+
304
+ // Create custom calendar
305
+ const createCalendarMutation = useMutation({
306
+ mutationFn: async (data: { name: string; color?: string }) => {
307
+ const response = await fetch(`/api/v1/workspaces/${wsId}/calendars`, {
308
+ method: 'POST',
309
+ headers: { 'Content-Type': 'application/json' },
310
+ body: JSON.stringify(data),
311
+ });
312
+ if (!response.ok) throw new Error('Failed to create calendar');
313
+ return response.json();
314
+ },
315
+ onSuccess: () => {
316
+ toast.success(t('calendar_created') || 'Calendar created');
317
+ queryClient.invalidateQueries({
318
+ queryKey: ['workspace-calendars', wsId],
319
+ });
320
+ },
321
+ onError: () =>
322
+ toast.error(t('calendar_creation_failed') || 'Failed to create calendar'),
323
+ });
324
+
325
+ // Delete custom calendar
326
+ const deleteCalendarMutation = useMutation({
327
+ mutationFn: async (id: string) => {
328
+ const response = await fetch(
329
+ `/api/v1/workspaces/${wsId}/calendars?id=${id}`,
330
+ {
331
+ method: 'DELETE',
332
+ }
333
+ );
334
+ if (!response.ok) throw new Error('Failed to delete calendar');
335
+ return response.json();
336
+ },
337
+ onSuccess: () => {
338
+ toast.success(t('calendar_deleted') || 'Calendar deleted');
339
+ queryClient.invalidateQueries({
340
+ queryKey: ['workspace-calendars', wsId],
341
+ });
342
+ },
343
+ onError: () =>
344
+ toast.error(t('calendar_deletion_failed') || 'Failed to delete calendar'),
345
+ });
346
+
347
+ // Reset all calendar data
348
+ const resetCalendarDataMutation = useMutation({
349
+ mutationFn: async () => {
350
+ const response = await fetch(
351
+ `/api/v1/workspaces/${wsId}/calendars/reset`,
352
+ { method: 'POST' }
353
+ );
354
+ if (!response.ok) {
355
+ const error = await response.json();
356
+ throw new Error(error.error || 'Failed to reset calendar data');
357
+ }
358
+ return response.json();
359
+ },
360
+ onSuccess: () => {
361
+ toast.success(
362
+ t('calendar_data_reset') || 'Calendar data reset successfully'
363
+ );
364
+ // Clear the calendar connections immediately
365
+ setCalendarConnections([]);
366
+ // Refetch all calendar-related queries to ensure UI updates
367
+ queryClient.refetchQueries({
368
+ queryKey: ['workspace-calendars', wsId],
369
+ });
370
+ queryClient.refetchQueries({ queryKey: ['calendar-accounts', wsId] });
371
+ queryClient.refetchQueries({
372
+ queryKey: ['databaseCalendarEvents', wsId],
373
+ });
374
+ queryClient.refetchQueries({
375
+ queryKey: ['googleCalendarEvents', wsId],
376
+ });
377
+ queryClient.refetchQueries({
378
+ queryKey: ['calendar-connections'],
379
+ });
380
+ // Refresh the page to reload server data
381
+ router.refresh();
382
+ },
383
+ onError: (error: Error) => {
384
+ toast.error(
385
+ error.message ||
386
+ t('calendar_reset_failed') ||
387
+ 'Failed to reset calendar data'
388
+ );
389
+ },
390
+ });
391
+
392
+ // Google auth mutation
393
+ const googleAuthMutation = useMutation<AuthResponse, Error, void>({
394
+ mutationKey: ['calendar', 'google-auth', wsId],
395
+ mutationFn: async () => {
396
+ const response = await fetch(`/api/v1/calendar/auth?wsId=${wsId}`);
397
+ if (!response.ok) throw new Error('Failed to get auth URL');
398
+ return response.json();
399
+ },
400
+ onSuccess: (data) => {
401
+ if (data.authUrl) window.location.href = data.authUrl;
402
+ else toast.error(t('auth_url_invalid'));
403
+ },
404
+ onError: () => toast.error(t('google_auth_failed')),
405
+ });
406
+
407
+ // Microsoft auth mutation
408
+ const microsoftAuthMutation = useMutation<AuthResponse, Error, void>({
409
+ mutationKey: ['calendar', 'microsoft-auth', wsId],
410
+ mutationFn: async () => {
411
+ const response = await fetch(
412
+ `/api/v1/calendar/auth/microsoft?wsId=${wsId}`
413
+ );
414
+ if (!response.ok) throw new Error('Failed to get auth URL');
415
+ return response.json();
416
+ },
417
+ onSuccess: (data) => {
418
+ if (data.authUrl) window.location.href = data.authUrl;
419
+ else toast.error(t('auth_url_invalid'));
420
+ },
421
+ onError: () => toast.error(t('microsoft_auth_failed')),
422
+ });
423
+
424
+ // Disconnect account mutation
425
+ const disconnectMutation = useMutation({
426
+ mutationFn: async (accountId: string) => {
427
+ const response = await fetch(
428
+ `/api/v1/calendar/auth/accounts?accountId=${accountId}&wsId=${wsId}`,
429
+ { method: 'DELETE' }
430
+ );
431
+ if (!response.ok) throw new Error('Failed to disconnect');
432
+ return response.json();
433
+ },
434
+ onMutate: (accountId) => {
435
+ setDisconnectingId(accountId);
436
+ },
437
+ onSuccess: () => {
438
+ toast.success(t('account_disconnected'));
439
+ queryClient.invalidateQueries({ queryKey: ['calendar-accounts', wsId] });
440
+ },
441
+ onError: () => toast.error(t('failed_to_disconnect')),
442
+ onSettled: () => setDisconnectingId(null),
443
+ });
444
+
445
+ // Toggle calendar visibility - handles both existing connections and new calendars
446
+ const handleToggle = async (
447
+ calendarId: string,
448
+ currentState: boolean,
449
+ calendarData?: {
450
+ calendar_id: string;
451
+ calendar_name: string;
452
+ color: string | null;
453
+ connectionExists: boolean;
454
+ accountId?: string;
455
+ accessRole?: string;
456
+ }
457
+ ) => {
458
+ const newState = !currentState;
459
+ setTogglingIds((prev) => new Set(prev).add(calendarId));
460
+
461
+ try {
462
+ // If connection doesn't exist in database and we're enabling, create it
463
+ if (calendarData && !calendarData.connectionExists && newState) {
464
+ const response = await fetch('/api/v1/calendar/connections', {
465
+ method: 'POST',
466
+ headers: { 'Content-Type': 'application/json' },
467
+ body: JSON.stringify({
468
+ wsId,
469
+ calendarId: calendarData.calendar_id,
470
+ calendarName: calendarData.calendar_name,
471
+ color: calendarData.color,
472
+ isEnabled: true,
473
+ authTokenId: calendarData.accountId,
474
+ accessRole: calendarData.accessRole,
475
+ }),
476
+ });
477
+
478
+ if (!response.ok) {
479
+ // Handle 409 Conflict - calendar already exists, update it instead
480
+ if (response.status === 409) {
481
+ // Calendar already exists, make a PATCH request to enable it
482
+ const patchResponse = await fetch('/api/v1/calendar/connections', {
483
+ method: 'PATCH',
484
+ headers: { 'Content-Type': 'application/json' },
485
+ body: JSON.stringify({
486
+ // Use calendar_id to find the connection since we don't have the connection ID
487
+ calendarId: calendarData.calendar_id,
488
+ wsId,
489
+ authTokenId: calendarData.accountId,
490
+ isEnabled: true,
491
+ accessRole: calendarData.accessRole,
492
+ }),
493
+ });
494
+
495
+ if (patchResponse.ok) {
496
+ // Update local state immediately
497
+ updateCalendarConnection(calendarId, true);
498
+ queryClient.invalidateQueries({
499
+ queryKey: ['provider-calendar-list', wsId],
500
+ });
501
+ queryClient.invalidateQueries({
502
+ queryKey: ['calendar-connections', wsId],
503
+ });
504
+ toast.success(t('calendar_enabled'));
505
+ } else {
506
+ toast.error(t('toggle_failed'));
507
+ }
508
+ } else {
509
+ toast.error(t('toggle_failed'));
510
+ }
511
+ } else {
512
+ // Update local state immediately
513
+ updateCalendarConnection(calendarId, true);
514
+ // Invalidate queries to refresh the list
515
+ queryClient.invalidateQueries({
516
+ queryKey: ['provider-calendar-list', wsId],
517
+ });
518
+ queryClient.invalidateQueries({
519
+ queryKey: ['calendar-connections', wsId],
520
+ });
521
+ toast.success(t('calendar_enabled'));
522
+ }
523
+ } else {
524
+ // Update existing connection
525
+ updateCalendarConnection(calendarId, newState);
526
+
527
+ const response = await fetch('/api/v1/calendar/connections', {
528
+ method: 'PATCH',
529
+ headers: { 'Content-Type': 'application/json' },
530
+ body: JSON.stringify({ id: calendarId, isEnabled: newState }),
531
+ });
532
+
533
+ if (!response.ok) {
534
+ updateCalendarConnection(calendarId, currentState);
535
+ toast.error(t('toggle_failed'));
536
+ }
537
+ }
538
+ } catch {
539
+ if (calendarData?.connectionExists) {
540
+ updateCalendarConnection(calendarId, currentState);
541
+ }
542
+ toast.error(t('toggle_failed'));
543
+ } finally {
544
+ setTogglingIds((prev) => {
545
+ const next = new Set(prev);
546
+ next.delete(calendarId);
547
+ return next;
548
+ });
549
+ }
550
+ };
551
+
552
+ const toggleAccountExpanded = (accountId: string) => {
553
+ setExpandedAccounts((prev) => {
554
+ const next = new Set(prev);
555
+ if (next.has(accountId)) next.delete(accountId);
556
+ else next.add(accountId);
557
+ return next;
558
+ });
559
+ };
560
+
561
+ const enabledCount =
562
+ calendarConnections.filter((c) => c.is_enabled).length +
563
+ tuturuuuEnabledCount;
564
+ const syncHealth = syncStatusData?.health;
565
+ const manualSyncDisabled = useMemo(() => {
566
+ if (syncMutation.isPending || isSyncing) {
567
+ return true;
568
+ }
569
+
570
+ if (syncHealth?.currentlyRunning) {
571
+ return true;
572
+ }
573
+
574
+ if ((syncHealth?.retryAfterSeconds ?? 0) > 0) {
575
+ return true;
576
+ }
577
+
578
+ return false;
579
+ }, [
580
+ isSyncing,
581
+ syncHealth?.currentlyRunning,
582
+ syncHealth?.retryAfterSeconds,
583
+ syncMutation.isPending,
584
+ ]);
585
+ const syncStatusStyles =
586
+ syncHealth?.state === 'syncing'
587
+ ? 'bg-dynamic-blue/10 text-dynamic-blue'
588
+ : syncHealth?.state === 'healthy'
589
+ ? 'bg-dynamic-green/10 text-dynamic-green'
590
+ : syncHealth?.state === 'disconnected'
591
+ ? 'bg-muted text-muted-foreground'
592
+ : syncHealth?.state === 'degraded'
593
+ ? 'bg-dynamic-orange/10 text-dynamic-orange'
594
+ : 'bg-dynamic-red/10 text-dynamic-red';
595
+
596
+ // Fetch Google calendars from API (must be before any conditional returns)
597
+ const { data: googleCalendarsData } = useQuery({
598
+ queryKey: ['provider-calendar-list', wsId],
599
+ enabled: hasConnectedAccounts,
600
+ queryFn: async () => {
601
+ const response = await fetch(
602
+ `/api/v1/calendar/auth/provider-calendars?wsId=${wsId}`,
603
+ { cache: 'no-store' }
604
+ );
605
+ if (!response.ok) return { calendars: [], byAccount: {} };
606
+ return response.json() as Promise<{
607
+ calendars: ProviderCalendar[];
608
+ byAccount: Record<string, ProviderCalendar[]>;
609
+ }>;
610
+ },
611
+ staleTime: 30_000,
612
+ });
613
+
614
+ const calendarsByAccountFromAPI = googleCalendarsData?.byAccount || {};
615
+
616
+ // Merge API calendars with existing connections for visibility state
617
+ const calendarsByAccount = accounts.reduce(
618
+ (acc, account) => {
619
+ const apiCalendars = calendarsByAccountFromAPI[account.id] || [];
620
+
621
+ // Map API calendars to connection-like objects, merging with existing connections
622
+ acc[account.id] = apiCalendars.map((apiCal) => {
623
+ // Find existing connection for this calendar
624
+ const existingConnection = calendarConnections.find(
625
+ (conn) =>
626
+ conn.calendar_id === apiCal.id && conn.auth_token_id === account.id
627
+ );
628
+
629
+ return {
630
+ id: existingConnection?.id || apiCal.id,
631
+ ws_id: wsId,
632
+ calendar_id: apiCal.id,
633
+ calendar_name: apiCal.name,
634
+ is_enabled: existingConnection?.is_enabled ?? false, // Default to hidden if no connection exists
635
+ color: existingConnection?.color || apiCal.backgroundColor,
636
+ isFromAPI: true,
637
+ connectionExists: !!existingConnection,
638
+ accountId: account.id,
639
+ accessRole: apiCal.accessRole,
640
+ };
641
+ });
642
+
643
+ return acc;
644
+ },
645
+ {} as Record<
646
+ string,
647
+ Array<{
648
+ id: string;
649
+ ws_id: string;
650
+ calendar_id: string;
651
+ calendar_name: string;
652
+ is_enabled: boolean;
653
+ color: string | null;
654
+ isFromAPI: boolean;
655
+ connectionExists: boolean;
656
+ accountId: string;
657
+ accessRole: string;
658
+ }>
659
+ >
660
+ );
661
+
662
+ // Note: Removed early return - we always show the full UI now so Tuturuuu calendars are visible
663
+
664
+ return {
665
+ accounts,
666
+ calendarConnections,
667
+ calendarsByAccount,
668
+ createCalendarMutation,
669
+ customCalendars,
670
+ defaultSourceData,
671
+ defaultSourceMutation,
672
+ deleteCalendarMutation,
673
+ disconnectingId,
674
+ disconnectMutation,
675
+ enabledCount,
676
+ expandedAccounts,
677
+ expandedPopoverAccounts,
678
+ getCalendarColor,
679
+ googleAuthMutation,
680
+ handleToggle,
681
+ hasConnectedAccounts,
682
+ isLoadingCalendars,
683
+ isSyncing,
684
+ manualSyncDisabled,
685
+ microsoftAuthMutation,
686
+ newCalendarName,
687
+ resetCalendarDataMutation,
688
+ setExpandedPopoverAccounts,
689
+ setNewCalendarName,
690
+ setShowCreateCalendarDialog,
691
+ showCreateCalendarDialog,
692
+ syncHealth,
693
+ syncMutation,
694
+ syncStatusStyles,
695
+ syncToTuturuuu,
696
+ systemCalendars,
697
+ t,
698
+ togglingIds,
699
+ togglingTuturuuuIds,
700
+ toggleAccountExpanded,
701
+ toggleWorkspaceCalendarMutation,
702
+ tuturuuuEnabledCount,
703
+ userEmail,
704
+ workspaceCalendars,
705
+ wsId,
706
+ };
707
+ }
708
+
709
+ export type CalendarConnectionsManagerState = ReturnType<
710
+ typeof useCalendarConnectionsManager
711
+ >;