@agent-native/core 0.192.0 → 0.192.1-nightly-20260926122223
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/corpus/README.md +1 -1
- package/corpus/templates/calendar/actions/cancel-booking.ts +12 -4
- package/corpus/templates/calendar/actions/list-bookings.ts +23 -2
- package/corpus/templates/calendar/actions/list-events.ts +42 -29
- package/corpus/templates/calendar/actions/update-booking-link.ts +27 -1
- package/corpus/templates/calendar/app/hooks/use-bookings.ts +9 -2
- package/corpus/templates/calendar/app/i18n/en-US.ts +8 -0
- package/corpus/templates/calendar/app/lib/booking-sorting.ts +7 -0
- package/corpus/templates/calendar/app/lib/event-colors.ts +29 -4
- package/corpus/templates/calendar/app/pages/BookingsList.tsx +99 -19
- package/corpus/templates/calendar/app/pages/CalendarView.tsx +5 -11
- package/corpus/templates/calendar/app/pages/ManageBookingPage.tsx +5 -0
- package/corpus/templates/calendar/changelog/2026-09-25-ambiguous-zoom-bookings-need-review.md +6 -0
- package/corpus/templates/calendar/changelog/2026-09-25-bookings-list-shows-newest-bookings-first.md +6 -0
- package/corpus/templates/calendar/changelog/2026-09-25-canceling-a-booking-now-resolves-its-zoom-meeting-first.md +6 -0
- package/corpus/templates/calendar/changelog/2026-09-25-colleague-events-use-sidebar-colors.md +6 -0
- package/corpus/templates/calendar/changelog/2026-09-25-public-booking-pages-reject-malformed-meeting-settings-befor.md +6 -0
- package/corpus/templates/calendar/changelog/2026-09-25-shared-booking-cancellations-require-editor-access-and-unres.md +6 -0
- package/corpus/templates/calendar/server/db/schema.ts +5 -0
- package/corpus/templates/calendar/server/handlers/bookings.ts +142 -26
- package/corpus/templates/calendar/server/lib/booking-link-utils.ts +46 -1
- package/corpus/templates/calendar/server/lib/zoom.ts +66 -1
- package/corpus/templates/calendar/server/plugins/db.ts +13 -0
- package/corpus/templates/calendar/shared/api.ts +4 -0
- package/corpus/templates/clips/app/components/capture-install-options.tsx +9 -6
- package/corpus/templates/clips/app/components/clips-command-menu.tsx +4 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +5 -4
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +4 -0
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -30
- package/corpus/templates/clips/changelog/2026-09-26-desktop-platform-icon.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-26-the-empty-comments-state-now-explains-how-screen-recordings-.md +6 -0
- package/corpus/templates/design/app/routes/design-systems.tsx +2 -4
- package/corpus/templates/design/app/routes/design.$id.tsx +2 -1
- package/corpus/templates/design/app/routes/public-design-meta.ts +0 -39
- package/corpus/templates/design/app/routes/templates.tsx +2 -4
- package/corpus/templates/design/app/routes/visual-edit_.$id.tsx +2 -1
- package/corpus/templates/design/changelog/2026-09-26-opening-shared-designs-no-longer-fails-during-client-startup.md +6 -0
- package/corpus/templates/design/package.json +1 -1
- package/corpus/templates/design/scripts/check-client-server-boundary.ts +55 -0
- package/corpus/templates/design/server/lib/public-design-meta.server.ts +40 -0
- package/dist/automation/index.d.ts +1 -1
- package/dist/client/settings/UsageSection.js +18 -5
- package/dist/client/settings/recent-prompt-groups.d.ts +12 -0
- package/dist/client/settings/recent-prompt-groups.js +20 -0
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +14 -14
- package/dist/provider-api/actions/provider-api.d.ts +13 -13
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
- package/dist/server/core-routes-plugin.d.ts +1 -1
- package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
- package/package.json +1 -1
package/corpus/README.md
CHANGED
|
@@ -7,16 +7,24 @@ import { requireActionUserEmail } from "./event-action-helpers.js";
|
|
|
7
7
|
|
|
8
8
|
export default defineAction({
|
|
9
9
|
description:
|
|
10
|
-
"Cancel a booking and its linked calendar
|
|
11
|
-
schema: z.object({
|
|
10
|
+
"Cancel a booking and its linked calendar and Zoom meetings. Use this instead of delete-event or delete-events for booking-backed events. If Zoom needs review, first confirm the meeting was canceled or no meeting exists.",
|
|
11
|
+
schema: z.object({
|
|
12
|
+
id: z.string().min(1).describe("Booking id"),
|
|
13
|
+
zoomMeetingResolved: z
|
|
14
|
+
.boolean()
|
|
15
|
+
.optional()
|
|
16
|
+
.describe("Confirm the Zoom meeting was checked and resolved"),
|
|
17
|
+
}),
|
|
12
18
|
// toolCallable only gates the sandboxed tools-iframe bridge; it does not
|
|
13
19
|
// remove this from the agent's own tool list. The agent chat loop still
|
|
14
20
|
// needs a real gate before it can send a cancellation email and delete the
|
|
15
21
|
// linked Google event on its own, so require human approval too.
|
|
16
22
|
toolCallable: false,
|
|
17
23
|
needsApproval: true,
|
|
18
|
-
run: async ({ id }) => {
|
|
24
|
+
run: async ({ id, zoomMeetingResolved }) => {
|
|
19
25
|
requireActionUserEmail();
|
|
20
|
-
return cancelBookingById(id, getAppProductionUrl()
|
|
26
|
+
return cancelBookingById(id, getAppProductionUrl(), {
|
|
27
|
+
zoomMeetingResolved,
|
|
28
|
+
});
|
|
21
29
|
},
|
|
22
30
|
});
|
|
@@ -4,6 +4,7 @@ import { inArray } from "drizzle-orm";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
|
|
6
6
|
import { getDb, schema } from "../server/db/index.js";
|
|
7
|
+
import { needsZoomCancellationReview } from "../server/lib/zoom.js";
|
|
7
8
|
import type { Booking } from "../shared/api.js";
|
|
8
9
|
|
|
9
10
|
function rowToBooking(
|
|
@@ -22,9 +23,13 @@ function rowToBooking(
|
|
|
22
23
|
| "meetingLink"
|
|
23
24
|
| "meetingLinkPending"
|
|
24
25
|
| "googleEventId"
|
|
26
|
+
| "zoomNeedsReview"
|
|
27
|
+
| "zoomMeetingId"
|
|
28
|
+
| "zoomAccountId"
|
|
25
29
|
| "status"
|
|
26
30
|
| "createdAt"
|
|
27
31
|
>,
|
|
32
|
+
conferencing?: string | null,
|
|
28
33
|
): Booking {
|
|
29
34
|
let fieldResponses: Record<string, string | boolean> | undefined;
|
|
30
35
|
if (row.fieldResponses) {
|
|
@@ -50,6 +55,11 @@ function rowToBooking(
|
|
|
50
55
|
meetingLinkPending:
|
|
51
56
|
row.meetingLinkPending && !row.meetingLink ? true : undefined,
|
|
52
57
|
googleEventId: row.googleEventId ?? undefined,
|
|
58
|
+
zoomNeedsReview: row.zoomNeedsReview,
|
|
59
|
+
zoomCancellationNeedsReview: needsZoomCancellationReview({
|
|
60
|
+
...row,
|
|
61
|
+
conferencing,
|
|
62
|
+
}),
|
|
53
63
|
status: row.status,
|
|
54
64
|
createdAt: row.createdAt,
|
|
55
65
|
};
|
|
@@ -61,11 +71,17 @@ export default defineAction({
|
|
|
61
71
|
http: { method: "GET" },
|
|
62
72
|
run: async () => {
|
|
63
73
|
const accessibleLinks = await getDb()
|
|
64
|
-
.select({
|
|
74
|
+
.select({
|
|
75
|
+
slug: schema.bookingLinks.slug,
|
|
76
|
+
conferencing: schema.bookingLinks.conferencing,
|
|
77
|
+
})
|
|
65
78
|
.from(schema.bookingLinks)
|
|
66
79
|
.where(accessFilter(schema.bookingLinks, schema.bookingLinkShares));
|
|
67
80
|
const slugs = accessibleLinks.map((link) => link.slug);
|
|
68
81
|
if (slugs.length === 0) return [];
|
|
82
|
+
const conferencingBySlug = new Map(
|
|
83
|
+
accessibleLinks.map((link) => [link.slug, link.conferencing]),
|
|
84
|
+
);
|
|
69
85
|
|
|
70
86
|
const rows = await getDb()
|
|
71
87
|
.select({
|
|
@@ -82,13 +98,18 @@ export default defineAction({
|
|
|
82
98
|
meetingLink: schema.bookings.meetingLink,
|
|
83
99
|
meetingLinkPending: schema.bookings.meetingLinkPending,
|
|
84
100
|
googleEventId: schema.bookings.googleEventId,
|
|
101
|
+
zoomNeedsReview: schema.bookings.zoomNeedsReview,
|
|
102
|
+
zoomMeetingId: schema.bookings.zoomMeetingId,
|
|
103
|
+
zoomAccountId: schema.bookings.zoomAccountId,
|
|
85
104
|
status: schema.bookings.status,
|
|
86
105
|
createdAt: schema.bookings.createdAt,
|
|
87
106
|
})
|
|
88
107
|
.from(schema.bookings)
|
|
89
108
|
.where(inArray(schema.bookings.slug, slugs))
|
|
90
109
|
.orderBy(schema.bookings.start);
|
|
91
|
-
return rows.map(
|
|
110
|
+
return rows.map((row) =>
|
|
111
|
+
rowToBooking(row, conferencingBySlug.get(row.slug)),
|
|
112
|
+
);
|
|
92
113
|
},
|
|
93
114
|
});
|
|
94
115
|
|
|
@@ -16,6 +16,7 @@ import { getDb, schema } from "../server/db/index.js";
|
|
|
16
16
|
import { getCalendarTimezone } from "../server/lib/calendar-settings.js";
|
|
17
17
|
import * as googleCalendar from "../server/lib/google-calendar.js";
|
|
18
18
|
import { fetchICalEvents } from "../server/lib/ical-fetcher.js";
|
|
19
|
+
import { needsZoomCancellationReview } from "../server/lib/zoom.js";
|
|
19
20
|
import {
|
|
20
21
|
getCalendarAttendeeCount,
|
|
21
22
|
getCalendarAttendeeStatusCounts,
|
|
@@ -474,6 +475,7 @@ async function listLocalBookingEvents(
|
|
|
474
475
|
slug: schema.bookingLinks.slug,
|
|
475
476
|
title: schema.bookingLinks.title,
|
|
476
477
|
color: schema.bookingLinks.color,
|
|
478
|
+
conferencing: schema.bookingLinks.conferencing,
|
|
477
479
|
})
|
|
478
480
|
.from(schema.bookingLinks)
|
|
479
481
|
.where(accessFilter(schema.bookingLinks, schema.bookingLinkShares));
|
|
@@ -498,6 +500,9 @@ async function listLocalBookingEvents(
|
|
|
498
500
|
meetingLink: schema.bookings.meetingLink,
|
|
499
501
|
meetingLinkPending: schema.bookings.meetingLinkPending,
|
|
500
502
|
googleEventId: schema.bookings.googleEventId,
|
|
503
|
+
zoomNeedsReview: schema.bookings.zoomNeedsReview,
|
|
504
|
+
zoomMeetingId: schema.bookings.zoomMeetingId,
|
|
505
|
+
zoomAccountId: schema.bookings.zoomAccountId,
|
|
501
506
|
status: schema.bookings.status,
|
|
502
507
|
createdAt: schema.bookings.createdAt,
|
|
503
508
|
})
|
|
@@ -511,36 +516,44 @@ async function listLocalBookingEvents(
|
|
|
511
516
|
),
|
|
512
517
|
);
|
|
513
518
|
|
|
514
|
-
return rows
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
519
|
+
return rows
|
|
520
|
+
.filter((booking) => {
|
|
521
|
+
const link = linkBySlug.get(booking.slug);
|
|
522
|
+
return !needsZoomCancellationReview({
|
|
523
|
+
...booking,
|
|
524
|
+
conferencing: link?.conferencing,
|
|
525
|
+
});
|
|
526
|
+
})
|
|
527
|
+
.map((booking) => {
|
|
528
|
+
const link = linkBySlug.get(booking.slug);
|
|
529
|
+
const description = [
|
|
530
|
+
booking.notes,
|
|
531
|
+
`Booked by ${booking.name} <${booking.email}>`,
|
|
532
|
+
]
|
|
533
|
+
.filter(Boolean)
|
|
534
|
+
.join("\n\n");
|
|
522
535
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
536
|
+
return {
|
|
537
|
+
id: `booking:${booking.id}`,
|
|
538
|
+
title:
|
|
539
|
+
booking.eventTitle || link?.title || `Booking with ${booking.name}`,
|
|
540
|
+
description,
|
|
541
|
+
start: booking.start,
|
|
542
|
+
end: booking.end,
|
|
543
|
+
location: booking.meetingLink ?? "",
|
|
544
|
+
allDay: false,
|
|
545
|
+
source: "local",
|
|
546
|
+
googleEventId: booking.googleEventId ?? undefined,
|
|
547
|
+
meetingLink: booking.meetingLink ?? undefined,
|
|
548
|
+
meetingLinkPending:
|
|
549
|
+
booking.meetingLinkPending && !booking.meetingLink ? true : undefined,
|
|
550
|
+
color: link?.color ?? undefined,
|
|
551
|
+
status: booking.status,
|
|
552
|
+
attendees: [{ email: booking.email, displayName: booking.name }],
|
|
553
|
+
createdAt: booking.createdAt,
|
|
554
|
+
updatedAt: booking.createdAt,
|
|
555
|
+
};
|
|
556
|
+
});
|
|
544
557
|
}
|
|
545
558
|
|
|
546
559
|
/**
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core/action";
|
|
2
2
|
import { assertAccess } from "@agent-native/core/sharing";
|
|
3
|
-
import { eq } from "drizzle-orm";
|
|
3
|
+
import { and, eq, isNull, ne, or } from "drizzle-orm";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
|
|
6
6
|
import { getDb, schema } from "../server/db/index.js";
|
|
7
7
|
import { normalizeBookingDurationInput } from "../server/lib/booking-durations.js";
|
|
8
8
|
import {
|
|
9
|
+
parseBookingConferencingConfig,
|
|
9
10
|
rowToBookingLink,
|
|
10
11
|
serializeBookingHosts,
|
|
11
12
|
} from "../server/lib/booking-link-utils.js";
|
|
@@ -91,6 +92,7 @@ export default defineAction({
|
|
|
91
92
|
slug: schema.bookingLinks.slug,
|
|
92
93
|
ownerEmail: schema.bookingLinks.ownerEmail,
|
|
93
94
|
isActive: schema.bookingLinks.isActive,
|
|
95
|
+
conferencing: schema.bookingLinks.conferencing,
|
|
94
96
|
})
|
|
95
97
|
.from(schema.bookingLinks)
|
|
96
98
|
.where(eq(schema.bookingLinks.id, args.id));
|
|
@@ -98,6 +100,30 @@ export default defineAction({
|
|
|
98
100
|
if (!current) throw new Error("Booking link not found");
|
|
99
101
|
const oldSlug = current.slug;
|
|
100
102
|
const slugChanged = oldSlug !== slug;
|
|
103
|
+
const savedConferencing = parseBookingConferencingConfig(
|
|
104
|
+
current.conferencing,
|
|
105
|
+
);
|
|
106
|
+
const mayHaveZoomBookings =
|
|
107
|
+
savedConferencing.status === "invalid" ||
|
|
108
|
+
(savedConferencing.status === "valid" &&
|
|
109
|
+
savedConferencing.config.type === "zoom");
|
|
110
|
+
|
|
111
|
+
if (mayHaveZoomBookings && args.conferencing?.type !== "zoom") {
|
|
112
|
+
// Preserve the review gate before the booking link loses its Zoom type.
|
|
113
|
+
await getDb()
|
|
114
|
+
.update(schema.bookings)
|
|
115
|
+
.set({ zoomNeedsReview: true })
|
|
116
|
+
.where(
|
|
117
|
+
and(
|
|
118
|
+
eq(schema.bookings.slug, oldSlug),
|
|
119
|
+
ne(schema.bookings.status, "cancelled"),
|
|
120
|
+
or(
|
|
121
|
+
isNull(schema.bookings.zoomMeetingId),
|
|
122
|
+
isNull(schema.bookings.zoomAccountId),
|
|
123
|
+
),
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
101
127
|
|
|
102
128
|
await getDb()
|
|
103
129
|
.update(schema.bookingLinks)
|
|
@@ -120,9 +120,16 @@ export function useCreateBooking() {
|
|
|
120
120
|
export function useDeleteBooking() {
|
|
121
121
|
const queryClient = useQueryClient();
|
|
122
122
|
return useMutation({
|
|
123
|
-
mutationFn: async (
|
|
124
|
-
|
|
123
|
+
mutationFn: async (input: {
|
|
124
|
+
id: string;
|
|
125
|
+
zoomMeetingResolved?: boolean;
|
|
126
|
+
}) => {
|
|
127
|
+
const res = await fetch(appApiPath(`/api/bookings/${input.id}`), {
|
|
125
128
|
method: "DELETE",
|
|
129
|
+
headers: { "Content-Type": "application/json" },
|
|
130
|
+
body: JSON.stringify({
|
|
131
|
+
...(input.zoomMeetingResolved ? { zoomMeetingResolved: true } : {}),
|
|
132
|
+
}),
|
|
126
133
|
});
|
|
127
134
|
if (!res.ok) throw new Error("Failed to cancel booking");
|
|
128
135
|
return res.json();
|
|
@@ -513,6 +513,14 @@ export default {
|
|
|
513
513
|
"Your time is reserved. The host will follow up with meeting details.",
|
|
514
514
|
confirmed: "Confirmed",
|
|
515
515
|
confirmedCount: "Confirmed ({{count}})",
|
|
516
|
+
zoomNeedsReview: "Check Zoom before retrying",
|
|
517
|
+
zoomCancellationNeedsReview: "Check Zoom before canceling",
|
|
518
|
+
zoomCancellationRequiresHostReview:
|
|
519
|
+
"The organizer must review the Zoom meeting before this booking can be canceled.",
|
|
520
|
+
zoomCancelTitle: "Check Zoom before canceling",
|
|
521
|
+
zoomCancelDescription:
|
|
522
|
+
"Zoom may have created a meeting for this booking. Check your Zoom account and cancel the meeting there if it exists. Continue only after the meeting is canceled or you confirm no meeting exists.",
|
|
523
|
+
zoomCancelConfirm: "I've checked Zoom",
|
|
516
524
|
confirming: "Confirming",
|
|
517
525
|
conferencing: "Conferencing",
|
|
518
526
|
connectZoom: "Connect Zoom",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CalendarEvent } from "@shared/api";
|
|
1
|
+
import type { CalendarEvent, OverlayPerson } from "@shared/api";
|
|
2
2
|
export {
|
|
3
3
|
GOOGLE_EVENT_COLOR_OPTIONS,
|
|
4
4
|
getGoogleEventColorHex,
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
CalendarColorMode,
|
|
8
8
|
CalendarColorSourceKey,
|
|
9
9
|
} from "./calendar-view-preferences";
|
|
10
|
+
import { isPersonCalendarId } from "./person-calendar";
|
|
10
11
|
|
|
11
12
|
// ─── Palette (dark-mode editor inspired) ─────────────────────────────────────
|
|
12
13
|
|
|
@@ -32,6 +33,32 @@ export interface CalendarColorPreferences {
|
|
|
32
33
|
googleCalendarColors?: Record<string, string>;
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
export function applyOverlayOwnerMarkers(
|
|
37
|
+
events: CalendarEvent[],
|
|
38
|
+
people: OverlayPerson[],
|
|
39
|
+
): CalendarEvent[] {
|
|
40
|
+
const ownersByEmail = new Map(
|
|
41
|
+
people.map((person) => [person.email.trim().toLowerCase(), person]),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return events.map((event) => {
|
|
45
|
+
const ownerEmail =
|
|
46
|
+
event.overlayEmail ??
|
|
47
|
+
(event.source === "google" &&
|
|
48
|
+
event.calendarPrimary === false &&
|
|
49
|
+
event.calendarId &&
|
|
50
|
+
isPersonCalendarId(event.calendarId)
|
|
51
|
+
? event.calendarId
|
|
52
|
+
: undefined);
|
|
53
|
+
const owner = ownerEmail
|
|
54
|
+
? ownersByEmail.get(ownerEmail.trim().toLowerCase())
|
|
55
|
+
: undefined;
|
|
56
|
+
return owner
|
|
57
|
+
? { ...event, ownerColor: owner.color, ownerName: owner.name }
|
|
58
|
+
: event;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
35
62
|
// ─── Free email providers (skip internal/external when user is on one) ───────
|
|
36
63
|
|
|
37
64
|
const FREE_DOMAINS = new Set([
|
|
@@ -135,9 +162,7 @@ export function getEventDisplayColor(
|
|
|
135
162
|
event: CalendarEvent,
|
|
136
163
|
preferences?: CalendarColorPreferences,
|
|
137
164
|
): string {
|
|
138
|
-
if (event.
|
|
139
|
-
return event.ownerColor;
|
|
140
|
-
}
|
|
165
|
+
if (event.ownerColor) return event.ownerColor;
|
|
141
166
|
|
|
142
167
|
if (event.source === "google" && !event.overlayEmail && preferences) {
|
|
143
168
|
const sourceColor = event.canonicalKey
|
|
@@ -5,6 +5,17 @@ import { format, parseISO } from "date-fns";
|
|
|
5
5
|
import { useMemo, useState } from "react";
|
|
6
6
|
import { toast } from "sonner";
|
|
7
7
|
|
|
8
|
+
import {
|
|
9
|
+
AlertDialog,
|
|
10
|
+
AlertDialogAction,
|
|
11
|
+
AlertDialogCancel,
|
|
12
|
+
AlertDialogContent,
|
|
13
|
+
AlertDialogDescription,
|
|
14
|
+
AlertDialogFooter,
|
|
15
|
+
AlertDialogHeader,
|
|
16
|
+
AlertDialogTitle,
|
|
17
|
+
AlertDialogTrigger,
|
|
18
|
+
} from "@/components/ui/alert-dialog";
|
|
8
19
|
import { Badge } from "@/components/ui/badge";
|
|
9
20
|
import { Button } from "@/components/ui/button";
|
|
10
21
|
import {
|
|
@@ -30,6 +41,7 @@ import {
|
|
|
30
41
|
} from "@/components/ui/tooltip";
|
|
31
42
|
import { useBookingLinks } from "@/hooks/use-booking-links";
|
|
32
43
|
import { useBookings, useDeleteBooking } from "@/hooks/use-bookings";
|
|
44
|
+
import { sortBookingsNewestFirst } from "@/lib/booking-sorting";
|
|
33
45
|
|
|
34
46
|
type FilterStatus = "all" | "confirmed" | "cancelled";
|
|
35
47
|
|
|
@@ -51,16 +63,27 @@ export default function BookingsList() {
|
|
|
51
63
|
return map;
|
|
52
64
|
}, [bookingLinks]);
|
|
53
65
|
|
|
54
|
-
const filtered =
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
66
|
+
const filtered = sortBookingsNewestFirst(
|
|
67
|
+
bookings.filter(
|
|
68
|
+
(b) =>
|
|
69
|
+
filter === "all" ||
|
|
70
|
+
(filter === "confirmed"
|
|
71
|
+
? b.status === "confirmed" && !b.zoomNeedsReview
|
|
72
|
+
: b.status === filter),
|
|
73
|
+
),
|
|
74
|
+
);
|
|
58
75
|
|
|
59
|
-
function handleCancel(booking: Booking) {
|
|
60
|
-
deleteBooking.mutate(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
76
|
+
function handleCancel(booking: Booking, zoomMeetingResolved = false) {
|
|
77
|
+
deleteBooking.mutate(
|
|
78
|
+
{
|
|
79
|
+
id: booking.id,
|
|
80
|
+
...(zoomMeetingResolved ? { zoomMeetingResolved } : {}),
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
onSuccess: () => toast.success(t("bookingLinks.bookingCancelled")),
|
|
84
|
+
onError: () => toast.error(t("bookingLinks.failedToCancelBooking")),
|
|
85
|
+
},
|
|
86
|
+
);
|
|
64
87
|
}
|
|
65
88
|
|
|
66
89
|
return (
|
|
@@ -72,7 +95,9 @@ export default function BookingsList() {
|
|
|
72
95
|
</TabsTrigger>
|
|
73
96
|
<TabsTrigger value="confirmed">
|
|
74
97
|
{t("bookingLinks.confirmedCount", {
|
|
75
|
-
count: bookings.filter(
|
|
98
|
+
count: bookings.filter(
|
|
99
|
+
(b) => b.status === "confirmed" && !b.zoomNeedsReview,
|
|
100
|
+
).length,
|
|
76
101
|
})}
|
|
77
102
|
</TabsTrigger>
|
|
78
103
|
<TabsTrigger value="cancelled">
|
|
@@ -112,7 +137,9 @@ export default function BookingsList() {
|
|
|
112
137
|
<TableHead>{t("bookingLinks.name")}</TableHead>
|
|
113
138
|
<TableHead>{t("bookingLinks.email")}</TableHead>
|
|
114
139
|
<TableHead>{t("eventForm.event")}</TableHead>
|
|
115
|
-
<TableHead
|
|
140
|
+
<TableHead className="min-w-[180px]">
|
|
141
|
+
{t("bookingLinks.dateAndTime")}
|
|
142
|
+
</TableHead>
|
|
116
143
|
<TableHead>{t("bookingLinks.details")}</TableHead>
|
|
117
144
|
<TableHead>{t("bookingLinks.status")}</TableHead>
|
|
118
145
|
<TableHead className="w-[80px]" />
|
|
@@ -126,7 +153,7 @@ export default function BookingsList() {
|
|
|
126
153
|
{booking.email}
|
|
127
154
|
</TableCell>
|
|
128
155
|
<TableCell>{booking.eventTitle}</TableCell>
|
|
129
|
-
<TableCell className="text-muted-foreground">
|
|
156
|
+
<TableCell className="min-w-[180px] whitespace-nowrap text-muted-foreground">
|
|
130
157
|
<div>{format(parseISO(booking.start), "MMM d, yyyy")}</div>
|
|
131
158
|
<div className="text-xs">
|
|
132
159
|
{format(parseISO(booking.start), "h:mm a")} -{" "}
|
|
@@ -143,14 +170,24 @@ export default function BookingsList() {
|
|
|
143
170
|
<div className="flex items-center gap-1.5">
|
|
144
171
|
<Badge
|
|
145
172
|
variant={
|
|
173
|
+
(booking.zoomNeedsReview ||
|
|
174
|
+
booking.zoomCancellationNeedsReview) &&
|
|
146
175
|
booking.status === "confirmed"
|
|
147
|
-
? "
|
|
148
|
-
: "
|
|
176
|
+
? "destructive"
|
|
177
|
+
: booking.status === "confirmed"
|
|
178
|
+
? "default"
|
|
179
|
+
: "secondary"
|
|
149
180
|
}
|
|
150
181
|
>
|
|
151
|
-
{booking.status === "confirmed"
|
|
152
|
-
|
|
153
|
-
|
|
182
|
+
{booking.status === "confirmed" &&
|
|
183
|
+
booking.zoomCancellationNeedsReview
|
|
184
|
+
? t("bookingLinks.zoomCancellationNeedsReview")
|
|
185
|
+
: booking.status === "confirmed" &&
|
|
186
|
+
booking.zoomNeedsReview
|
|
187
|
+
? t("bookingLinks.zoomNeedsReview")
|
|
188
|
+
: booking.status === "confirmed"
|
|
189
|
+
? t("bookingLinks.confirmed")
|
|
190
|
+
: t("bookingLinks.cancelled")}
|
|
154
191
|
</Badge>
|
|
155
192
|
{booking.meetingLinkPending && (
|
|
156
193
|
<Tooltip>
|
|
@@ -177,12 +214,55 @@ export default function BookingsList() {
|
|
|
177
214
|
</div>
|
|
178
215
|
</TableCell>
|
|
179
216
|
<TableCell>
|
|
180
|
-
{booking.status === "confirmed" &&
|
|
217
|
+
{booking.status === "confirmed" &&
|
|
218
|
+
booking.zoomCancellationNeedsReview ? (
|
|
219
|
+
<AlertDialog>
|
|
220
|
+
<Tooltip>
|
|
221
|
+
<TooltipTrigger asChild>
|
|
222
|
+
<AlertDialogTrigger asChild>
|
|
223
|
+
<Button
|
|
224
|
+
variant="ghost"
|
|
225
|
+
size="icon"
|
|
226
|
+
aria-label={t("bookingLinks.cancelBooking")}
|
|
227
|
+
disabled={deleteBooking.isPending}
|
|
228
|
+
>
|
|
229
|
+
<IconCircleX className="h-4 w-4 text-destructive" />
|
|
230
|
+
</Button>
|
|
231
|
+
</AlertDialogTrigger>
|
|
232
|
+
</TooltipTrigger>
|
|
233
|
+
<TooltipContent>
|
|
234
|
+
{t("bookingLinks.cancelBooking")}
|
|
235
|
+
</TooltipContent>
|
|
236
|
+
</Tooltip>
|
|
237
|
+
<AlertDialogContent>
|
|
238
|
+
<AlertDialogHeader>
|
|
239
|
+
<AlertDialogTitle>
|
|
240
|
+
{t("bookingLinks.zoomCancelTitle")}
|
|
241
|
+
</AlertDialogTitle>
|
|
242
|
+
<AlertDialogDescription>
|
|
243
|
+
{t("bookingLinks.zoomCancelDescription")}
|
|
244
|
+
</AlertDialogDescription>
|
|
245
|
+
</AlertDialogHeader>
|
|
246
|
+
<AlertDialogFooter>
|
|
247
|
+
<AlertDialogCancel>
|
|
248
|
+
{t("eventDialog.cancel")}
|
|
249
|
+
</AlertDialogCancel>
|
|
250
|
+
<AlertDialogAction
|
|
251
|
+
onClick={() => handleCancel(booking, true)}
|
|
252
|
+
disabled={deleteBooking.isPending}
|
|
253
|
+
>
|
|
254
|
+
{t("bookingLinks.zoomCancelConfirm")}
|
|
255
|
+
</AlertDialogAction>
|
|
256
|
+
</AlertDialogFooter>
|
|
257
|
+
</AlertDialogContent>
|
|
258
|
+
</AlertDialog>
|
|
259
|
+
) : booking.status === "confirmed" ? (
|
|
181
260
|
<Tooltip>
|
|
182
261
|
<TooltipTrigger asChild>
|
|
183
262
|
<Button
|
|
184
263
|
variant="ghost"
|
|
185
264
|
size="icon"
|
|
265
|
+
aria-label={t("bookingLinks.cancelBooking")}
|
|
186
266
|
onClick={() => handleCancel(booking)}
|
|
187
267
|
disabled={deleteBooking.isPending}
|
|
188
268
|
>
|
|
@@ -193,7 +273,7 @@ export default function BookingsList() {
|
|
|
193
273
|
{t("bookingLinks.cancelBooking")}
|
|
194
274
|
</TooltipContent>
|
|
195
275
|
</Tooltip>
|
|
196
|
-
)}
|
|
276
|
+
) : null}
|
|
197
277
|
</TableCell>
|
|
198
278
|
</TableRow>
|
|
199
279
|
))}
|
|
@@ -126,7 +126,10 @@ import {
|
|
|
126
126
|
normalizeNumberOfDays,
|
|
127
127
|
} from "@/lib/calendar-view-preferences";
|
|
128
128
|
import { resolveEventAccountEmail } from "@/lib/event-account-selection";
|
|
129
|
-
import {
|
|
129
|
+
import {
|
|
130
|
+
applyOverlayOwnerMarkers,
|
|
131
|
+
getGoogleEventColorHex,
|
|
132
|
+
} from "@/lib/event-colors";
|
|
130
133
|
import {
|
|
131
134
|
buildEventTitleUpdate,
|
|
132
135
|
dateTimeInTimezoneToIso,
|
|
@@ -804,20 +807,11 @@ export default function CalendarView() {
|
|
|
804
807
|
|
|
805
808
|
// Apply overlay ownership markers and filter hidden calendars
|
|
806
809
|
const events = useMemo(() => {
|
|
807
|
-
const ownerMap = new Map(overlayPeople.map((p) => [p.email, p]));
|
|
808
810
|
const sourceEvents = draftEvent
|
|
809
811
|
? [...rawEvents.filter((e) => e.id !== draftEvent.id), draftEvent]
|
|
810
812
|
: rawEvents;
|
|
811
|
-
return sourceEvents
|
|
813
|
+
return applyOverlayOwnerMarkers(sourceEvents, overlayPeople)
|
|
812
814
|
.map((e) => {
|
|
813
|
-
if (e.overlayEmail && ownerMap.has(e.overlayEmail)) {
|
|
814
|
-
const owner = ownerMap.get(e.overlayEmail);
|
|
815
|
-
return {
|
|
816
|
-
...e,
|
|
817
|
-
ownerColor: owner?.color,
|
|
818
|
-
ownerName: owner?.name,
|
|
819
|
-
};
|
|
820
|
-
}
|
|
821
815
|
const tempId = quickEditTempIds[e.id];
|
|
822
816
|
return tempId && !e._tempId ? { ...e, _tempId: tempId } : e;
|
|
823
817
|
})
|
|
@@ -34,6 +34,7 @@ interface BookingInfo {
|
|
|
34
34
|
end: string;
|
|
35
35
|
slug: string;
|
|
36
36
|
meetingLink?: string;
|
|
37
|
+
zoomCancellationNeedsReview?: boolean;
|
|
37
38
|
meetingLinkPending?: boolean;
|
|
38
39
|
status: "confirmed" | "cancelled";
|
|
39
40
|
}
|
|
@@ -164,6 +165,10 @@ export function ManageBookingPage() {
|
|
|
164
165
|
<p className="text-center text-sm text-muted-foreground">
|
|
165
166
|
{t("manageBooking.pastMeeting")}
|
|
166
167
|
</p>
|
|
168
|
+
) : booking.zoomCancellationNeedsReview ? (
|
|
169
|
+
<p className="text-center text-sm text-muted-foreground">
|
|
170
|
+
{t("bookingLinks.zoomCancellationRequiresHostReview")}
|
|
171
|
+
</p>
|
|
167
172
|
) : (
|
|
168
173
|
<div className="flex flex-col gap-3">
|
|
169
174
|
{booking.slug && (
|
|
@@ -30,6 +30,11 @@ export const bookings = table("bookings", {
|
|
|
30
30
|
calendarAccountId: text("calendar_account_id"),
|
|
31
31
|
/** Token for public cancel/reschedule link */
|
|
32
32
|
cancelToken: text("cancel_token"),
|
|
33
|
+
/** Zoom may have created the meeting before the booking request failed. */
|
|
34
|
+
zoomNeedsReview: boolean("zoom_needs_review").notNull().default(false),
|
|
35
|
+
/** Zoom provider identifiers are required to clean up a known meeting. */
|
|
36
|
+
zoomMeetingId: text("zoom_meeting_id"),
|
|
37
|
+
zoomAccountId: text("zoom_account_id"),
|
|
33
38
|
status: text("status", { enum: ["confirmed", "cancelled"] })
|
|
34
39
|
.notNull()
|
|
35
40
|
.default("confirmed"),
|