@tuturuuu/ui 0.22.0 → 0.24.0
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 +36 -0
- package/biome.json +1 -1
- package/package.json +78 -78
- package/src/components/ui/calendar-app/components/require-workspace-timezone-dialog.test.tsx +121 -0
- package/src/components/ui/calendar-app/components/require-workspace-timezone-dialog.tsx +37 -4
- package/src/components/ui/chat/ai-message-markdown.tsx +6 -1
- package/src/components/ui/chat/chat-sidebar-groups.test.ts +33 -0
- package/src/components/ui/chat/chat-sidebar-items.tsx +10 -2
- package/src/components/ui/chat/chat-sidebar-panel.test.tsx +29 -1
- package/src/components/ui/chat/chat-sidebar-panel.tsx +4 -3
- package/src/components/ui/chat/chat-sidebar-sections.ts +24 -10
- package/src/components/ui/chat/chat-utils.test.ts +107 -0
- package/src/components/ui/chat/chat-workspace-header.tsx +11 -5
- package/src/components/ui/chat/chat-workspace.tsx +66 -24
- package/src/components/ui/chat/hooks-conversations.ts +9 -1
- package/src/components/ui/chat/hooks-messages.ts +1 -0
- package/src/components/ui/chat/message-bubble.tsx +49 -4
- package/src/components/ui/chat/message-composer.tsx +29 -23
- package/src/components/ui/chat/message-markdown.tsx +4 -1
- package/src/components/ui/chat/query-keys.ts +7 -1
- package/src/components/ui/chat/utils.ts +85 -3
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +72 -0
- package/src/components/ui/custom/onboarding-settings-panel.tsx +131 -0
- package/src/components/ui/custom/settings-dialog-shell.tsx +55 -18
- package/src/components/ui/custom/workspace-select-helpers.ts +50 -0
- package/src/components/ui/custom/workspace-select.tsx +37 -38
- package/src/components/ui/finance/analytics/analytics-page.tsx +10 -0
- package/src/components/ui/finance/analytics/inventory-provider-summary.tsx +110 -0
- package/src/components/ui/finance/finance-overview-metrics.tsx +49 -0
- package/src/components/ui/finance/transactions/inventory-reconciliation-entry-row.tsx +122 -0
- package/src/components/ui/finance/transactions/inventory-reconciliation-panel.tsx +407 -0
- package/src/components/ui/finance/transactions/transaction-card.tsx +15 -0
- package/src/components/ui/finance/transactions/transaction-edit-dialog.test.tsx +31 -0
- package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +26 -4
- package/src/components/ui/finance/transactions/transactionId/transaction-details-client-page.tsx +22 -0
- package/src/components/ui/finance/transactions/transactions-page.tsx +8 -0
- package/src/components/ui/finance/wallets/walletId/inventory-wallet-contribution.tsx +84 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +4 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +9 -0
- package/src/components/ui/legacy/meet/create-plan-dialog.tsx +27 -10
- package/src/components/ui/legacy/meet/edit-plan-dialog.tsx +90 -26
- package/src/components/ui/legacy/meet/form.tsx +25 -1
- package/src/components/ui/legacy/meet/lib/meet-ics.test.ts +40 -0
- package/src/components/ui/legacy/meet/lib/meet-ics.ts +50 -0
- package/src/components/ui/legacy/meet/lib/meet-insights.test.ts +76 -0
- package/src/components/ui/legacy/meet/lib/meet-insights.ts +219 -0
- package/src/components/ui/legacy/meet/lib/meet-timezone.test.ts +53 -0
- package/src/components/ui/legacy/meet/lib/meet-timezone.ts +45 -0
- package/src/components/ui/legacy/meet/page.tsx +1 -0
- package/src/components/ui/legacy/meet/planId/agenda-details.tsx +46 -23
- package/src/components/ui/legacy/meet/planId/copy-link-button.tsx +24 -197
- package/src/components/ui/legacy/meet/planId/date-planner.tsx +3 -28
- package/src/components/ui/legacy/meet/planId/meet-insights-panel.tsx +135 -0
- package/src/components/ui/legacy/meet/planId/meet-plan-live-root.tsx +41 -0
- package/src/components/ui/legacy/meet/planId/meet-query.ts +26 -0
- package/src/components/ui/legacy/meet/planId/meet-suggestions-panel.tsx +463 -0
- package/src/components/ui/legacy/meet/planId/page.tsx +58 -18
- package/src/components/ui/legacy/meet/planId/plan-details-client.tsx +154 -140
- package/src/components/ui/legacy/meet/planId/selectable-day-time.tsx +88 -56
- package/src/components/ui/legacy/meet/planId/show-qr-button.tsx +3 -5
- package/src/components/ui/legacy/meet/planId/unified-availability.tsx +36 -13
- package/src/components/ui/legacy/meet/planId/utility-buttons.tsx +3 -24
- package/src/components/ui/legacy/meet/plans-grid.tsx +1 -1
- package/src/components/ui/legacy/meet/plans-list-view.tsx +1 -1
- package/src/components/ui/nuqs-adapter.tsx +1 -0
- package/src/hooks/time-blocking-provider.tsx +127 -211
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import type { MeetTogetherPlan } from '@tuturuuu/types/primitives/MeetTogetherPlan';
|
|
4
|
-
import {
|
|
5
|
-
import { useEffect, useState } from 'react';
|
|
6
|
-
import CopyLinkButton, { generateTumeetMeUrl } from './copy-link-button';
|
|
4
|
+
import CopyLinkButton, { generateCanonicalMeetUrl } from './copy-link-button';
|
|
7
5
|
import DownloadAsPNG from './download-as-png';
|
|
8
6
|
import EmailButton from './email-button';
|
|
9
7
|
import LoggedInAsButton from './logged-in-as-button';
|
|
@@ -18,35 +16,16 @@ export default function UtilityButtons({
|
|
|
18
16
|
plan,
|
|
19
17
|
handlePNG,
|
|
20
18
|
}: UtilityButtonsProps) {
|
|
21
|
-
const pathname = usePathname();
|
|
22
|
-
const [url, setUrl] = useState('');
|
|
23
|
-
|
|
24
|
-
// const { user } = useTimeBlocking();
|
|
25
|
-
// const isCreator = !user?.is_guest && user?.id === plan.creator_id;
|
|
26
|
-
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
setUrl(`${window.location.origin}${pathname}`);
|
|
29
|
-
}, [pathname]);
|
|
30
|
-
|
|
31
|
-
const tumeetMeUrl = generateTumeetMeUrl(url);
|
|
32
|
-
|
|
33
19
|
if (!plan?.id) return null;
|
|
20
|
+
const url = generateCanonicalMeetUrl(plan.id);
|
|
34
21
|
|
|
35
22
|
return (
|
|
36
23
|
<div className="flex w-full flex-col items-center justify-between gap-4 md:flex-row md:items-start">
|
|
37
24
|
<div className="flex w-full flex-wrap items-start gap-2">
|
|
38
25
|
<CopyLinkButton url={url} />
|
|
39
26
|
<ShowQRButton url={url} />
|
|
40
|
-
<EmailButton plan={plan} url={
|
|
27
|
+
<EmailButton plan={plan} url={url} />
|
|
41
28
|
<DownloadAsPNG onClick={handlePNG} />
|
|
42
|
-
{/* {isCreator && (
|
|
43
|
-
<>
|
|
44
|
-
<ConfirmButton
|
|
45
|
-
planId={plan.id}
|
|
46
|
-
isConfirmPlan={Boolean(plan.is_confirmed)}
|
|
47
|
-
/>
|
|
48
|
-
</>
|
|
49
|
-
)} */}
|
|
50
29
|
</div>
|
|
51
30
|
<LoggedInAsButton />
|
|
52
31
|
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { NuqsAdapter } from 'nuqs/adapters/next/app';
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
3
4
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from '@tuturuuu/
|
|
5
|
+
getMeetPlanSnapshot,
|
|
6
|
+
type MeetPlanSnapshot,
|
|
7
|
+
replaceMeetAvailability,
|
|
8
|
+
} from '@tuturuuu/internal-api';
|
|
8
9
|
import type {
|
|
9
10
|
GuestUser,
|
|
10
11
|
MeetTogetherPlan,
|
|
@@ -35,58 +36,11 @@ dayjs.extend(minMax);
|
|
|
35
36
|
interface EditingParams {
|
|
36
37
|
enabled: boolean;
|
|
37
38
|
mode?: 'add' | 'remove';
|
|
38
|
-
initialTouch?: { x: number; y: number };
|
|
39
39
|
startDate?: Date;
|
|
40
40
|
endDate?: Date;
|
|
41
41
|
tentativeMode?: boolean;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
// Utility function to compare timeblock arrays
|
|
45
|
-
const areTimeBlockArraysEqual = (arr1: Timeblock[], arr2: Timeblock[]) => {
|
|
46
|
-
if (arr1.length !== arr2.length) return false;
|
|
47
|
-
|
|
48
|
-
return arr1.every((tb1) =>
|
|
49
|
-
arr2.some(
|
|
50
|
-
(tb2) =>
|
|
51
|
-
tb1.date === tb2.date &&
|
|
52
|
-
tb1.start_time === tb2.start_time &&
|
|
53
|
-
tb1.end_time === tb2.end_time
|
|
54
|
-
)
|
|
55
|
-
);
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
// Utility function to find timeblocks that exist in arr1 but not in arr2
|
|
59
|
-
const findTimeBlocksToRemove = (
|
|
60
|
-
serverTimeblocks: Timeblock[],
|
|
61
|
-
localTimeblocks: Timeblock[]
|
|
62
|
-
) => {
|
|
63
|
-
return serverTimeblocks.filter(
|
|
64
|
-
(serverTimeblock: Timeblock) =>
|
|
65
|
-
!localTimeblocks.some(
|
|
66
|
-
(localTimeblock: Timeblock) =>
|
|
67
|
-
localTimeblock.date === serverTimeblock.date &&
|
|
68
|
-
localTimeblock.start_time === serverTimeblock.start_time &&
|
|
69
|
-
localTimeblock.end_time === serverTimeblock.end_time
|
|
70
|
-
)
|
|
71
|
-
);
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
// Utility function to find timeblocks that exist in arr2 but not in arr1
|
|
75
|
-
const findTimeBlocksToAdd = (
|
|
76
|
-
localTimeblocks: Timeblock[],
|
|
77
|
-
serverTimeblocks: Timeblock[]
|
|
78
|
-
) => {
|
|
79
|
-
return localTimeblocks.filter(
|
|
80
|
-
(localTimeblock: Timeblock) =>
|
|
81
|
-
!serverTimeblocks?.some(
|
|
82
|
-
(serverTimeblock: Timeblock) =>
|
|
83
|
-
serverTimeblock.date === localTimeblock.date &&
|
|
84
|
-
serverTimeblock.start_time === localTimeblock.start_time &&
|
|
85
|
-
serverTimeblock.end_time === localTimeblock.end_time
|
|
86
|
-
)
|
|
87
|
-
);
|
|
88
|
-
};
|
|
89
|
-
|
|
90
44
|
const TimeBlockContext = createContext({
|
|
91
45
|
user: null as PlatformUser | GuestUser | null,
|
|
92
46
|
originalPlatformUser: null as PlatformUser | null,
|
|
@@ -102,7 +56,11 @@ const TimeBlockContext = createContext({
|
|
|
102
56
|
displayMode: 'account-switcher' as 'login' | 'account-switcher' | undefined,
|
|
103
57
|
isDirty: false,
|
|
104
58
|
isSaving: false,
|
|
59
|
+
canUndo: false,
|
|
60
|
+
canRedo: false,
|
|
105
61
|
handleSave: () => {},
|
|
62
|
+
undo: () => {},
|
|
63
|
+
redo: () => {},
|
|
106
64
|
|
|
107
65
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
108
66
|
getPreviewUsers: (_: Timeblock[]) =>
|
|
@@ -122,10 +80,11 @@ const TimeBlockContext = createContext({
|
|
|
122
80
|
setPreviewDate: (_: Date | null) => {},
|
|
123
81
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
124
82
|
setSelectedTimeBlocks: (_: { planId?: string; data: Timeblock[] }) => {},
|
|
125
|
-
edit: (
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
83
|
+
edit: (_: {
|
|
84
|
+
mode: 'add' | 'remove';
|
|
85
|
+
date: Date;
|
|
86
|
+
tentativeMode?: boolean;
|
|
87
|
+
}) => {},
|
|
129
88
|
endEditing: () => {},
|
|
130
89
|
setDisplayMode: (
|
|
131
90
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
@@ -158,6 +117,7 @@ const TimeBlockingProvider = ({
|
|
|
158
117
|
children: ReactNode;
|
|
159
118
|
}) => {
|
|
160
119
|
const router = useRouter();
|
|
120
|
+
const queryClient = useQueryClient();
|
|
161
121
|
const [planUsers, setInternalUsers] = useState(users);
|
|
162
122
|
const [filteredUserIds, setFilteredUserIds] = useState<string[]>([]);
|
|
163
123
|
|
|
@@ -173,6 +133,8 @@ const TimeBlockingProvider = ({
|
|
|
173
133
|
}, [users]);
|
|
174
134
|
|
|
175
135
|
const [previewDate, setPreviewDate] = useState<Date | null>(null);
|
|
136
|
+
const [undoStack, setUndoStack] = useState<Timeblock[][]>([]);
|
|
137
|
+
const [redoStack, setRedoStack] = useState<Timeblock[][]>([]);
|
|
176
138
|
|
|
177
139
|
const setPreviewDateCallback = useCallback((date: Date | null) => {
|
|
178
140
|
setPreviewDate(date);
|
|
@@ -390,66 +352,25 @@ const TimeBlockingProvider = ({
|
|
|
390
352
|
);
|
|
391
353
|
|
|
392
354
|
const edit = useCallback(
|
|
393
|
-
(
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
event && 'touches' in event ? event.touches?.[0] : undefined;
|
|
403
|
-
|
|
355
|
+
({
|
|
356
|
+
mode,
|
|
357
|
+
date,
|
|
358
|
+
tentativeMode,
|
|
359
|
+
}: {
|
|
360
|
+
mode: 'add' | 'remove';
|
|
361
|
+
date: Date;
|
|
362
|
+
tentativeMode?: boolean;
|
|
363
|
+
}) => {
|
|
404
364
|
setEditing((prevData) => {
|
|
405
365
|
const nextMode = prevData?.mode ?? mode;
|
|
406
366
|
const nextTentativeMode = prevData?.tentativeMode ?? tentativeMode;
|
|
407
|
-
const nextTouch =
|
|
408
|
-
prevData?.initialTouch ??
|
|
409
|
-
(touch
|
|
410
|
-
? {
|
|
411
|
-
x: touch.clientX,
|
|
412
|
-
y: touch.clientY,
|
|
413
|
-
}
|
|
414
|
-
: undefined);
|
|
415
|
-
|
|
416
367
|
const nextStart = prevData?.startDate ?? date;
|
|
417
368
|
|
|
418
|
-
const touchXDiff =
|
|
419
|
-
(touch?.clientX || 0) - (prevData?.initialTouch?.x || 0);
|
|
420
|
-
|
|
421
|
-
const touchYDiff =
|
|
422
|
-
(touch?.clientY || 0) - (prevData?.initialTouch?.y || 0);
|
|
423
|
-
|
|
424
|
-
const nextEnd =
|
|
425
|
-
prevData?.initialTouch !== undefined && nextTouch
|
|
426
|
-
? nextStart
|
|
427
|
-
? // Only apply complex calculation if there's significant movement
|
|
428
|
-
// For taps with minimal movement, use the same date as start
|
|
429
|
-
// Increased thresholds to prevent accidental multi-selection on mobile
|
|
430
|
-
Math.abs(touchXDiff) > 30 || Math.abs(touchYDiff) > 30
|
|
431
|
-
? dayjs(nextStart)
|
|
432
|
-
.add(Math.floor((touchYDiff / 15) * 1.25) * 15, 'minute')
|
|
433
|
-
.add(
|
|
434
|
-
// Only add days if horizontal movement is significant (more than 80px)
|
|
435
|
-
// and vertical movement is minimal (less than 40px) to avoid accidental cross-day selection
|
|
436
|
-
// Also ensure we're actually dragging (not just a tap)
|
|
437
|
-
Math.abs(touchXDiff) > 80 && Math.abs(touchYDiff) < 40
|
|
438
|
-
? Math.floor(touchXDiff / 100)
|
|
439
|
-
: 0,
|
|
440
|
-
'day'
|
|
441
|
-
)
|
|
442
|
-
.toDate()
|
|
443
|
-
: nextStart // For single taps, end = start
|
|
444
|
-
: nextStart
|
|
445
|
-
: date;
|
|
446
|
-
|
|
447
369
|
return {
|
|
448
370
|
enabled: true,
|
|
449
371
|
mode: nextMode,
|
|
450
372
|
startDate: nextStart,
|
|
451
|
-
endDate:
|
|
452
|
-
initialTouch: nextTouch,
|
|
373
|
+
endDate: date,
|
|
453
374
|
tentativeMode: nextTentativeMode,
|
|
454
375
|
};
|
|
455
376
|
});
|
|
@@ -482,6 +403,8 @@ const TimeBlockingProvider = ({
|
|
|
482
403
|
endEditingInProgressRef.current = true;
|
|
483
404
|
|
|
484
405
|
try {
|
|
406
|
+
setUndoStack((stack) => [...stack.slice(-19), selectedTimeBlocks.data]);
|
|
407
|
+
setRedoStack([]);
|
|
485
408
|
setSelectedTimeBlocks((prevTimeblocks) => {
|
|
486
409
|
const dates = [
|
|
487
410
|
editing.startDate,
|
|
@@ -536,7 +459,23 @@ const TimeBlockingProvider = ({
|
|
|
536
459
|
endEditingInProgressRef.current = false;
|
|
537
460
|
}
|
|
538
461
|
}, 100); // Increased delay to better prevent rapid calls
|
|
539
|
-
}, [plan.id, editing]);
|
|
462
|
+
}, [plan.id, editing, selectedTimeBlocks.data]);
|
|
463
|
+
|
|
464
|
+
const undo = useCallback(() => {
|
|
465
|
+
const previous = undoStack.at(-1);
|
|
466
|
+
if (!previous || !plan.id) return;
|
|
467
|
+
setRedoStack((stack) => [...stack.slice(-19), selectedTimeBlocks.data]);
|
|
468
|
+
setUndoStack((stack) => stack.slice(0, -1));
|
|
469
|
+
setSelectedTimeBlocks({ planId: plan.id, data: previous });
|
|
470
|
+
}, [plan.id, selectedTimeBlocks.data, undoStack]);
|
|
471
|
+
|
|
472
|
+
const redo = useCallback(() => {
|
|
473
|
+
const next = redoStack.at(-1);
|
|
474
|
+
if (!next || !plan.id) return;
|
|
475
|
+
setUndoStack((stack) => [...stack.slice(-19), selectedTimeBlocks.data]);
|
|
476
|
+
setRedoStack((stack) => stack.slice(0, -1));
|
|
477
|
+
setSelectedTimeBlocks({ planId: plan.id, data: next });
|
|
478
|
+
}, [plan.id, redoStack, selectedTimeBlocks.data]);
|
|
540
479
|
|
|
541
480
|
// Cleanup timeout on component unmount to prevent memory leaks
|
|
542
481
|
useEffect(() => {
|
|
@@ -567,16 +506,14 @@ const TimeBlockingProvider = ({
|
|
|
567
506
|
|
|
568
507
|
const fetchCurrentTimeBlocks = useCallback(
|
|
569
508
|
async (planId: string) => {
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
return
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
tb.user_id === user?.id && tb.is_guest === (user?.is_guest ?? false)
|
|
577
|
-
);
|
|
509
|
+
const snapshot = await getMeetPlanSnapshot(planId);
|
|
510
|
+
queryClient.setQueryData(['meet-plan', planId], snapshot);
|
|
511
|
+
return snapshot.timeblocks.filter(
|
|
512
|
+
(tb: Timeblock) =>
|
|
513
|
+
tb.user_id === user?.id && tb.is_guest === (user?.is_guest ?? false)
|
|
514
|
+
);
|
|
578
515
|
},
|
|
579
|
-
[user?.id, user?.is_guest]
|
|
516
|
+
[queryClient, user?.id, user?.is_guest]
|
|
580
517
|
);
|
|
581
518
|
|
|
582
519
|
const resetLocalTimeblocks = useCallback(async () => {
|
|
@@ -589,112 +526,79 @@ const TimeBlockingProvider = ({
|
|
|
589
526
|
setIsDirty(false);
|
|
590
527
|
}, [fetchCurrentTimeBlocks, plan.id, user?.id]);
|
|
591
528
|
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
user_id: userId,
|
|
605
|
-
password_hash: passwordHash,
|
|
606
|
-
timeblocks: timeblocksToAdd,
|
|
529
|
+
const availabilityMutation = useMutation({
|
|
530
|
+
mutationFn: async (nextTimeblocks: Timeblock[]) => {
|
|
531
|
+
if (!plan.id || !user?.id) throw new Error('A plan identity is required');
|
|
532
|
+
return replaceMeetAvailability(plan.id, {
|
|
533
|
+
guestId: user.is_guest ? user.id : undefined,
|
|
534
|
+
passwordHash: user.is_guest ? user.password_hash : undefined,
|
|
535
|
+
timeblocks: nextTimeblocks.map((timeblock) => ({
|
|
536
|
+
date: timeblock.date,
|
|
537
|
+
start_time: timeblock.start_time,
|
|
538
|
+
end_time: timeblock.end_time,
|
|
539
|
+
tentative: timeblock.tentative,
|
|
540
|
+
})),
|
|
607
541
|
});
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
await
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
542
|
+
},
|
|
543
|
+
onMutate: async (nextTimeblocks) => {
|
|
544
|
+
if (!plan.id || !user?.id) return {};
|
|
545
|
+
const queryKey = ['meet-plan', plan.id] as const;
|
|
546
|
+
await queryClient.cancelQueries({ queryKey });
|
|
547
|
+
const previous = queryClient.getQueryData<MeetPlanSnapshot>(queryKey);
|
|
548
|
+
if (previous) {
|
|
549
|
+
queryClient.setQueryData<MeetPlanSnapshot>(queryKey, {
|
|
550
|
+
...previous,
|
|
551
|
+
timeblocks: [
|
|
552
|
+
...previous.timeblocks.filter(
|
|
553
|
+
(timeblock) =>
|
|
554
|
+
timeblock.user_id !== user.id ||
|
|
555
|
+
timeblock.is_guest !== Boolean(user.is_guest)
|
|
556
|
+
),
|
|
557
|
+
...nextTimeblocks.map((timeblock) => ({
|
|
558
|
+
...timeblock,
|
|
559
|
+
plan_id: plan.id,
|
|
560
|
+
user_id: user.id,
|
|
561
|
+
is_guest: Boolean(user.is_guest),
|
|
562
|
+
})),
|
|
563
|
+
],
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
return { previous };
|
|
567
|
+
},
|
|
568
|
+
onError: (_error, _next, context) => {
|
|
569
|
+
if (plan.id && context?.previous) {
|
|
570
|
+
queryClient.setQueryData(['meet-plan', plan.id], context.previous);
|
|
571
|
+
}
|
|
572
|
+
},
|
|
573
|
+
onSuccess: (snapshot) => {
|
|
574
|
+
if (plan.id) queryClient.setQueryData(['meet-plan', plan.id], snapshot);
|
|
575
|
+
},
|
|
576
|
+
});
|
|
617
577
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
// No changes needed, clear dirty state
|
|
623
|
-
clearDirtyStateWithTimeblocks([]);
|
|
624
|
-
return;
|
|
625
|
-
}
|
|
626
|
-
if (serverTimeblocks.length === 0 && localTimeblocks.length > 0) {
|
|
627
|
-
await addTimeBlocksBatch(localTimeblocks);
|
|
628
|
-
const syncedServerTimeblocks = await fetchCurrentTimeBlocks(plan?.id);
|
|
629
|
-
setSelectedTimeBlocks({
|
|
630
|
-
planId: plan.id,
|
|
631
|
-
data: syncedServerTimeblocks,
|
|
632
|
-
});
|
|
633
|
-
clearDirtyStateWithTimeblocks(syncedServerTimeblocks);
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
636
|
-
if (serverTimeblocks.length > 0 && localTimeblocks.length === 0) {
|
|
637
|
-
await Promise.all(
|
|
638
|
-
serverTimeblocks.map((timeblock: Timeblock) =>
|
|
639
|
-
removeTimeBlock(timeblock)
|
|
640
|
-
)
|
|
641
|
-
);
|
|
642
|
-
const syncedServerTimeblocks = await fetchCurrentTimeBlocks(plan?.id);
|
|
643
|
-
setSelectedTimeBlocks({
|
|
644
|
-
planId: plan.id,
|
|
645
|
-
data: syncedServerTimeblocks,
|
|
646
|
-
});
|
|
647
|
-
clearDirtyStateWithTimeblocks(syncedServerTimeblocks);
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
|
-
if (areTimeBlockArraysEqual(serverTimeblocks, localTimeblocks)) {
|
|
651
|
-
// No changes needed, clear dirty state
|
|
652
|
-
clearDirtyStateWithTimeblocks(serverTimeblocks);
|
|
653
|
-
return;
|
|
654
|
-
}
|
|
655
|
-
const timeblocksToRemove = findTimeBlocksToRemove(
|
|
656
|
-
serverTimeblocks,
|
|
657
|
-
localTimeblocks
|
|
578
|
+
const syncTimeBlocks = useCallback(async () => {
|
|
579
|
+
if (!plan.id || !user?.id) return;
|
|
580
|
+
const snapshot = await availabilityMutation.mutateAsync(
|
|
581
|
+
selectedTimeBlocks.data
|
|
658
582
|
);
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
|
|
583
|
+
const synced = snapshot.timeblocks.filter(
|
|
584
|
+
(timeblock) =>
|
|
585
|
+
timeblock.user_id === user.id &&
|
|
586
|
+
timeblock.is_guest === Boolean(user.is_guest)
|
|
662
587
|
);
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
clearDirtyStateWithTimeblocks(serverTimeblocks);
|
|
666
|
-
return;
|
|
667
|
-
}
|
|
668
|
-
if (timeblocksToRemove.length > 0)
|
|
669
|
-
await Promise.all(
|
|
670
|
-
timeblocksToRemove.map((timeblock) =>
|
|
671
|
-
timeblock.id ? removeTimeBlock(timeblock) : null
|
|
672
|
-
)
|
|
673
|
-
);
|
|
674
|
-
if (timeblocksToAdd.length > 0) await addTimeBlocksBatch(timeblocksToAdd);
|
|
675
|
-
const syncedServerTimeblocks = await fetchCurrentTimeBlocks(plan?.id);
|
|
676
|
-
setSelectedTimeBlocks({
|
|
677
|
-
planId: plan.id,
|
|
678
|
-
data: syncedServerTimeblocks,
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
// Clear dirty state with the synced timeblocks
|
|
682
|
-
clearDirtyStateWithTimeblocks(syncedServerTimeblocks);
|
|
588
|
+
setSelectedTimeBlocks({ planId: plan.id, data: synced });
|
|
589
|
+
clearDirtyStateWithTimeblocks(synced);
|
|
683
590
|
}, [
|
|
684
|
-
|
|
591
|
+
availabilityMutation,
|
|
592
|
+
clearDirtyStateWithTimeblocks,
|
|
685
593
|
plan.id,
|
|
594
|
+
selectedTimeBlocks.data,
|
|
686
595
|
user,
|
|
687
|
-
selectedTimeBlocks,
|
|
688
|
-
clearDirtyStateWithTimeblocks,
|
|
689
596
|
]);
|
|
690
597
|
|
|
691
|
-
// --- Remove the auto-sync useEffect ---
|
|
692
|
-
// useEffect(() => { ... if (editing.enabled) return; syncTimeBlocks(); }, [plan.id, user, selectedTimeBlocks, editing.enabled]);
|
|
693
|
-
|
|
694
598
|
const [isSaving, setIsSaving] = useState(false);
|
|
695
599
|
|
|
696
600
|
// Handle manual save
|
|
697
|
-
const handleSave = async () => {
|
|
601
|
+
const handleSave = useCallback(async () => {
|
|
698
602
|
setIsSaving(true);
|
|
699
603
|
try {
|
|
700
604
|
await syncTimeBlocks();
|
|
@@ -704,7 +608,15 @@ const TimeBlockingProvider = ({
|
|
|
704
608
|
} finally {
|
|
705
609
|
setIsSaving(false);
|
|
706
610
|
}
|
|
707
|
-
};
|
|
611
|
+
}, [router, syncTimeBlocks]);
|
|
612
|
+
|
|
613
|
+
useEffect(() => {
|
|
614
|
+
if (editing.enabled || !isDirty || !user?.id || plan.is_confirmed) return;
|
|
615
|
+
const timeout = window.setTimeout(() => {
|
|
616
|
+
void handleSave();
|
|
617
|
+
}, 350);
|
|
618
|
+
return () => window.clearTimeout(timeout);
|
|
619
|
+
}, [editing.enabled, handleSave, isDirty, plan.is_confirmed, user?.id]);
|
|
708
620
|
|
|
709
621
|
return (
|
|
710
622
|
<TimeBlockContext.Provider
|
|
@@ -719,7 +631,11 @@ const TimeBlockingProvider = ({
|
|
|
719
631
|
displayMode,
|
|
720
632
|
isDirty,
|
|
721
633
|
isSaving,
|
|
634
|
+
canUndo: undoStack.length > 0,
|
|
635
|
+
canRedo: redoStack.length > 0,
|
|
722
636
|
handleSave,
|
|
637
|
+
undo,
|
|
638
|
+
redo,
|
|
723
639
|
getPreviewUsers,
|
|
724
640
|
getOpacityForDate,
|
|
725
641
|
|