@tuturuuu/ui 0.2.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.
- package/CHANGELOG.md +53 -0
- package/package.json +79 -67
- package/src/components/ui/__tests__/avatar.test.tsx +8 -5
- package/src/components/ui/calendar-app/components/calendar-connections-compact.tsx +414 -0
- package/src/components/ui/calendar-app/components/calendar-connections-manager.tsx +5 -1
- package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +529 -0
- package/src/components/ui/calendar-app/components/calendar-connections-unified.tsx +26 -1429
- package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +711 -0
- package/src/components/ui/chart.test.tsx +29 -0
- package/src/components/ui/chart.tsx +12 -3
- package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +24 -1
- package/src/components/ui/custom/__tests__/tuturuuu-logo.test.ts +12 -3
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +39 -0
- package/src/components/ui/custom/common-footer.tsx +16 -1
- package/src/components/ui/custom/production-indicator.tsx +1 -1
- package/src/components/ui/custom/settings/sidebar-settings.tsx +1 -1
- package/src/components/ui/custom/settings/task-settings.tsx +18 -0
- package/src/components/ui/custom/settings-dialog-shell.tsx +38 -23
- package/src/components/ui/custom/sidebar-context-compile-graph.test.ts +60 -0
- package/src/components/ui/custom/sidebar-context.tsx +61 -61
- package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +123 -0
- package/src/components/ui/custom/tuturuuu-logo-urls.ts +6 -0
- package/src/components/ui/custom/tuturuuu-logo.tsx +25 -7
- package/src/components/ui/custom/workspace-select-helpers.ts +20 -0
- package/src/components/ui/custom/workspace-select.tsx +33 -12
- package/src/components/ui/finance/invoices/components/invoice-checkout-summary.tsx +7 -1
- package/src/components/ui/finance/invoices/components/invoice-payment-settings.tsx +3 -0
- package/src/components/ui/finance/invoices/components/invoice-products-permission-warning.tsx +58 -0
- package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +12 -20
- package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +10 -9
- package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-content.ts +10 -5
- package/src/components/ui/finance/invoices/hooks.ts +75 -20
- package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +137 -0
- package/src/components/ui/finance/invoices/new-invoice-page.tsx +86 -37
- package/src/components/ui/finance/invoices/product-selection.test.tsx +8 -26
- package/src/components/ui/finance/invoices/product-selection.tsx +2 -10
- package/src/components/ui/finance/invoices/standard-invoice.tsx +88 -26
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +154 -46
- package/src/components/ui/finance/invoices/utils.test.ts +50 -0
- package/src/components/ui/finance/invoices/utils.ts +75 -17
- package/src/components/ui/finance/shared/finance-display-amount.tsx +3 -1
- package/src/components/ui/finance/shared/finance-permission-warning-dialog.test.tsx +34 -0
- package/src/components/ui/finance/shared/finance-permission-warning-dialog.tsx +157 -0
- package/src/components/ui/finance/transactions/form-basic-tab.tsx +8 -0
- package/src/components/ui/finance/transactions/form-more-tab.tsx +8 -0
- package/src/components/ui/finance/transactions/form-types.ts +2 -0
- package/src/components/ui/finance/transactions/form.test.tsx +43 -0
- package/src/components/ui/finance/transactions/form.tsx +60 -0
- package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +27 -0
- package/src/components/ui/finance/transactions/transactions-create-summary.tsx +13 -1
- package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +4 -0
- package/src/components/ui/finance/transactions/transactions-page.tsx +23 -1
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +5 -0
- package/src/components/ui/legacy/calendar/calendar-content.tsx +9 -1
- package/src/components/ui/legacy/calendar/event-modal.tsx +146 -2
- package/src/components/ui/legacy/calendar/event-preview-popover.tsx +200 -0
- package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +76 -0
- package/src/components/ui/legacy/calendar/smart-calendar.tsx +13 -1
- package/src/components/ui/legacy/meet/page.test.ts +180 -0
- package/src/components/ui/legacy/meet/page.tsx +87 -39
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +79 -25
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-external-workspaces.test.tsx +392 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.test.tsx +57 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.tsx +106 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +106 -161
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-assignees.ts +96 -150
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-labels.ts +63 -79
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-projects.ts +64 -83
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +115 -155
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-utils.ts +319 -2
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +8 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +63 -37
- package/src/components/ui/tu-do/boards/boardId/kanban/kanban-column-collapse.ts +16 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +46 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +5 -3
- package/src/components/ui/tu-do/boards/boardId/kanban.tsx +19 -7
- package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-menus.test.tsx +181 -2
- package/src/components/ui/tu-do/boards/boardId/menus/index.ts +1 -0
- package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-menu.tsx +463 -0
- package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-utils.ts +109 -0
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +4 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardCheckbox.tsx +6 -3
- package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardDates.tsx +26 -9
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-checkbox-style.ts +39 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.test.ts +43 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +33 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.test.ts +31 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.ts +9 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.test.tsx +124 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.tsx +88 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +151 -76
- package/src/components/ui/tu-do/boards/boardId/task-card/task-scheduling-badge.tsx +174 -0
- package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +34 -13
- package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +54 -1
- package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +158 -0
- package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +5 -2
- package/src/components/ui/tu-do/shared/board-client.tsx +12 -2
- package/src/components/ui/tu-do/shared/board-views.tsx +195 -328
- package/src/components/ui/tu-do/shared/list-view.tsx +18 -8
- package/src/components/ui/tu-do/shared/task-due-date-visibility.test.ts +72 -0
- package/src/components/ui/tu-do/shared/task-due-date-visibility.ts +38 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +6 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +2 -2
- package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +33 -0
- package/src/hooks/__tests__/use-calendar-readonly.test.tsx +74 -3
- package/src/hooks/__tests__/use-task-actions.test.tsx +118 -0
- package/src/hooks/__tests__/use-user-config.test.tsx +65 -0
- package/src/hooks/__tests__/use-workspace-presence.test.tsx +1 -1
- package/src/hooks/use-calendar-sync.tsx +22 -277
- package/src/hooks/use-calendar.tsx +95 -525
- package/src/hooks/use-task-actions.ts +43 -117
- package/src/hooks/use-user-config.ts +1 -1
- package/src/hooks/use-workspace-config.ts +6 -2
- package/src/hooks/use-workspace-presence.ts +1 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-bar.tsx +0 -94
|
@@ -1,1433 +1,30 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { CalendarConnectionsCompact } from './calendar-connections-compact';
|
|
4
|
+
import { CalendarConnectionsSettingsContent } from './calendar-connections-settings-content';
|
|
4
5
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
AlertDialogCancel,
|
|
30
|
-
AlertDialogContent,
|
|
31
|
-
AlertDialogDescription,
|
|
32
|
-
AlertDialogFooter,
|
|
33
|
-
AlertDialogHeader,
|
|
34
|
-
AlertDialogTitle,
|
|
35
|
-
AlertDialogTrigger,
|
|
36
|
-
} from '../../alert-dialog';
|
|
37
|
-
import { Badge } from '../../badge';
|
|
38
|
-
import { Button } from '../../button';
|
|
39
|
-
import {
|
|
40
|
-
Collapsible,
|
|
41
|
-
CollapsibleContent,
|
|
42
|
-
CollapsibleTrigger,
|
|
43
|
-
} from '../../collapsible';
|
|
44
|
-
import {
|
|
45
|
-
Dialog,
|
|
46
|
-
DialogContent,
|
|
47
|
-
DialogDescription,
|
|
48
|
-
DialogFooter,
|
|
49
|
-
DialogHeader,
|
|
50
|
-
DialogTitle,
|
|
51
|
-
DialogTrigger,
|
|
52
|
-
} from '../../dialog';
|
|
53
|
-
import { Input } from '../../input';
|
|
54
|
-
import { Popover, PopoverContent, PopoverTrigger } from '../../popover';
|
|
55
|
-
import { Separator } from '../../separator';
|
|
56
|
-
import { toast } from '../../sonner';
|
|
57
|
-
import { Switch } from '../../switch';
|
|
58
|
-
import type {
|
|
59
|
-
AuthResponse,
|
|
60
|
-
CalendarSyncHealth,
|
|
61
|
-
ConnectedAccount,
|
|
62
|
-
ProviderCalendar,
|
|
63
|
-
} from './calendar-types';
|
|
64
|
-
|
|
65
|
-
interface AccountsResponse {
|
|
66
|
-
accounts: ConnectedAccount[];
|
|
67
|
-
grouped: {
|
|
68
|
-
google: ConnectedAccount[];
|
|
69
|
-
microsoft: ConnectedAccount[];
|
|
70
|
-
};
|
|
71
|
-
total: number;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
interface WorkspaceCalendar {
|
|
75
|
-
id: string;
|
|
76
|
-
ws_id: string;
|
|
77
|
-
name: string;
|
|
78
|
-
description: string | null;
|
|
79
|
-
color: string | null;
|
|
80
|
-
calendar_type: 'primary' | 'tasks' | 'habits' | 'custom';
|
|
81
|
-
is_system: boolean;
|
|
82
|
-
is_enabled: boolean;
|
|
83
|
-
position: number;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
interface WorkspaceCalendarsResponse {
|
|
87
|
-
calendars: WorkspaceCalendar[];
|
|
88
|
-
grouped: {
|
|
89
|
-
system: WorkspaceCalendar[];
|
|
90
|
-
custom: WorkspaceCalendar[];
|
|
91
|
-
};
|
|
92
|
-
total: number;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
interface ManualSyncResponse {
|
|
96
|
-
ok: boolean;
|
|
97
|
-
alreadyRunning?: boolean;
|
|
98
|
-
code?: string;
|
|
99
|
-
error?: string;
|
|
100
|
-
retryAfterSeconds?: number | null;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export default function CalendarConnectionsUnified({ wsId }: { wsId: string }) {
|
|
104
|
-
const t = useTranslations('calendar');
|
|
105
|
-
const router = useRouter();
|
|
106
|
-
const queryClient = useQueryClient();
|
|
107
|
-
const [disconnectingId, setDisconnectingId] = useState<string | null>(null);
|
|
108
|
-
const [togglingIds, setTogglingIds] = useState<Set<string>>(new Set());
|
|
109
|
-
const [togglingTuturuuuIds, setTogglingTuturuuuIds] = useState<Set<string>>(
|
|
110
|
-
new Set()
|
|
111
|
-
);
|
|
112
|
-
const [expandedAccounts, setExpandedAccounts] = useState<Set<string>>(
|
|
113
|
-
new Set()
|
|
114
|
-
);
|
|
115
|
-
const [expandedPopoverAccounts, setExpandedPopoverAccounts] = useState<
|
|
116
|
-
Set<string>
|
|
117
|
-
>(
|
|
118
|
-
new Set(['tuturuuu']) // Tuturuuu expanded by default
|
|
119
|
-
);
|
|
120
|
-
const [showCreateCalendarDialog, setShowCreateCalendarDialog] =
|
|
121
|
-
useState(false);
|
|
122
|
-
const [newCalendarName, setNewCalendarName] = useState('');
|
|
123
|
-
|
|
124
|
-
const {
|
|
125
|
-
calendarConnections,
|
|
126
|
-
updateCalendarConnection,
|
|
127
|
-
setCalendarConnections,
|
|
128
|
-
syncToTuturuuu,
|
|
129
|
-
isSyncing,
|
|
130
|
-
} = useCalendarSync();
|
|
131
|
-
|
|
132
|
-
const syncMutation = useMutation({
|
|
133
|
-
mutationFn: async () => {
|
|
134
|
-
const response = await fetch(`/api/v1/workspaces/${wsId}/calendar/sync`, {
|
|
135
|
-
method: 'POST',
|
|
136
|
-
headers: { 'Content-Type': 'application/json' },
|
|
137
|
-
body: JSON.stringify({ direction: 'inbound', source: 'manual' }),
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
const result = (await response.json()) as ManualSyncResponse &
|
|
141
|
-
Record<string, unknown>;
|
|
142
|
-
if (!response.ok) {
|
|
143
|
-
throw new Error(
|
|
144
|
-
typeof result.error === 'string'
|
|
145
|
-
? result.error
|
|
146
|
-
: 'Failed to sync calendars'
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return result;
|
|
151
|
-
},
|
|
152
|
-
onSuccess: async (result) => {
|
|
153
|
-
await queryClient.invalidateQueries({
|
|
154
|
-
queryKey: ['calendar-sync-status', wsId],
|
|
155
|
-
});
|
|
156
|
-
await queryClient.invalidateQueries({
|
|
157
|
-
queryKey: ['databaseCalendarEvents', wsId],
|
|
158
|
-
exact: false,
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
if (!result.alreadyRunning) {
|
|
162
|
-
toast.success(t('calendar_sync_started') || 'Calendar sync started');
|
|
163
|
-
}
|
|
164
|
-
},
|
|
165
|
-
onError: (error: Error) => {
|
|
166
|
-
toast.error(error.message);
|
|
167
|
-
},
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
// Fetch connected accounts
|
|
171
|
-
const { data: accountsData, isLoading: isLoadingAccounts } = useQuery({
|
|
172
|
-
queryKey: ['calendar-accounts', wsId],
|
|
173
|
-
queryFn: async () => {
|
|
174
|
-
const response = await fetch(
|
|
175
|
-
`/api/v1/calendar/auth/accounts?wsId=${wsId}`,
|
|
176
|
-
{ cache: 'no-store' }
|
|
177
|
-
);
|
|
178
|
-
if (!response.ok)
|
|
179
|
-
return {
|
|
180
|
-
accounts: [],
|
|
181
|
-
grouped: { google: [], microsoft: [] },
|
|
182
|
-
total: 0,
|
|
183
|
-
};
|
|
184
|
-
return response.json() as Promise<AccountsResponse>;
|
|
185
|
-
},
|
|
186
|
-
staleTime: 30_000,
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
const accounts = accountsData?.accounts || [];
|
|
190
|
-
const hasConnectedAccounts = accounts.length > 0;
|
|
191
|
-
|
|
192
|
-
const { data: syncStatusData } = useQuery({
|
|
193
|
-
queryKey: ['calendar-sync-status', wsId],
|
|
194
|
-
queryFn: async () => {
|
|
195
|
-
const response = await fetch(
|
|
196
|
-
`/api/v1/workspaces/${wsId}/calendar/sync-status`,
|
|
197
|
-
{ cache: 'no-store' }
|
|
198
|
-
);
|
|
199
|
-
if (!response.ok) return null;
|
|
200
|
-
return response.json() as Promise<{
|
|
201
|
-
health: CalendarSyncHealth;
|
|
202
|
-
accountsSummary: { total: number; google: number; microsoft: number };
|
|
203
|
-
connectionsSummary: { total: number; enabled: number };
|
|
204
|
-
}>;
|
|
205
|
-
},
|
|
206
|
-
staleTime: 15_000,
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// Fetch current user's email
|
|
210
|
-
const { data: userEmail } = useQuery({
|
|
211
|
-
queryKey: ['current-user-email'],
|
|
212
|
-
queryFn: async () => {
|
|
213
|
-
const supabase = createClient();
|
|
214
|
-
const {
|
|
215
|
-
data: { user },
|
|
216
|
-
} = await supabase.auth.getUser();
|
|
217
|
-
return user?.email || null;
|
|
218
|
-
},
|
|
219
|
-
staleTime: Infinity, // User email rarely changes
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
// Fetch workspace calendars (Tuturuuu native calendars)
|
|
223
|
-
const {
|
|
224
|
-
data: workspaceCalendarsData,
|
|
225
|
-
isLoading: isLoadingWorkspaceCalendars,
|
|
226
|
-
} = useQuery({
|
|
227
|
-
queryKey: ['workspace-calendars', wsId],
|
|
228
|
-
queryFn: async () => {
|
|
229
|
-
const response = await fetch(`/api/v1/workspaces/${wsId}/calendars`, {
|
|
230
|
-
cache: 'no-store',
|
|
231
|
-
});
|
|
232
|
-
if (!response.ok)
|
|
233
|
-
return { calendars: [], grouped: { system: [], custom: [] }, total: 0 };
|
|
234
|
-
return response.json() as Promise<WorkspaceCalendarsResponse>;
|
|
235
|
-
},
|
|
236
|
-
staleTime: 30_000,
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
const isLoadingCalendars = isLoadingAccounts || isLoadingWorkspaceCalendars;
|
|
240
|
-
|
|
241
|
-
const workspaceCalendars = workspaceCalendarsData?.calendars || [];
|
|
242
|
-
const systemCalendars = workspaceCalendarsData?.grouped?.system || [];
|
|
243
|
-
const customCalendars = workspaceCalendarsData?.grouped?.custom || [];
|
|
244
|
-
|
|
245
|
-
// Calendar color mapping helper
|
|
246
|
-
const getCalendarColor = (color: string): string => {
|
|
247
|
-
const colorMap: Record<string, string> = {
|
|
248
|
-
BLUE: '#3b82f6',
|
|
249
|
-
RED: '#ef4444',
|
|
250
|
-
GREEN: '#22c55e',
|
|
251
|
-
YELLOW: '#eab308',
|
|
252
|
-
ORANGE: '#f97316',
|
|
253
|
-
PURPLE: '#a855f7',
|
|
254
|
-
PINK: '#ec4899',
|
|
255
|
-
CYAN: '#06b6d4',
|
|
256
|
-
GRAY: '#6b7280',
|
|
257
|
-
};
|
|
258
|
-
return colorMap[color.toUpperCase()] || color;
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
// Calculate total enabled calendars count
|
|
262
|
-
const tuturuuuEnabledCount = workspaceCalendars.filter(
|
|
263
|
-
(c) => c.is_enabled
|
|
264
|
-
).length;
|
|
265
|
-
|
|
266
|
-
// Toggle workspace calendar visibility
|
|
267
|
-
const toggleWorkspaceCalendarMutation = useMutation({
|
|
268
|
-
mutationFn: async ({
|
|
269
|
-
id,
|
|
270
|
-
is_enabled,
|
|
271
|
-
}: {
|
|
272
|
-
id: string;
|
|
273
|
-
is_enabled: boolean;
|
|
274
|
-
}) => {
|
|
275
|
-
const response = await fetch(`/api/v1/workspaces/${wsId}/calendars`, {
|
|
276
|
-
method: 'PATCH',
|
|
277
|
-
headers: { 'Content-Type': 'application/json' },
|
|
278
|
-
body: JSON.stringify({ id, is_enabled }),
|
|
279
|
-
});
|
|
280
|
-
if (!response.ok) throw new Error('Failed to toggle calendar');
|
|
281
|
-
return response.json();
|
|
282
|
-
},
|
|
283
|
-
onMutate: ({ id }) => {
|
|
284
|
-
setTogglingTuturuuuIds((prev) => new Set(prev).add(id));
|
|
285
|
-
},
|
|
286
|
-
onSuccess: () => {
|
|
287
|
-
queryClient.invalidateQueries({
|
|
288
|
-
queryKey: ['workspace-calendars', wsId],
|
|
289
|
-
});
|
|
290
|
-
},
|
|
291
|
-
onError: () =>
|
|
292
|
-
toast.error(t('calendar_toggle_failed') || 'Failed to toggle calendar'),
|
|
293
|
-
onSettled: (_, __, { id }) => {
|
|
294
|
-
setTogglingTuturuuuIds((prev) => {
|
|
295
|
-
const next = new Set(prev);
|
|
296
|
-
next.delete(id);
|
|
297
|
-
return next;
|
|
298
|
-
});
|
|
299
|
-
},
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
// Create custom calendar
|
|
303
|
-
const createCalendarMutation = useMutation({
|
|
304
|
-
mutationFn: async (data: { name: string; color?: string }) => {
|
|
305
|
-
const response = await fetch(`/api/v1/workspaces/${wsId}/calendars`, {
|
|
306
|
-
method: 'POST',
|
|
307
|
-
headers: { 'Content-Type': 'application/json' },
|
|
308
|
-
body: JSON.stringify(data),
|
|
309
|
-
});
|
|
310
|
-
if (!response.ok) throw new Error('Failed to create calendar');
|
|
311
|
-
return response.json();
|
|
312
|
-
},
|
|
313
|
-
onSuccess: () => {
|
|
314
|
-
toast.success(t('calendar_created') || 'Calendar created');
|
|
315
|
-
queryClient.invalidateQueries({
|
|
316
|
-
queryKey: ['workspace-calendars', wsId],
|
|
317
|
-
});
|
|
318
|
-
},
|
|
319
|
-
onError: () =>
|
|
320
|
-
toast.error(t('calendar_creation_failed') || 'Failed to create calendar'),
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
// Delete custom calendar
|
|
324
|
-
const deleteCalendarMutation = useMutation({
|
|
325
|
-
mutationFn: async (id: string) => {
|
|
326
|
-
const response = await fetch(
|
|
327
|
-
`/api/v1/workspaces/${wsId}/calendars?id=${id}`,
|
|
328
|
-
{
|
|
329
|
-
method: 'DELETE',
|
|
330
|
-
}
|
|
331
|
-
);
|
|
332
|
-
if (!response.ok) throw new Error('Failed to delete calendar');
|
|
333
|
-
return response.json();
|
|
334
|
-
},
|
|
335
|
-
onSuccess: () => {
|
|
336
|
-
toast.success(t('calendar_deleted') || 'Calendar deleted');
|
|
337
|
-
queryClient.invalidateQueries({
|
|
338
|
-
queryKey: ['workspace-calendars', wsId],
|
|
339
|
-
});
|
|
340
|
-
},
|
|
341
|
-
onError: () =>
|
|
342
|
-
toast.error(t('calendar_deletion_failed') || 'Failed to delete calendar'),
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
// Reset all calendar data
|
|
346
|
-
const resetCalendarDataMutation = useMutation({
|
|
347
|
-
mutationFn: async () => {
|
|
348
|
-
const response = await fetch(
|
|
349
|
-
`/api/v1/workspaces/${wsId}/calendars/reset`,
|
|
350
|
-
{ method: 'POST' }
|
|
351
|
-
);
|
|
352
|
-
if (!response.ok) {
|
|
353
|
-
const error = await response.json();
|
|
354
|
-
throw new Error(error.error || 'Failed to reset calendar data');
|
|
355
|
-
}
|
|
356
|
-
return response.json();
|
|
357
|
-
},
|
|
358
|
-
onSuccess: (data) => {
|
|
359
|
-
toast.success(
|
|
360
|
-
t('calendar_data_reset') || 'Calendar data reset successfully'
|
|
361
|
-
);
|
|
362
|
-
// Clear the calendar connections immediately
|
|
363
|
-
setCalendarConnections([]);
|
|
364
|
-
// Refetch all calendar-related queries to ensure UI updates
|
|
365
|
-
queryClient.refetchQueries({
|
|
366
|
-
queryKey: ['workspace-calendars', wsId],
|
|
367
|
-
});
|
|
368
|
-
queryClient.refetchQueries({ queryKey: ['calendar-accounts', wsId] });
|
|
369
|
-
queryClient.refetchQueries({
|
|
370
|
-
queryKey: ['databaseCalendarEvents', wsId],
|
|
371
|
-
});
|
|
372
|
-
queryClient.refetchQueries({
|
|
373
|
-
queryKey: ['googleCalendarEvents', wsId],
|
|
374
|
-
});
|
|
375
|
-
queryClient.refetchQueries({
|
|
376
|
-
queryKey: ['calendar-connections'],
|
|
377
|
-
});
|
|
378
|
-
// Refresh the page to reload server data
|
|
379
|
-
router.refresh();
|
|
380
|
-
console.log('Reset results:', data);
|
|
381
|
-
},
|
|
382
|
-
onError: (error: Error) => {
|
|
383
|
-
toast.error(
|
|
384
|
-
error.message ||
|
|
385
|
-
t('calendar_reset_failed') ||
|
|
386
|
-
'Failed to reset calendar data'
|
|
387
|
-
);
|
|
388
|
-
},
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
// Google auth mutation
|
|
392
|
-
const googleAuthMutation = useMutation<AuthResponse, Error, void>({
|
|
393
|
-
mutationKey: ['calendar', 'google-auth', wsId],
|
|
394
|
-
mutationFn: async () => {
|
|
395
|
-
const response = await fetch(`/api/v1/calendar/auth?wsId=${wsId}`);
|
|
396
|
-
if (!response.ok) throw new Error('Failed to get auth URL');
|
|
397
|
-
return response.json();
|
|
398
|
-
},
|
|
399
|
-
onSuccess: (data) => {
|
|
400
|
-
if (data.authUrl) window.location.href = data.authUrl;
|
|
401
|
-
else toast.error(t('auth_url_invalid'));
|
|
402
|
-
},
|
|
403
|
-
onError: () => toast.error(t('google_auth_failed')),
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
// Microsoft auth mutation
|
|
407
|
-
const microsoftAuthMutation = useMutation<AuthResponse, Error, void>({
|
|
408
|
-
mutationKey: ['calendar', 'microsoft-auth', wsId],
|
|
409
|
-
mutationFn: async () => {
|
|
410
|
-
const response = await fetch(
|
|
411
|
-
`/api/v1/calendar/auth/microsoft?wsId=${wsId}`
|
|
412
|
-
);
|
|
413
|
-
if (!response.ok) throw new Error('Failed to get auth URL');
|
|
414
|
-
return response.json();
|
|
415
|
-
},
|
|
416
|
-
onSuccess: (data) => {
|
|
417
|
-
if (data.authUrl) window.location.href = data.authUrl;
|
|
418
|
-
else toast.error(t('auth_url_invalid'));
|
|
419
|
-
},
|
|
420
|
-
onError: () => toast.error(t('microsoft_auth_failed')),
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
// Disconnect account mutation
|
|
424
|
-
const disconnectMutation = useMutation({
|
|
425
|
-
mutationFn: async (accountId: string) => {
|
|
426
|
-
const response = await fetch(
|
|
427
|
-
`/api/v1/calendar/auth/accounts?accountId=${accountId}&wsId=${wsId}`,
|
|
428
|
-
{ method: 'DELETE' }
|
|
429
|
-
);
|
|
430
|
-
if (!response.ok) throw new Error('Failed to disconnect');
|
|
431
|
-
return response.json();
|
|
432
|
-
},
|
|
433
|
-
onMutate: (accountId) => {
|
|
434
|
-
setDisconnectingId(accountId);
|
|
435
|
-
},
|
|
436
|
-
onSuccess: () => {
|
|
437
|
-
toast.success(t('account_disconnected'));
|
|
438
|
-
queryClient.invalidateQueries({ queryKey: ['calendar-accounts', wsId] });
|
|
439
|
-
},
|
|
440
|
-
onError: () => toast.error(t('failed_to_disconnect')),
|
|
441
|
-
onSettled: () => setDisconnectingId(null),
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
// Toggle calendar visibility - handles both existing connections and new calendars
|
|
445
|
-
const handleToggle = async (
|
|
446
|
-
calendarId: string,
|
|
447
|
-
currentState: boolean,
|
|
448
|
-
calendarData?: {
|
|
449
|
-
calendar_id: string;
|
|
450
|
-
calendar_name: string;
|
|
451
|
-
color: string | null;
|
|
452
|
-
connectionExists: boolean;
|
|
453
|
-
accountId?: string;
|
|
454
|
-
}
|
|
455
|
-
) => {
|
|
456
|
-
const newState = !currentState;
|
|
457
|
-
setTogglingIds((prev) => new Set(prev).add(calendarId));
|
|
458
|
-
|
|
459
|
-
try {
|
|
460
|
-
// If connection doesn't exist in database and we're enabling, create it
|
|
461
|
-
if (calendarData && !calendarData.connectionExists && newState) {
|
|
462
|
-
const response = await fetch('/api/v1/calendar/connections', {
|
|
463
|
-
method: 'POST',
|
|
464
|
-
headers: { 'Content-Type': 'application/json' },
|
|
465
|
-
body: JSON.stringify({
|
|
466
|
-
wsId,
|
|
467
|
-
calendarId: calendarData.calendar_id,
|
|
468
|
-
calendarName: calendarData.calendar_name,
|
|
469
|
-
color: calendarData.color,
|
|
470
|
-
isEnabled: true,
|
|
471
|
-
authTokenId: calendarData.accountId,
|
|
472
|
-
}),
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
if (!response.ok) {
|
|
476
|
-
// Handle 409 Conflict - calendar already exists, update it instead
|
|
477
|
-
if (response.status === 409) {
|
|
478
|
-
// Calendar already exists, make a PATCH request to enable it
|
|
479
|
-
const patchResponse = await fetch('/api/v1/calendar/connections', {
|
|
480
|
-
method: 'PATCH',
|
|
481
|
-
headers: { 'Content-Type': 'application/json' },
|
|
482
|
-
body: JSON.stringify({
|
|
483
|
-
// Use calendar_id to find the connection since we don't have the connection ID
|
|
484
|
-
calendarId: calendarData.calendar_id,
|
|
485
|
-
wsId,
|
|
486
|
-
isEnabled: true,
|
|
487
|
-
}),
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
if (patchResponse.ok) {
|
|
491
|
-
// Update local state immediately
|
|
492
|
-
updateCalendarConnection(calendarId, true);
|
|
493
|
-
queryClient.invalidateQueries({
|
|
494
|
-
queryKey: ['provider-calendar-list', wsId],
|
|
495
|
-
});
|
|
496
|
-
queryClient.invalidateQueries({
|
|
497
|
-
queryKey: ['calendar-connections', wsId],
|
|
498
|
-
});
|
|
499
|
-
toast.success(t('calendar_enabled'));
|
|
500
|
-
} else {
|
|
501
|
-
toast.error(t('toggle_failed'));
|
|
502
|
-
}
|
|
503
|
-
} else {
|
|
504
|
-
const errorText = await response.text().catch(() => '');
|
|
505
|
-
console.error('Failed to create connection:', {
|
|
506
|
-
status: response.status,
|
|
507
|
-
statusText: response.statusText,
|
|
508
|
-
rawText: errorText,
|
|
509
|
-
});
|
|
510
|
-
toast.error(t('toggle_failed'));
|
|
511
|
-
}
|
|
512
|
-
} else {
|
|
513
|
-
// Update local state immediately
|
|
514
|
-
updateCalendarConnection(calendarId, true);
|
|
515
|
-
// Invalidate queries to refresh the list
|
|
516
|
-
queryClient.invalidateQueries({
|
|
517
|
-
queryKey: ['provider-calendar-list', wsId],
|
|
518
|
-
});
|
|
519
|
-
queryClient.invalidateQueries({
|
|
520
|
-
queryKey: ['calendar-connections', wsId],
|
|
521
|
-
});
|
|
522
|
-
toast.success(t('calendar_enabled'));
|
|
523
|
-
}
|
|
524
|
-
} else {
|
|
525
|
-
// Update existing connection
|
|
526
|
-
updateCalendarConnection(calendarId, newState);
|
|
527
|
-
|
|
528
|
-
const response = await fetch('/api/v1/calendar/connections', {
|
|
529
|
-
method: 'PATCH',
|
|
530
|
-
headers: { 'Content-Type': 'application/json' },
|
|
531
|
-
body: JSON.stringify({ id: calendarId, isEnabled: newState }),
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
if (!response.ok) {
|
|
535
|
-
updateCalendarConnection(calendarId, currentState);
|
|
536
|
-
toast.error(t('toggle_failed'));
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
} catch {
|
|
540
|
-
if (calendarData?.connectionExists) {
|
|
541
|
-
updateCalendarConnection(calendarId, currentState);
|
|
542
|
-
}
|
|
543
|
-
toast.error(t('toggle_failed'));
|
|
544
|
-
} finally {
|
|
545
|
-
setTogglingIds((prev) => {
|
|
546
|
-
const next = new Set(prev);
|
|
547
|
-
next.delete(calendarId);
|
|
548
|
-
return next;
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
};
|
|
552
|
-
|
|
553
|
-
const toggleAccountExpanded = (accountId: string) => {
|
|
554
|
-
setExpandedAccounts((prev) => {
|
|
555
|
-
const next = new Set(prev);
|
|
556
|
-
if (next.has(accountId)) next.delete(accountId);
|
|
557
|
-
else next.add(accountId);
|
|
558
|
-
return next;
|
|
559
|
-
});
|
|
560
|
-
};
|
|
561
|
-
|
|
562
|
-
const enabledCount =
|
|
563
|
-
calendarConnections.filter((c) => c.is_enabled).length +
|
|
564
|
-
tuturuuuEnabledCount;
|
|
565
|
-
const syncHealth = syncStatusData?.health;
|
|
566
|
-
const manualSyncDisabled = useMemo(() => {
|
|
567
|
-
if (syncMutation.isPending || isSyncing) {
|
|
568
|
-
return true;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
if (syncHealth?.currentlyRunning) {
|
|
572
|
-
return true;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
if ((syncHealth?.retryAfterSeconds ?? 0) > 0) {
|
|
576
|
-
return true;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
return false;
|
|
580
|
-
}, [
|
|
581
|
-
isSyncing,
|
|
582
|
-
syncHealth?.currentlyRunning,
|
|
583
|
-
syncHealth?.retryAfterSeconds,
|
|
584
|
-
syncMutation.isPending,
|
|
585
|
-
]);
|
|
586
|
-
const syncStatusStyles =
|
|
587
|
-
syncHealth?.state === 'syncing'
|
|
588
|
-
? 'bg-dynamic-blue/10 text-dynamic-blue'
|
|
589
|
-
: syncHealth?.state === 'healthy'
|
|
590
|
-
? 'bg-dynamic-green/10 text-dynamic-green'
|
|
591
|
-
: syncHealth?.state === 'disconnected'
|
|
592
|
-
? 'bg-muted text-muted-foreground'
|
|
593
|
-
: syncHealth?.state === 'degraded'
|
|
594
|
-
? 'bg-dynamic-orange/10 text-dynamic-orange'
|
|
595
|
-
: 'bg-dynamic-red/10 text-dynamic-red';
|
|
596
|
-
|
|
597
|
-
// Fetch Google calendars from API (must be before any conditional returns)
|
|
598
|
-
const { data: googleCalendarsData } = useQuery({
|
|
599
|
-
queryKey: ['provider-calendar-list', wsId],
|
|
600
|
-
enabled: hasConnectedAccounts,
|
|
601
|
-
queryFn: async () => {
|
|
602
|
-
const response = await fetch(
|
|
603
|
-
`/api/v1/calendar/auth/provider-calendars?wsId=${wsId}`,
|
|
604
|
-
{ cache: 'no-store' }
|
|
605
|
-
);
|
|
606
|
-
if (!response.ok) return { calendars: [], byAccount: {} };
|
|
607
|
-
return response.json() as Promise<{
|
|
608
|
-
calendars: ProviderCalendar[];
|
|
609
|
-
byAccount: Record<string, ProviderCalendar[]>;
|
|
610
|
-
}>;
|
|
611
|
-
},
|
|
612
|
-
staleTime: 30_000,
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
const calendarsByAccountFromAPI = googleCalendarsData?.byAccount || {};
|
|
616
|
-
|
|
617
|
-
// Merge API calendars with existing connections for visibility state
|
|
618
|
-
const calendarsByAccount = accounts.reduce(
|
|
619
|
-
(acc, account) => {
|
|
620
|
-
const apiCalendars = calendarsByAccountFromAPI[account.id] || [];
|
|
621
|
-
|
|
622
|
-
// Map API calendars to connection-like objects, merging with existing connections
|
|
623
|
-
acc[account.id] = apiCalendars.map((apiCal) => {
|
|
624
|
-
// Find existing connection for this calendar
|
|
625
|
-
const existingConnection = calendarConnections.find(
|
|
626
|
-
(conn) => conn.calendar_id === apiCal.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
|
-
};
|
|
640
|
-
});
|
|
641
|
-
|
|
642
|
-
return acc;
|
|
643
|
-
},
|
|
644
|
-
{} as Record<
|
|
645
|
-
string,
|
|
646
|
-
Array<{
|
|
647
|
-
id: string;
|
|
648
|
-
ws_id: string;
|
|
649
|
-
calendar_id: string;
|
|
650
|
-
calendar_name: string;
|
|
651
|
-
is_enabled: boolean;
|
|
652
|
-
color: string | null;
|
|
653
|
-
isFromAPI: boolean;
|
|
654
|
-
connectionExists: boolean;
|
|
655
|
-
accountId: string;
|
|
656
|
-
}>
|
|
657
|
-
>
|
|
658
|
-
);
|
|
659
|
-
|
|
660
|
-
// Note: Removed early return - we always show the full UI now so Tuturuuu calendars are visible
|
|
661
|
-
|
|
662
|
-
return (
|
|
663
|
-
<div className="flex items-center gap-2">
|
|
664
|
-
{/* Quick calendar visibility toggle */}
|
|
665
|
-
<Popover>
|
|
666
|
-
<PopoverTrigger asChild>
|
|
667
|
-
<Button variant="outline" size="sm" className="gap-2">
|
|
668
|
-
<Calendar className="h-4 w-4" />
|
|
669
|
-
<span className="hidden sm:inline">{t('calendars')}</span>
|
|
670
|
-
{enabledCount > 0 && (
|
|
671
|
-
<Badge variant="secondary" className="ml-1">
|
|
672
|
-
{enabledCount}
|
|
673
|
-
</Badge>
|
|
674
|
-
)}
|
|
675
|
-
</Button>
|
|
676
|
-
</PopoverTrigger>
|
|
677
|
-
<PopoverContent className="w-80" align="end">
|
|
678
|
-
<div className="space-y-4">
|
|
679
|
-
<div className="flex items-center justify-between">
|
|
680
|
-
<div>
|
|
681
|
-
<h4 className="font-medium text-sm">
|
|
682
|
-
{t('visible_calendars')}
|
|
683
|
-
</h4>
|
|
684
|
-
<p className="text-muted-foreground text-xs">
|
|
685
|
-
{enabledCount} {t('calendars_selected') || 'selected'}
|
|
686
|
-
</p>
|
|
687
|
-
</div>
|
|
688
|
-
<Dialog>
|
|
689
|
-
<DialogTrigger asChild>
|
|
690
|
-
<Button variant="ghost" size="icon" className="h-8 w-8">
|
|
691
|
-
<Settings className="h-4 w-4" />
|
|
692
|
-
</Button>
|
|
693
|
-
</DialogTrigger>
|
|
694
|
-
<DialogContent className="max-h-[80vh] overflow-y-auto sm:max-w-lg">
|
|
695
|
-
<DialogHeader>
|
|
696
|
-
<DialogTitle>{t('manage_calendar_accounts')}</DialogTitle>
|
|
697
|
-
<DialogDescription>
|
|
698
|
-
{t('manage_calendar_accounts_desc')}
|
|
699
|
-
</DialogDescription>
|
|
700
|
-
</DialogHeader>
|
|
701
|
-
|
|
702
|
-
<div className="space-y-4 py-4">
|
|
703
|
-
{/* Tuturuuu Calendars Section */}
|
|
704
|
-
<div className="space-y-3">
|
|
705
|
-
<div className="flex items-center justify-between">
|
|
706
|
-
<div className="flex items-center gap-2">
|
|
707
|
-
<Image
|
|
708
|
-
src="/icon-512x512.png"
|
|
709
|
-
alt="Tuturuuu"
|
|
710
|
-
width={24}
|
|
711
|
-
height={24}
|
|
712
|
-
/>
|
|
713
|
-
<span className="font-medium text-sm">
|
|
714
|
-
{t('tuturuuu_calendars') || 'Tuturuuu Calendars'}
|
|
715
|
-
</span>
|
|
716
|
-
</div>
|
|
717
|
-
<Button
|
|
718
|
-
variant="ghost"
|
|
719
|
-
size="sm"
|
|
720
|
-
className="h-7 gap-1 text-xs"
|
|
721
|
-
onClick={() => setShowCreateCalendarDialog(true)}
|
|
722
|
-
disabled={createCalendarMutation.isPending}
|
|
723
|
-
>
|
|
724
|
-
{createCalendarMutation.isPending ? (
|
|
725
|
-
<Loader2 className="h-3 w-3 animate-spin" />
|
|
726
|
-
) : (
|
|
727
|
-
<Plus className="h-3 w-3" />
|
|
728
|
-
)}
|
|
729
|
-
{t('new') || 'New'}
|
|
730
|
-
</Button>
|
|
731
|
-
<Dialog
|
|
732
|
-
open={showCreateCalendarDialog}
|
|
733
|
-
onOpenChange={(open) => {
|
|
734
|
-
setShowCreateCalendarDialog(open);
|
|
735
|
-
if (!open) setNewCalendarName('');
|
|
736
|
-
}}
|
|
737
|
-
>
|
|
738
|
-
<DialogContent className="sm:max-w-md">
|
|
739
|
-
<DialogHeader>
|
|
740
|
-
<DialogTitle>{t('create_calendar')}</DialogTitle>
|
|
741
|
-
</DialogHeader>
|
|
742
|
-
<div className="py-4">
|
|
743
|
-
<Input
|
|
744
|
-
value={newCalendarName}
|
|
745
|
-
onChange={(
|
|
746
|
-
e: React.ChangeEvent<HTMLInputElement>
|
|
747
|
-
) => setNewCalendarName(e.target.value)}
|
|
748
|
-
placeholder={
|
|
749
|
-
t('enter_calendar_name') ||
|
|
750
|
-
'Enter calendar name'
|
|
751
|
-
}
|
|
752
|
-
autoFocus
|
|
753
|
-
onKeyDown={(
|
|
754
|
-
e: React.KeyboardEvent<HTMLInputElement>
|
|
755
|
-
) => {
|
|
756
|
-
if (
|
|
757
|
-
e.key === 'Enter' &&
|
|
758
|
-
newCalendarName.trim()
|
|
759
|
-
) {
|
|
760
|
-
createCalendarMutation.mutate({
|
|
761
|
-
name: newCalendarName.trim(),
|
|
762
|
-
});
|
|
763
|
-
setShowCreateCalendarDialog(false);
|
|
764
|
-
setNewCalendarName('');
|
|
765
|
-
}
|
|
766
|
-
}}
|
|
767
|
-
/>
|
|
768
|
-
</div>
|
|
769
|
-
<DialogFooter>
|
|
770
|
-
<Button
|
|
771
|
-
onClick={() => {
|
|
772
|
-
if (newCalendarName.trim()) {
|
|
773
|
-
createCalendarMutation.mutate({
|
|
774
|
-
name: newCalendarName.trim(),
|
|
775
|
-
});
|
|
776
|
-
setShowCreateCalendarDialog(false);
|
|
777
|
-
setNewCalendarName('');
|
|
778
|
-
}
|
|
779
|
-
}}
|
|
780
|
-
disabled={
|
|
781
|
-
!newCalendarName.trim() ||
|
|
782
|
-
createCalendarMutation.isPending
|
|
783
|
-
}
|
|
784
|
-
>
|
|
785
|
-
{createCalendarMutation.isPending && (
|
|
786
|
-
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
787
|
-
)}
|
|
788
|
-
{t('create')}
|
|
789
|
-
</Button>
|
|
790
|
-
</DialogFooter>
|
|
791
|
-
</DialogContent>
|
|
792
|
-
</Dialog>
|
|
793
|
-
</div>
|
|
794
|
-
|
|
795
|
-
{/* System Calendars */}
|
|
796
|
-
<div className="space-y-1">
|
|
797
|
-
{systemCalendars.map((cal) => {
|
|
798
|
-
const Icon =
|
|
799
|
-
cal.calendar_type === 'primary'
|
|
800
|
-
? Calendar
|
|
801
|
-
: cal.calendar_type === 'tasks'
|
|
802
|
-
? Target
|
|
803
|
-
: cal.calendar_type === 'habits'
|
|
804
|
-
? Sparkles
|
|
805
|
-
: Calendar;
|
|
806
|
-
return (
|
|
807
|
-
<div
|
|
808
|
-
key={cal.id}
|
|
809
|
-
className="flex items-center justify-between gap-2 rounded-md px-2 py-1.5 hover:bg-muted/50"
|
|
810
|
-
>
|
|
811
|
-
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
812
|
-
<div
|
|
813
|
-
className="flex h-5 w-5 shrink-0 items-center justify-center rounded"
|
|
814
|
-
style={{
|
|
815
|
-
backgroundColor: `${getCalendarColor(cal.color || 'BLUE')}20`,
|
|
816
|
-
}}
|
|
817
|
-
>
|
|
818
|
-
<Icon
|
|
819
|
-
className="h-3 w-3"
|
|
820
|
-
style={{
|
|
821
|
-
color: getCalendarColor(
|
|
822
|
-
cal.color || 'BLUE'
|
|
823
|
-
),
|
|
824
|
-
}}
|
|
825
|
-
/>
|
|
826
|
-
</div>
|
|
827
|
-
<span
|
|
828
|
-
className="line-clamp-1 break-all text-sm"
|
|
829
|
-
title={cal.name}
|
|
830
|
-
>
|
|
831
|
-
{cal.name}
|
|
832
|
-
</span>
|
|
833
|
-
<Lock className="h-3 w-3 shrink-0 text-muted-foreground" />
|
|
834
|
-
</div>
|
|
835
|
-
<Switch
|
|
836
|
-
checked={cal.is_enabled}
|
|
837
|
-
onCheckedChange={() =>
|
|
838
|
-
toggleWorkspaceCalendarMutation.mutate({
|
|
839
|
-
id: cal.id,
|
|
840
|
-
is_enabled: !cal.is_enabled,
|
|
841
|
-
})
|
|
842
|
-
}
|
|
843
|
-
disabled={togglingTuturuuuIds.has(cal.id)}
|
|
844
|
-
/>
|
|
845
|
-
</div>
|
|
846
|
-
);
|
|
847
|
-
})}
|
|
848
|
-
</div>
|
|
849
|
-
|
|
850
|
-
{/* Custom Calendars */}
|
|
851
|
-
{customCalendars.length > 0 && (
|
|
852
|
-
<div className="space-y-1">
|
|
853
|
-
<p className="px-2 text-muted-foreground text-xs">
|
|
854
|
-
{t('custom_calendars') || 'Custom'}
|
|
855
|
-
</p>
|
|
856
|
-
{customCalendars.map((cal) => (
|
|
857
|
-
<div
|
|
858
|
-
key={cal.id}
|
|
859
|
-
className="flex items-center justify-between gap-2 rounded-md px-2 py-1.5 hover:bg-muted/50"
|
|
860
|
-
>
|
|
861
|
-
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
862
|
-
<div
|
|
863
|
-
className="h-3 w-3 shrink-0 rounded-full"
|
|
864
|
-
style={{
|
|
865
|
-
backgroundColor: getCalendarColor(
|
|
866
|
-
cal.color || 'BLUE'
|
|
867
|
-
),
|
|
868
|
-
}}
|
|
869
|
-
/>
|
|
870
|
-
<span
|
|
871
|
-
className="line-clamp-1 break-all text-sm"
|
|
872
|
-
title={cal.name}
|
|
873
|
-
>
|
|
874
|
-
{cal.name}
|
|
875
|
-
</span>
|
|
876
|
-
</div>
|
|
877
|
-
<div className="flex items-center gap-1">
|
|
878
|
-
<Button
|
|
879
|
-
variant="ghost"
|
|
880
|
-
size="icon"
|
|
881
|
-
className="h-6 w-6 text-destructive hover:bg-destructive/10"
|
|
882
|
-
onClick={() =>
|
|
883
|
-
deleteCalendarMutation.mutate(cal.id)
|
|
884
|
-
}
|
|
885
|
-
disabled={deleteCalendarMutation.isPending}
|
|
886
|
-
>
|
|
887
|
-
<Trash2 className="h-3 w-3" />
|
|
888
|
-
</Button>
|
|
889
|
-
<Switch
|
|
890
|
-
checked={cal.is_enabled}
|
|
891
|
-
onCheckedChange={() =>
|
|
892
|
-
toggleWorkspaceCalendarMutation.mutate({
|
|
893
|
-
id: cal.id,
|
|
894
|
-
is_enabled: !cal.is_enabled,
|
|
895
|
-
})
|
|
896
|
-
}
|
|
897
|
-
disabled={togglingTuturuuuIds.has(cal.id)}
|
|
898
|
-
/>
|
|
899
|
-
</div>
|
|
900
|
-
</div>
|
|
901
|
-
))}
|
|
902
|
-
</div>
|
|
903
|
-
)}
|
|
904
|
-
</div>
|
|
905
|
-
|
|
906
|
-
<Separator />
|
|
907
|
-
|
|
908
|
-
{/* Connected accounts with calendars */}
|
|
909
|
-
{accounts.map((account) => (
|
|
910
|
-
<Collapsible
|
|
911
|
-
key={account.id}
|
|
912
|
-
open={expandedAccounts.has(account.id)}
|
|
913
|
-
onOpenChange={() => toggleAccountExpanded(account.id)}
|
|
914
|
-
>
|
|
915
|
-
<div className="rounded-lg border">
|
|
916
|
-
<CollapsibleTrigger className="flex w-full items-center justify-between p-3 transition-colors hover:bg-muted/50">
|
|
917
|
-
<div className="flex items-center gap-3">
|
|
918
|
-
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-muted">
|
|
919
|
-
<Image
|
|
920
|
-
src={
|
|
921
|
-
account.provider === 'google'
|
|
922
|
-
? '/media/logos/google.svg'
|
|
923
|
-
: '/media/logos/microsoft.svg'
|
|
924
|
-
}
|
|
925
|
-
alt={account.provider}
|
|
926
|
-
width={18}
|
|
927
|
-
height={18}
|
|
928
|
-
/>
|
|
929
|
-
</div>
|
|
930
|
-
<div className="text-left">
|
|
931
|
-
<p className="font-medium text-sm">
|
|
932
|
-
{account.account_name ||
|
|
933
|
-
account.account_email ||
|
|
934
|
-
t('unknown_account')}
|
|
935
|
-
</p>
|
|
936
|
-
{account.account_email &&
|
|
937
|
-
account.account_name && (
|
|
938
|
-
<p className="text-muted-foreground text-xs">
|
|
939
|
-
{account.account_email}
|
|
940
|
-
</p>
|
|
941
|
-
)}
|
|
942
|
-
</div>
|
|
943
|
-
</div>
|
|
944
|
-
<div className="flex items-center gap-2">
|
|
945
|
-
<Badge
|
|
946
|
-
variant="outline"
|
|
947
|
-
className="text-xs capitalize"
|
|
948
|
-
>
|
|
949
|
-
{account.provider}
|
|
950
|
-
</Badge>
|
|
951
|
-
<ChevronDown
|
|
952
|
-
className={`h-4 w-4 transition-transform ${expandedAccounts.has(account.id) ? 'rotate-180' : ''}`}
|
|
953
|
-
/>
|
|
954
|
-
</div>
|
|
955
|
-
</CollapsibleTrigger>
|
|
956
|
-
|
|
957
|
-
<CollapsibleContent>
|
|
958
|
-
<Separator />
|
|
959
|
-
<div className="space-y-2 p-3">
|
|
960
|
-
{(calendarsByAccount[account.id] || []).length >
|
|
961
|
-
0 ? (
|
|
962
|
-
(calendarsByAccount[account.id] || []).map(
|
|
963
|
-
(cal) => (
|
|
964
|
-
<div
|
|
965
|
-
key={cal.id}
|
|
966
|
-
className="flex items-center justify-between gap-2 rounded-md px-2 py-1.5 hover:bg-muted/50"
|
|
967
|
-
>
|
|
968
|
-
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
969
|
-
<div
|
|
970
|
-
className="h-3 w-3 shrink-0 rounded-full"
|
|
971
|
-
style={{
|
|
972
|
-
backgroundColor:
|
|
973
|
-
cal.color || '#4285f4',
|
|
974
|
-
}}
|
|
975
|
-
/>
|
|
976
|
-
<span
|
|
977
|
-
className="line-clamp-1 break-all text-sm"
|
|
978
|
-
title={cal.calendar_name}
|
|
979
|
-
>
|
|
980
|
-
{cal.calendar_name}
|
|
981
|
-
</span>
|
|
982
|
-
</div>
|
|
983
|
-
<Switch
|
|
984
|
-
checked={cal.is_enabled}
|
|
985
|
-
onCheckedChange={() =>
|
|
986
|
-
handleToggle(cal.id, cal.is_enabled, {
|
|
987
|
-
calendar_id: cal.calendar_id,
|
|
988
|
-
calendar_name: cal.calendar_name,
|
|
989
|
-
color: cal.color,
|
|
990
|
-
connectionExists:
|
|
991
|
-
cal.connectionExists,
|
|
992
|
-
accountId: cal.accountId,
|
|
993
|
-
})
|
|
994
|
-
}
|
|
995
|
-
disabled={togglingIds.has(cal.id)}
|
|
996
|
-
/>
|
|
997
|
-
</div>
|
|
998
|
-
)
|
|
999
|
-
)
|
|
1000
|
-
) : (
|
|
1001
|
-
<p className="py-2 text-center text-muted-foreground text-xs">
|
|
1002
|
-
{t('no_calendars_found')}
|
|
1003
|
-
</p>
|
|
1004
|
-
)}
|
|
1005
|
-
|
|
1006
|
-
<Separator className="my-2" />
|
|
1007
|
-
<Button
|
|
1008
|
-
variant="ghost"
|
|
1009
|
-
size="sm"
|
|
1010
|
-
className="w-full text-destructive hover:bg-destructive/10 hover:text-destructive"
|
|
1011
|
-
onClick={() =>
|
|
1012
|
-
disconnectMutation.mutate(account.id)
|
|
1013
|
-
}
|
|
1014
|
-
disabled={disconnectingId === account.id}
|
|
1015
|
-
>
|
|
1016
|
-
{disconnectingId === account.id ? (
|
|
1017
|
-
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
1018
|
-
) : (
|
|
1019
|
-
<Trash2 className="mr-2 h-4 w-4" />
|
|
1020
|
-
)}
|
|
1021
|
-
{t('disconnect')}
|
|
1022
|
-
</Button>
|
|
1023
|
-
</div>
|
|
1024
|
-
</CollapsibleContent>
|
|
1025
|
-
</div>
|
|
1026
|
-
</Collapsible>
|
|
1027
|
-
))}
|
|
1028
|
-
|
|
1029
|
-
{/* Add new account buttons */}
|
|
1030
|
-
<div className="space-y-2 pt-2">
|
|
1031
|
-
<p className="font-medium text-muted-foreground text-xs uppercase tracking-wider">
|
|
1032
|
-
{t('add_account')}
|
|
1033
|
-
</p>
|
|
1034
|
-
<div className="grid grid-cols-2 gap-2">
|
|
1035
|
-
<Button
|
|
1036
|
-
variant="outline"
|
|
1037
|
-
size="sm"
|
|
1038
|
-
className="gap-2 text-center"
|
|
1039
|
-
onClick={() => googleAuthMutation.mutate()}
|
|
1040
|
-
disabled={googleAuthMutation.isPending}
|
|
1041
|
-
>
|
|
1042
|
-
{googleAuthMutation.isPending ? (
|
|
1043
|
-
<Loader2 className="h-4 w-4 animate-spin" />
|
|
1044
|
-
) : (
|
|
1045
|
-
<Image
|
|
1046
|
-
src="/media/logos/google.svg"
|
|
1047
|
-
alt="Google"
|
|
1048
|
-
width={16}
|
|
1049
|
-
height={16}
|
|
1050
|
-
/>
|
|
1051
|
-
)}
|
|
1052
|
-
{t('google')}
|
|
1053
|
-
</Button>
|
|
1054
|
-
<Button
|
|
1055
|
-
variant="outline"
|
|
1056
|
-
size="sm"
|
|
1057
|
-
className="gap-2 text-center"
|
|
1058
|
-
onClick={() => microsoftAuthMutation.mutate()}
|
|
1059
|
-
disabled={microsoftAuthMutation.isPending}
|
|
1060
|
-
>
|
|
1061
|
-
{microsoftAuthMutation.isPending ? (
|
|
1062
|
-
<Loader2 className="h-4 w-4 animate-spin" />
|
|
1063
|
-
) : (
|
|
1064
|
-
<Image
|
|
1065
|
-
src="/media/logos/microsoft.svg"
|
|
1066
|
-
alt="Microsoft"
|
|
1067
|
-
width={16}
|
|
1068
|
-
height={16}
|
|
1069
|
-
/>
|
|
1070
|
-
)}
|
|
1071
|
-
{t('outlook')}
|
|
1072
|
-
</Button>
|
|
1073
|
-
</div>
|
|
1074
|
-
</div>
|
|
1075
|
-
|
|
1076
|
-
{/* Danger Zone */}
|
|
1077
|
-
<Separator />
|
|
1078
|
-
<div className="space-y-2">
|
|
1079
|
-
<p className="font-medium text-destructive text-xs uppercase tracking-wider">
|
|
1080
|
-
{t('danger_zone') || 'Danger Zone'}
|
|
1081
|
-
</p>
|
|
1082
|
-
<AlertDialog>
|
|
1083
|
-
<AlertDialogTrigger asChild>
|
|
1084
|
-
<Button
|
|
1085
|
-
variant="destructive"
|
|
1086
|
-
size="sm"
|
|
1087
|
-
className="w-full gap-2"
|
|
1088
|
-
disabled={resetCalendarDataMutation.isPending}
|
|
1089
|
-
>
|
|
1090
|
-
{resetCalendarDataMutation.isPending ? (
|
|
1091
|
-
<Loader2 className="h-4 w-4 animate-spin" />
|
|
1092
|
-
) : (
|
|
1093
|
-
<RotateCcw className="h-4 w-4" />
|
|
1094
|
-
)}
|
|
1095
|
-
{t('reset_calendar_data') || 'Reset Calendar Data'}
|
|
1096
|
-
</Button>
|
|
1097
|
-
</AlertDialogTrigger>
|
|
1098
|
-
<AlertDialogContent>
|
|
1099
|
-
<AlertDialogHeader>
|
|
1100
|
-
<AlertDialogTitle>
|
|
1101
|
-
{t('reset_calendar_confirm_title') ||
|
|
1102
|
-
'Reset all calendar data?'}
|
|
1103
|
-
</AlertDialogTitle>
|
|
1104
|
-
<AlertDialogDescription>
|
|
1105
|
-
{t('reset_calendar_confirm_desc') ||
|
|
1106
|
-
'This will permanently delete all calendar events, disconnect all linked accounts (Google, Microsoft), and remove custom calendars. System calendars will be preserved but emptied. This action cannot be undone.'}
|
|
1107
|
-
</AlertDialogDescription>
|
|
1108
|
-
</AlertDialogHeader>
|
|
1109
|
-
<AlertDialogFooter>
|
|
1110
|
-
<AlertDialogCancel>
|
|
1111
|
-
{t('cancel') || 'Cancel'}
|
|
1112
|
-
</AlertDialogCancel>
|
|
1113
|
-
<AlertDialogAction
|
|
1114
|
-
onClick={() => resetCalendarDataMutation.mutate()}
|
|
1115
|
-
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
1116
|
-
>
|
|
1117
|
-
{t('reset_all_data') || 'Reset All Data'}
|
|
1118
|
-
</AlertDialogAction>
|
|
1119
|
-
</AlertDialogFooter>
|
|
1120
|
-
</AlertDialogContent>
|
|
1121
|
-
</AlertDialog>
|
|
1122
|
-
</div>
|
|
1123
|
-
</div>
|
|
1124
|
-
</DialogContent>
|
|
1125
|
-
</Dialog>
|
|
1126
|
-
</div>
|
|
1127
|
-
|
|
1128
|
-
{!hasConnectedAccounts && (
|
|
1129
|
-
<div className="rounded-xl border border-border/60 bg-muted/20 p-3">
|
|
1130
|
-
<div className="space-y-1">
|
|
1131
|
-
<p className="font-medium text-sm">
|
|
1132
|
-
{t('connect_calendar_accounts') ||
|
|
1133
|
-
'Connect calendar accounts'}
|
|
1134
|
-
</p>
|
|
1135
|
-
<p className="text-muted-foreground text-xs">
|
|
1136
|
-
{t('connect_calendar_accounts_desc') ||
|
|
1137
|
-
'Link Google or Outlook to keep external calendars visible and continuously synchronized.'}
|
|
1138
|
-
</p>
|
|
1139
|
-
</div>
|
|
1140
|
-
<div className="mt-3 grid grid-cols-2 gap-2">
|
|
1141
|
-
<Button
|
|
1142
|
-
variant="outline"
|
|
1143
|
-
size="sm"
|
|
1144
|
-
className="gap-2"
|
|
1145
|
-
onClick={() => googleAuthMutation.mutate()}
|
|
1146
|
-
disabled={googleAuthMutation.isPending}
|
|
1147
|
-
>
|
|
1148
|
-
{googleAuthMutation.isPending ? (
|
|
1149
|
-
<Loader2 className="h-4 w-4 animate-spin" />
|
|
1150
|
-
) : (
|
|
1151
|
-
<Image
|
|
1152
|
-
src="/media/logos/google.svg"
|
|
1153
|
-
alt="Google"
|
|
1154
|
-
width={16}
|
|
1155
|
-
height={16}
|
|
1156
|
-
/>
|
|
1157
|
-
)}
|
|
1158
|
-
{t('google')}
|
|
1159
|
-
</Button>
|
|
1160
|
-
<Button
|
|
1161
|
-
variant="outline"
|
|
1162
|
-
size="sm"
|
|
1163
|
-
className="gap-2"
|
|
1164
|
-
onClick={() => microsoftAuthMutation.mutate()}
|
|
1165
|
-
disabled={microsoftAuthMutation.isPending}
|
|
1166
|
-
>
|
|
1167
|
-
{microsoftAuthMutation.isPending ? (
|
|
1168
|
-
<Loader2 className="h-4 w-4 animate-spin" />
|
|
1169
|
-
) : (
|
|
1170
|
-
<Image
|
|
1171
|
-
src="/media/logos/microsoft.svg"
|
|
1172
|
-
alt="Microsoft"
|
|
1173
|
-
width={16}
|
|
1174
|
-
height={16}
|
|
1175
|
-
/>
|
|
1176
|
-
)}
|
|
1177
|
-
{t('outlook')}
|
|
1178
|
-
</Button>
|
|
1179
|
-
</div>
|
|
1180
|
-
</div>
|
|
1181
|
-
)}
|
|
1182
|
-
|
|
1183
|
-
<Separator />
|
|
1184
|
-
|
|
1185
|
-
{/* Calendar list grouped by source */}
|
|
1186
|
-
<div className="max-h-64 space-y-3 overflow-y-auto">
|
|
1187
|
-
{isLoadingCalendars ? (
|
|
1188
|
-
<div className="flex items-center justify-center py-6">
|
|
1189
|
-
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
|
1190
|
-
</div>
|
|
1191
|
-
) : (
|
|
1192
|
-
<>
|
|
1193
|
-
{/* Tuturuuu Calendars */}
|
|
1194
|
-
{workspaceCalendars.length > 0 && (
|
|
1195
|
-
<Collapsible
|
|
1196
|
-
open={expandedPopoverAccounts.has('tuturuuu')}
|
|
1197
|
-
onOpenChange={(open) => {
|
|
1198
|
-
setExpandedPopoverAccounts((prev) => {
|
|
1199
|
-
const next = new Set(prev);
|
|
1200
|
-
if (open) next.add('tuturuuu');
|
|
1201
|
-
else next.delete('tuturuuu');
|
|
1202
|
-
return next;
|
|
1203
|
-
});
|
|
1204
|
-
}}
|
|
1205
|
-
>
|
|
1206
|
-
<CollapsibleTrigger className="flex w-full cursor-pointer items-center gap-2 rounded-md px-1 py-1 hover:bg-muted/50">
|
|
1207
|
-
<ChevronDown
|
|
1208
|
-
className={`h-3 w-3 shrink-0 text-muted-foreground transition-transform ${expandedPopoverAccounts.has('tuturuuu') ? '' : '-rotate-90'}`}
|
|
1209
|
-
/>
|
|
1210
|
-
<Image
|
|
1211
|
-
src="/icon-512x512.png"
|
|
1212
|
-
alt="Tuturuuu"
|
|
1213
|
-
width={14}
|
|
1214
|
-
height={14}
|
|
1215
|
-
/>
|
|
1216
|
-
<div className="flex min-w-0 flex-1 flex-col text-left">
|
|
1217
|
-
<span className="font-medium text-muted-foreground text-xs">
|
|
1218
|
-
{t('tuturuuu_calendars') || 'Tuturuuu'}
|
|
1219
|
-
</span>
|
|
1220
|
-
{userEmail && (
|
|
1221
|
-
<span className="truncate text-[10px] text-muted-foreground">
|
|
1222
|
-
{userEmail}
|
|
1223
|
-
</span>
|
|
1224
|
-
)}
|
|
1225
|
-
</div>
|
|
1226
|
-
</CollapsibleTrigger>
|
|
1227
|
-
<CollapsibleContent className="space-y-1 pt-1">
|
|
1228
|
-
{workspaceCalendars.map((cal) => (
|
|
1229
|
-
<div
|
|
1230
|
-
key={cal.id}
|
|
1231
|
-
className="flex items-center justify-between rounded-md px-2 py-1 hover:bg-muted/50"
|
|
1232
|
-
>
|
|
1233
|
-
<div className="flex items-center gap-2">
|
|
1234
|
-
<div
|
|
1235
|
-
className="h-2.5 w-2.5 rounded-full"
|
|
1236
|
-
style={{
|
|
1237
|
-
backgroundColor: getCalendarColor(
|
|
1238
|
-
cal.color || 'BLUE'
|
|
1239
|
-
),
|
|
1240
|
-
}}
|
|
1241
|
-
/>
|
|
1242
|
-
<span className="line-clamp-1 max-w-45 break-all text-sm">
|
|
1243
|
-
{cal.name}
|
|
1244
|
-
</span>
|
|
1245
|
-
{cal.is_system && (
|
|
1246
|
-
<Lock className="h-2.5 w-2.5 text-muted-foreground" />
|
|
1247
|
-
)}
|
|
1248
|
-
</div>
|
|
1249
|
-
<Button
|
|
1250
|
-
variant="ghost"
|
|
1251
|
-
size="icon"
|
|
1252
|
-
className="h-6 w-6"
|
|
1253
|
-
onClick={() =>
|
|
1254
|
-
toggleWorkspaceCalendarMutation.mutate({
|
|
1255
|
-
id: cal.id,
|
|
1256
|
-
is_enabled: !cal.is_enabled,
|
|
1257
|
-
})
|
|
1258
|
-
}
|
|
1259
|
-
disabled={togglingTuturuuuIds.has(cal.id)}
|
|
1260
|
-
>
|
|
1261
|
-
{togglingTuturuuuIds.has(cal.id) ? (
|
|
1262
|
-
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
1263
|
-
) : cal.is_enabled ? (
|
|
1264
|
-
<Eye className="h-3.5 w-3.5 text-primary" />
|
|
1265
|
-
) : (
|
|
1266
|
-
<EyeOff className="h-3.5 w-3.5 text-muted-foreground" />
|
|
1267
|
-
)}
|
|
1268
|
-
</Button>
|
|
1269
|
-
</div>
|
|
1270
|
-
))}
|
|
1271
|
-
</CollapsibleContent>
|
|
1272
|
-
</Collapsible>
|
|
1273
|
-
)}
|
|
1274
|
-
|
|
1275
|
-
{/* External accounts calendars */}
|
|
1276
|
-
{accounts.map((account) => {
|
|
1277
|
-
const accountCals = calendarsByAccount[account.id] || [];
|
|
1278
|
-
if (accountCals.length === 0) return null;
|
|
1279
|
-
const accountKey = `account-${account.id}`;
|
|
1280
|
-
|
|
1281
|
-
return (
|
|
1282
|
-
<Collapsible
|
|
1283
|
-
key={account.id}
|
|
1284
|
-
open={expandedPopoverAccounts.has(accountKey)}
|
|
1285
|
-
onOpenChange={(open) => {
|
|
1286
|
-
setExpandedPopoverAccounts((prev) => {
|
|
1287
|
-
const next = new Set(prev);
|
|
1288
|
-
if (open) next.add(accountKey);
|
|
1289
|
-
else next.delete(accountKey);
|
|
1290
|
-
return next;
|
|
1291
|
-
});
|
|
1292
|
-
}}
|
|
1293
|
-
>
|
|
1294
|
-
<CollapsibleTrigger className="flex w-full cursor-pointer items-center gap-2 rounded-md px-1 py-1 hover:bg-muted/50">
|
|
1295
|
-
<ChevronDown
|
|
1296
|
-
className={`h-3 w-3 shrink-0 text-muted-foreground transition-transform ${expandedPopoverAccounts.has(accountKey) ? '' : '-rotate-90'}`}
|
|
1297
|
-
/>
|
|
1298
|
-
<Image
|
|
1299
|
-
src={
|
|
1300
|
-
account.provider === 'google'
|
|
1301
|
-
? '/media/logos/google.svg'
|
|
1302
|
-
: '/media/logos/microsoft.svg'
|
|
1303
|
-
}
|
|
1304
|
-
alt={account.provider}
|
|
1305
|
-
width={12}
|
|
1306
|
-
height={12}
|
|
1307
|
-
/>
|
|
1308
|
-
<span className="min-w-0 flex-1 truncate text-left text-muted-foreground text-xs">
|
|
1309
|
-
{account.account_email || account.account_name}
|
|
1310
|
-
</span>
|
|
1311
|
-
</CollapsibleTrigger>
|
|
1312
|
-
<CollapsibleContent className="space-y-1 pt-1">
|
|
1313
|
-
{accountCals.map((cal) => (
|
|
1314
|
-
<div
|
|
1315
|
-
key={cal.id}
|
|
1316
|
-
className="flex items-center justify-between rounded-md px-2 py-1 hover:bg-muted/50"
|
|
1317
|
-
>
|
|
1318
|
-
<div className="flex items-center gap-2">
|
|
1319
|
-
<div
|
|
1320
|
-
className="h-2.5 w-2.5 rounded-full"
|
|
1321
|
-
style={{
|
|
1322
|
-
backgroundColor: cal.color || '#4285f4',
|
|
1323
|
-
}}
|
|
1324
|
-
/>
|
|
1325
|
-
<span className="line-clamp-1 max-w-45 break-all text-sm">
|
|
1326
|
-
{cal.calendar_name}
|
|
1327
|
-
</span>
|
|
1328
|
-
</div>
|
|
1329
|
-
<Button
|
|
1330
|
-
variant="ghost"
|
|
1331
|
-
size="icon"
|
|
1332
|
-
className="h-6 w-6"
|
|
1333
|
-
onClick={() =>
|
|
1334
|
-
handleToggle(cal.id, cal.is_enabled, {
|
|
1335
|
-
calendar_id: cal.calendar_id,
|
|
1336
|
-
calendar_name: cal.calendar_name,
|
|
1337
|
-
color: cal.color,
|
|
1338
|
-
connectionExists: cal.connectionExists,
|
|
1339
|
-
accountId: cal.accountId,
|
|
1340
|
-
})
|
|
1341
|
-
}
|
|
1342
|
-
disabled={togglingIds.has(cal.id)}
|
|
1343
|
-
>
|
|
1344
|
-
{togglingIds.has(cal.id) ? (
|
|
1345
|
-
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
1346
|
-
) : cal.is_enabled ? (
|
|
1347
|
-
<Eye className="h-3.5 w-3.5 text-primary" />
|
|
1348
|
-
) : (
|
|
1349
|
-
<EyeOff className="h-3.5 w-3.5 text-muted-foreground" />
|
|
1350
|
-
)}
|
|
1351
|
-
</Button>
|
|
1352
|
-
</div>
|
|
1353
|
-
))}
|
|
1354
|
-
</CollapsibleContent>
|
|
1355
|
-
</Collapsible>
|
|
1356
|
-
);
|
|
1357
|
-
})}
|
|
1358
|
-
|
|
1359
|
-
{calendarConnections.length === 0 &&
|
|
1360
|
-
workspaceCalendars.length === 0 && (
|
|
1361
|
-
<div className="rounded-lg border border-dashed p-4 text-center">
|
|
1362
|
-
<p className="font-medium text-sm">
|
|
1363
|
-
{t('no_calendars_synced')}
|
|
1364
|
-
</p>
|
|
1365
|
-
<p className="mt-1 text-muted-foreground text-xs">
|
|
1366
|
-
{t('choose_calendars_to_show') ||
|
|
1367
|
-
'Connect an account, then choose which calendars should appear here.'}
|
|
1368
|
-
</p>
|
|
1369
|
-
</div>
|
|
1370
|
-
)}
|
|
1371
|
-
</>
|
|
1372
|
-
)}
|
|
1373
|
-
</div>
|
|
1374
|
-
|
|
1375
|
-
<Separator />
|
|
1376
|
-
|
|
1377
|
-
{/* Sync actions */}
|
|
1378
|
-
<div className="flex items-center justify-between">
|
|
1379
|
-
<div
|
|
1380
|
-
className={`rounded-full px-2 py-0.5 text-xs ${syncStatusStyles}`}
|
|
1381
|
-
>
|
|
1382
|
-
{syncHealth?.state === 'syncing' && (
|
|
1383
|
-
<Loader2 className="mr-1 inline h-3 w-3 animate-spin" />
|
|
1384
|
-
)}
|
|
1385
|
-
{syncHealth?.state === 'degraded'
|
|
1386
|
-
? t('degraded') || 'Degraded'
|
|
1387
|
-
: syncHealth?.state === 'healthy'
|
|
1388
|
-
? t('healthy') || 'Healthy'
|
|
1389
|
-
: syncHealth?.state === 'disconnected'
|
|
1390
|
-
? t('connect_accounts') || 'Connect accounts'
|
|
1391
|
-
: t('syncing_calendars') || 'Syncing calendars'}
|
|
1392
|
-
</div>
|
|
1393
|
-
<div className="flex gap-1">
|
|
1394
|
-
<Button
|
|
1395
|
-
variant="ghost"
|
|
1396
|
-
size="icon"
|
|
1397
|
-
className="h-7 w-7"
|
|
1398
|
-
onClick={() => syncMutation.mutate()}
|
|
1399
|
-
disabled={manualSyncDisabled}
|
|
1400
|
-
title={t('sync_now') || 'Sync now'}
|
|
1401
|
-
>
|
|
1402
|
-
<ExternalLink
|
|
1403
|
-
className={`h-3.5 w-3.5 ${
|
|
1404
|
-
manualSyncDisabled ? 'animate-pulse' : ''
|
|
1405
|
-
}`}
|
|
1406
|
-
/>
|
|
1407
|
-
</Button>
|
|
1408
|
-
<Button
|
|
1409
|
-
variant="ghost"
|
|
1410
|
-
size="icon"
|
|
1411
|
-
className="h-7 w-7"
|
|
1412
|
-
onClick={() => syncToTuturuuu()}
|
|
1413
|
-
disabled={manualSyncDisabled}
|
|
1414
|
-
title={t('sync_from_google')}
|
|
1415
|
-
>
|
|
1416
|
-
<RefreshCw
|
|
1417
|
-
className={`h-3.5 w-3.5 ${manualSyncDisabled ? 'animate-spin' : ''}`}
|
|
1418
|
-
/>
|
|
1419
|
-
</Button>
|
|
1420
|
-
</div>
|
|
1421
|
-
</div>
|
|
1422
|
-
{syncHealth?.lastSuccessAt && (
|
|
1423
|
-
<p className="text-muted-foreground text-xs">
|
|
1424
|
-
{t('last_synced_at') || 'Last synced'}:{' '}
|
|
1425
|
-
{new Date(syncHealth.lastSuccessAt).toLocaleString()}
|
|
1426
|
-
</p>
|
|
1427
|
-
)}
|
|
1428
|
-
</div>
|
|
1429
|
-
</PopoverContent>
|
|
1430
|
-
</Popover>
|
|
1431
|
-
</div>
|
|
1432
|
-
);
|
|
6
|
+
type CalendarConnectionsUnifiedVariant,
|
|
7
|
+
useCalendarConnectionsManager,
|
|
8
|
+
} from './use-calendar-connections-manager';
|
|
9
|
+
|
|
10
|
+
export type { CalendarConnectionsUnifiedVariant };
|
|
11
|
+
|
|
12
|
+
export default function CalendarConnectionsUnified({
|
|
13
|
+
wsId,
|
|
14
|
+
variant = 'compact',
|
|
15
|
+
className,
|
|
16
|
+
}: {
|
|
17
|
+
wsId: string;
|
|
18
|
+
variant?: CalendarConnectionsUnifiedVariant;
|
|
19
|
+
className?: string;
|
|
20
|
+
}) {
|
|
21
|
+
const state = useCalendarConnectionsManager(wsId);
|
|
22
|
+
|
|
23
|
+
if (variant === 'settings') {
|
|
24
|
+
return (
|
|
25
|
+
<CalendarConnectionsSettingsContent state={state} className={className} />
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return <CalendarConnectionsCompact state={state} />;
|
|
1433
30
|
}
|