@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
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MeetTogetherPlan,
|
|
3
|
+
PlanUser,
|
|
4
|
+
} from '@tuturuuu/types/primitives/MeetTogetherPlan';
|
|
5
|
+
import type { Timeblock } from '@tuturuuu/types/primitives/Timeblock';
|
|
6
|
+
|
|
7
|
+
export const MEET_SLOT_MINUTES = 15;
|
|
8
|
+
|
|
9
|
+
export interface MeetRankedTimeframe {
|
|
10
|
+
date: string;
|
|
11
|
+
startMinute: number;
|
|
12
|
+
endMinute: number;
|
|
13
|
+
confirmedUserIds: string[];
|
|
14
|
+
tentativeUserIds: string[];
|
|
15
|
+
unavailableUserIds: string[];
|
|
16
|
+
confirmedPercent: number;
|
|
17
|
+
weightedPercent: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface MeetPlanInsights {
|
|
21
|
+
participantCount: number;
|
|
22
|
+
respondedCount: number;
|
|
23
|
+
responsePercent: number;
|
|
24
|
+
peakAttendance: number;
|
|
25
|
+
peakAttendancePercent: number;
|
|
26
|
+
averageAvailabilityPercent: number;
|
|
27
|
+
overlapByDate: Array<{ date: string; peak: number }>;
|
|
28
|
+
availabilityByHour: Array<{ hour: number; percent: number }>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function minuteFromTime(value?: string) {
|
|
32
|
+
const match = value?.match(/^(\d{1,2}):(\d{2})/u);
|
|
33
|
+
if (!match) return 0;
|
|
34
|
+
return Number(match[1]) * 60 + Number(match[2]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function cellKey(date: string, minute: number) {
|
|
38
|
+
return `${date}:${minute}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildUserStatusMap(timeblocks: Timeblock[]) {
|
|
42
|
+
const status = new Map<string, 'confirmed' | 'tentative'>();
|
|
43
|
+
|
|
44
|
+
for (const block of timeblocks) {
|
|
45
|
+
if (!block.user_id || !block.date) continue;
|
|
46
|
+
const start = minuteFromTime(block.start_time);
|
|
47
|
+
const end = minuteFromTime(block.end_time);
|
|
48
|
+
|
|
49
|
+
for (let minute = start; minute < end; minute += MEET_SLOT_MINUTES) {
|
|
50
|
+
const key = `${block.user_id}:${cellKey(block.date, minute)}`;
|
|
51
|
+
if (!block.tentative || !status.has(key)) {
|
|
52
|
+
status.set(key, block.tentative ? 'tentative' : 'confirmed');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return status;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function rankMeetTimeframes({
|
|
61
|
+
plan,
|
|
62
|
+
users,
|
|
63
|
+
timeblocks,
|
|
64
|
+
includeTentative = true,
|
|
65
|
+
}: {
|
|
66
|
+
plan: MeetTogetherPlan;
|
|
67
|
+
users: PlanUser[];
|
|
68
|
+
timeblocks: Timeblock[];
|
|
69
|
+
includeTentative?: boolean;
|
|
70
|
+
}): MeetRankedTimeframe[] {
|
|
71
|
+
const userIds = users.flatMap((user) => (user.id ? [user.id] : []));
|
|
72
|
+
const participantCount = userIds.length;
|
|
73
|
+
if (participantCount === 0) return [];
|
|
74
|
+
|
|
75
|
+
const startMinute = minuteFromTime(plan.start_time);
|
|
76
|
+
const endMinute = minuteFromTime(plan.end_time);
|
|
77
|
+
const duration = Math.max(MEET_SLOT_MINUTES, plan.duration_minutes ?? 60);
|
|
78
|
+
const status = buildUserStatusMap(timeblocks);
|
|
79
|
+
const ranked: MeetRankedTimeframe[] = [];
|
|
80
|
+
|
|
81
|
+
for (const date of plan.dates ?? []) {
|
|
82
|
+
for (
|
|
83
|
+
let candidateStart = startMinute;
|
|
84
|
+
candidateStart + duration <= endMinute;
|
|
85
|
+
candidateStart += MEET_SLOT_MINUTES
|
|
86
|
+
) {
|
|
87
|
+
const confirmedUserIds: string[] = [];
|
|
88
|
+
const tentativeUserIds: string[] = [];
|
|
89
|
+
const unavailableUserIds: string[] = [];
|
|
90
|
+
|
|
91
|
+
for (const userId of userIds) {
|
|
92
|
+
let confirmed = true;
|
|
93
|
+
let available = true;
|
|
94
|
+
|
|
95
|
+
for (
|
|
96
|
+
let minute = candidateStart;
|
|
97
|
+
minute < candidateStart + duration;
|
|
98
|
+
minute += MEET_SLOT_MINUTES
|
|
99
|
+
) {
|
|
100
|
+
const cellStatus = status.get(`${userId}:${cellKey(date, minute)}`);
|
|
101
|
+
confirmed &&= cellStatus === 'confirmed';
|
|
102
|
+
available &&=
|
|
103
|
+
cellStatus === 'confirmed' || cellStatus === 'tentative';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (confirmed) confirmedUserIds.push(userId);
|
|
107
|
+
else if (available && includeTentative) tentativeUserIds.push(userId);
|
|
108
|
+
else unavailableUserIds.push(userId);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const confirmedPercent =
|
|
112
|
+
(confirmedUserIds.length / participantCount) * 100;
|
|
113
|
+
const weightedPercent =
|
|
114
|
+
((confirmedUserIds.length + tentativeUserIds.length * 0.5) /
|
|
115
|
+
participantCount) *
|
|
116
|
+
100;
|
|
117
|
+
|
|
118
|
+
ranked.push({
|
|
119
|
+
date,
|
|
120
|
+
startMinute: candidateStart,
|
|
121
|
+
endMinute: candidateStart + duration,
|
|
122
|
+
confirmedUserIds,
|
|
123
|
+
tentativeUserIds,
|
|
124
|
+
unavailableUserIds,
|
|
125
|
+
confirmedPercent,
|
|
126
|
+
weightedPercent,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return ranked.sort(
|
|
132
|
+
(a, b) =>
|
|
133
|
+
b.confirmedUserIds.length - a.confirmedUserIds.length ||
|
|
134
|
+
b.weightedPercent - a.weightedPercent ||
|
|
135
|
+
a.date.localeCompare(b.date) ||
|
|
136
|
+
a.startMinute - b.startMinute
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function calculateMeetPlanInsights({
|
|
141
|
+
plan,
|
|
142
|
+
users,
|
|
143
|
+
timeblocks,
|
|
144
|
+
}: {
|
|
145
|
+
plan: MeetTogetherPlan;
|
|
146
|
+
users: PlanUser[];
|
|
147
|
+
timeblocks: Timeblock[];
|
|
148
|
+
}): MeetPlanInsights {
|
|
149
|
+
const participantCount = users.filter((user) => user.id).length;
|
|
150
|
+
const respondedIds = new Set(
|
|
151
|
+
timeblocks.flatMap((block) => (block.user_id ? [block.user_id] : []))
|
|
152
|
+
);
|
|
153
|
+
const ranked = rankMeetTimeframes({ plan, users, timeblocks });
|
|
154
|
+
const overlapByDate = (plan.dates ?? []).map((date) => ({
|
|
155
|
+
date,
|
|
156
|
+
peak: Math.max(
|
|
157
|
+
0,
|
|
158
|
+
...ranked
|
|
159
|
+
.filter((candidate) => candidate.date === date)
|
|
160
|
+
.map((candidate) => candidate.confirmedUserIds.length)
|
|
161
|
+
),
|
|
162
|
+
}));
|
|
163
|
+
const peakAttendance = Math.max(0, ...overlapByDate.map(({ peak }) => peak));
|
|
164
|
+
const averageAvailabilityPercent = ranked.length
|
|
165
|
+
? ranked.reduce((sum, candidate) => sum + candidate.weightedPercent, 0) /
|
|
166
|
+
ranked.length
|
|
167
|
+
: 0;
|
|
168
|
+
const status = buildUserStatusMap(timeblocks);
|
|
169
|
+
const userIds = users.flatMap((user) => (user.id ? [user.id] : []));
|
|
170
|
+
const dates = plan.dates ?? [];
|
|
171
|
+
const startHour = Math.floor(minuteFromTime(plan.start_time) / 60);
|
|
172
|
+
const endHour = Math.ceil(minuteFromTime(plan.end_time) / 60);
|
|
173
|
+
const availabilityByHour = Array.from(
|
|
174
|
+
{ length: Math.max(0, endHour - startHour) },
|
|
175
|
+
(_, index) => {
|
|
176
|
+
const hour = startHour + index;
|
|
177
|
+
let weightedCells = 0;
|
|
178
|
+
for (const date of dates) {
|
|
179
|
+
for (const userId of userIds) {
|
|
180
|
+
for (let minute = hour * 60; minute < hour * 60 + 60; minute += 15) {
|
|
181
|
+
const cellStatus = status.get(`${userId}:${cellKey(date, minute)}`);
|
|
182
|
+
weightedCells +=
|
|
183
|
+
cellStatus === 'confirmed'
|
|
184
|
+
? 1
|
|
185
|
+
: cellStatus === 'tentative'
|
|
186
|
+
? 0.5
|
|
187
|
+
: 0;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const possibleCells = dates.length * userIds.length * 4;
|
|
192
|
+
return {
|
|
193
|
+
hour,
|
|
194
|
+
percent: possibleCells ? (weightedCells / possibleCells) * 100 : 0,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
participantCount,
|
|
201
|
+
respondedCount: respondedIds.size,
|
|
202
|
+
responsePercent: participantCount
|
|
203
|
+
? (respondedIds.size / participantCount) * 100
|
|
204
|
+
: 0,
|
|
205
|
+
peakAttendance,
|
|
206
|
+
peakAttendancePercent: participantCount
|
|
207
|
+
? (peakAttendance / participantCount) * 100
|
|
208
|
+
: 0,
|
|
209
|
+
averageAvailabilityPercent,
|
|
210
|
+
overlapByDate,
|
|
211
|
+
availabilityByHour,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function formatMinuteOfDay(minute: number) {
|
|
216
|
+
const hours = Math.floor(minute / 60);
|
|
217
|
+
const minutes = minute % 60;
|
|
218
|
+
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
|
|
219
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { MeetTogetherPlan } from '@tuturuuu/types/primitives/MeetTogetherPlan';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import type { MeetRankedTimeframe } from './meet-insights';
|
|
4
|
+
import { candidateToAbsoluteRange } from './meet-timezone';
|
|
5
|
+
|
|
6
|
+
const candidate: MeetRankedTimeframe = {
|
|
7
|
+
date: '2026-08-04',
|
|
8
|
+
startMinute: 540,
|
|
9
|
+
endMinute: 600,
|
|
10
|
+
confirmedUserIds: [],
|
|
11
|
+
tentativeUserIds: [],
|
|
12
|
+
unavailableUserIds: [],
|
|
13
|
+
confirmedPercent: 0,
|
|
14
|
+
weightedPercent: 0,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
describe('Tuturuuu Meet timezone conversion', () => {
|
|
18
|
+
it('preserves the fixed offset of a legacy plan', () => {
|
|
19
|
+
const plan: MeetTogetherPlan = {
|
|
20
|
+
duration_minutes: 60,
|
|
21
|
+
start_time: '09:00:00+07',
|
|
22
|
+
};
|
|
23
|
+
expect(candidateToAbsoluteRange(candidate, plan)).toEqual({
|
|
24
|
+
startAt: '2026-08-04T02:00:00.000Z',
|
|
25
|
+
endAt: '2026-08-04T03:00:00.000Z',
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('uses IANA daylight-saving rules', () => {
|
|
30
|
+
const plan: MeetTogetherPlan = {
|
|
31
|
+
duration_minutes: 60,
|
|
32
|
+
start_time: '09:00:00-04',
|
|
33
|
+
timezone: 'America/New_York',
|
|
34
|
+
};
|
|
35
|
+
expect(candidateToAbsoluteRange(candidate, plan)?.startAt).toBe(
|
|
36
|
+
'2026-08-04T13:00:00.000Z'
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('rejects a nonexistent DST window', () => {
|
|
41
|
+
const plan: MeetTogetherPlan = {
|
|
42
|
+
duration_minutes: 60,
|
|
43
|
+
start_time: '01:00:00-05',
|
|
44
|
+
timezone: 'America/New_York',
|
|
45
|
+
};
|
|
46
|
+
expect(
|
|
47
|
+
candidateToAbsoluteRange(
|
|
48
|
+
{ ...candidate, date: '2026-03-08', startMinute: 120, endMinute: 180 },
|
|
49
|
+
plan
|
|
50
|
+
)
|
|
51
|
+
).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { MeetTogetherPlan } from '@tuturuuu/types/primitives/MeetTogetherPlan';
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
import timezone from 'dayjs/plugin/timezone';
|
|
4
|
+
import utc from 'dayjs/plugin/utc';
|
|
5
|
+
import { formatMinuteOfDay, type MeetRankedTimeframe } from './meet-insights';
|
|
6
|
+
|
|
7
|
+
dayjs.extend(utc);
|
|
8
|
+
dayjs.extend(timezone);
|
|
9
|
+
|
|
10
|
+
function legacyOffset(plan: MeetTogetherPlan) {
|
|
11
|
+
const offset = plan.start_time?.match(/([+-]\d{2}(?::?\d{2})?)$/u)?.[1];
|
|
12
|
+
if (!offset) return 'Z';
|
|
13
|
+
if (/^[+-]\d{2}$/u.test(offset)) return `${offset}:00`;
|
|
14
|
+
if (/^[+-]\d{4}$/u.test(offset))
|
|
15
|
+
return `${offset.slice(0, 3)}:${offset.slice(3)}`;
|
|
16
|
+
return offset;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function localTimestamp(date: string, minute: number) {
|
|
20
|
+
return `${date}T${formatMinuteOfDay(minute)}:00`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function candidateToAbsoluteRange(
|
|
24
|
+
candidate: MeetRankedTimeframe,
|
|
25
|
+
plan: MeetTogetherPlan
|
|
26
|
+
) {
|
|
27
|
+
const startLocal = localTimestamp(candidate.date, candidate.startMinute);
|
|
28
|
+
const endLocal = localTimestamp(candidate.date, candidate.endMinute);
|
|
29
|
+
const start = plan.timezone
|
|
30
|
+
? dayjs.tz(startLocal, plan.timezone)
|
|
31
|
+
: dayjs(`${startLocal}${legacyOffset(plan)}`);
|
|
32
|
+
const end = plan.timezone
|
|
33
|
+
? dayjs.tz(endLocal, plan.timezone)
|
|
34
|
+
: dayjs(`${endLocal}${legacyOffset(plan)}`);
|
|
35
|
+
const expectedDuration = plan.duration_minutes ?? 60;
|
|
36
|
+
|
|
37
|
+
if (
|
|
38
|
+
!start.isValid() ||
|
|
39
|
+
!end.isValid() ||
|
|
40
|
+
end.diff(start, 'minute') !== expectedDuration
|
|
41
|
+
)
|
|
42
|
+
return null;
|
|
43
|
+
|
|
44
|
+
return { startAt: start.toISOString(), endAt: end.toISOString() };
|
|
45
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
4
4
|
import { ClipboardList, Pencil, Plus } from '@tuturuuu/icons';
|
|
5
|
+
import { type MeetPlanSnapshot, updateMeetPlan } from '@tuturuuu/internal-api';
|
|
5
6
|
import type { MeetTogetherPlan } from '@tuturuuu/types/primitives/MeetTogetherPlan';
|
|
6
7
|
import type { JSONContent } from '@tuturuuu/types/tiptap';
|
|
7
8
|
import { Button } from '@tuturuuu/ui/button';
|
|
@@ -16,14 +17,41 @@ interface AgendaDetailsProps {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export default function AgendaDetails({ plan }: AgendaDetailsProps) {
|
|
19
|
-
const t = useTranslations(
|
|
20
|
+
const t = useTranslations();
|
|
20
21
|
const router = useRouter();
|
|
22
|
+
const queryClient = useQueryClient();
|
|
21
23
|
const { user } = useTimeBlocking();
|
|
22
24
|
const [editContent, setEditContent] = useState<JSONContent | null>(
|
|
23
25
|
plan.agenda_content || null
|
|
24
26
|
);
|
|
25
27
|
const [isEditing, setIsEditing] = useState(false);
|
|
26
28
|
const [isLoading, setIsLoading] = useState(false);
|
|
29
|
+
const mutation = useMutation({
|
|
30
|
+
mutationFn: (agendaContent: JSONContent | null) =>
|
|
31
|
+
updateMeetPlan(plan.id!, { agenda_content: agendaContent }),
|
|
32
|
+
onMutate: async (agendaContent) => {
|
|
33
|
+
if (!plan.id) return {};
|
|
34
|
+
const queryKey = ['meet-plan', plan.id] as const;
|
|
35
|
+
await queryClient.cancelQueries({ queryKey });
|
|
36
|
+
const previous = queryClient.getQueryData<MeetPlanSnapshot>(queryKey);
|
|
37
|
+
if (previous)
|
|
38
|
+
queryClient.setQueryData<MeetPlanSnapshot>(queryKey, {
|
|
39
|
+
...previous,
|
|
40
|
+
plan: {
|
|
41
|
+
...previous.plan,
|
|
42
|
+
agenda_content: agendaContent ?? undefined,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
return { previous };
|
|
46
|
+
},
|
|
47
|
+
onError: (_error, _agenda, context) => {
|
|
48
|
+
if (plan.id && context?.previous)
|
|
49
|
+
queryClient.setQueryData(['meet-plan', plan.id], context.previous);
|
|
50
|
+
},
|
|
51
|
+
onSuccess: (snapshot) => {
|
|
52
|
+
if (plan.id) queryClient.setQueryData(['meet-plan', plan.id], snapshot);
|
|
53
|
+
},
|
|
54
|
+
});
|
|
27
55
|
|
|
28
56
|
const handleEdit = useCallback(() => {
|
|
29
57
|
setIsEditing(true);
|
|
@@ -40,23 +68,15 @@ export default function AgendaDetails({ plan }: AgendaDetailsProps) {
|
|
|
40
68
|
|
|
41
69
|
setIsLoading(true);
|
|
42
70
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
if (result.data) {
|
|
49
|
-
setIsEditing(false);
|
|
50
|
-
router.refresh();
|
|
51
|
-
} else {
|
|
52
|
-
console.error('Failed to save agenda:', result.error);
|
|
53
|
-
}
|
|
71
|
+
await mutation.mutateAsync(editContent);
|
|
72
|
+
setIsEditing(false);
|
|
73
|
+
router.refresh();
|
|
54
74
|
} catch (error) {
|
|
55
75
|
console.error('Error saving agenda:', error);
|
|
56
76
|
} finally {
|
|
57
77
|
setIsLoading(false);
|
|
58
78
|
}
|
|
59
|
-
}, [plan.id,
|
|
79
|
+
}, [editContent, mutation, plan.id, router]);
|
|
60
80
|
|
|
61
81
|
const handleContentChange = useCallback((content: JSONContent | null) => {
|
|
62
82
|
setEditContent(content || null);
|
|
@@ -69,9 +89,9 @@ export default function AgendaDetails({ plan }: AgendaDetailsProps) {
|
|
|
69
89
|
<div className="w-full space-y-8">
|
|
70
90
|
<div className="flex flex-col items-start justify-between gap-4 md:flex-row">
|
|
71
91
|
<div className="space-y-4">
|
|
72
|
-
<p className="font-semibold text-4xl">{t('agenda')}</p>
|
|
92
|
+
<p className="font-semibold text-4xl">{t('meet-together.agenda')}</p>
|
|
73
93
|
<p className="text-md text-muted-foreground">
|
|
74
|
-
{t('agenda_description')}
|
|
94
|
+
{t('meet-together.agenda_description')}
|
|
75
95
|
</p>
|
|
76
96
|
</div>
|
|
77
97
|
{isEditing ? (
|
|
@@ -82,22 +102,24 @@ export default function AgendaDetails({ plan }: AgendaDetailsProps) {
|
|
|
82
102
|
size="lg"
|
|
83
103
|
disabled={isLoading}
|
|
84
104
|
>
|
|
85
|
-
|
|
105
|
+
{t('common.cancel')}
|
|
86
106
|
</Button>
|
|
87
107
|
<Button onClick={handleSave} size="lg" disabled={isLoading}>
|
|
88
|
-
{isLoading
|
|
108
|
+
{isLoading
|
|
109
|
+
? t('meet-together-plan-details.saving')
|
|
110
|
+
: t('common.save')}
|
|
89
111
|
</Button>
|
|
90
112
|
</div>
|
|
91
113
|
) : canEdit ? (
|
|
92
114
|
plan.agenda_content ? (
|
|
93
115
|
<Button onClick={handleEdit} variant="outline" size="lg">
|
|
94
116
|
<Pencil size={16} />
|
|
95
|
-
|
|
117
|
+
{t('common.edit')}
|
|
96
118
|
</Button>
|
|
97
119
|
) : (
|
|
98
120
|
<Button onClick={handleEdit} variant="default" size="lg">
|
|
99
121
|
<Plus size={16} />
|
|
100
|
-
|
|
122
|
+
{t('meet-together-plan-details.add_agenda')}
|
|
101
123
|
</Button>
|
|
102
124
|
)
|
|
103
125
|
) : null}
|
|
@@ -117,10 +139,11 @@ export default function AgendaDetails({ plan }: AgendaDetailsProps) {
|
|
|
117
139
|
<ClipboardList size={48} className="text-muted-foreground" />
|
|
118
140
|
</div>
|
|
119
141
|
<div className="space-y-2">
|
|
120
|
-
<h3 className="font-medium text-lg">
|
|
142
|
+
<h3 className="font-medium text-lg">
|
|
143
|
+
{t('meet-together-plan-details.no_agenda_title')}
|
|
144
|
+
</h3>
|
|
121
145
|
<p className="max-w-md text-muted-foreground text-sm">
|
|
122
|
-
|
|
123
|
-
to keep everyone on track to make your meeting more productive.
|
|
146
|
+
{t('meet-together-plan-details.no_agenda_description')}
|
|
124
147
|
</p>
|
|
125
148
|
</div>
|
|
126
149
|
</div>
|