Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Someday is a simple, open-source scheduling tool designed specifically for Gmail
- **Multiple Event Types**: Create diverse meeting options like "Quick Chat" or "Deep Dive" with unique durations and availability settings.
- **Team Scheduling**: Add multiple calendars including teammates' calendars (with read access) for collaborative scheduling.
- **Flexible Scheduling Strategies**: Choose between "Collective" (all team members must be free) or "Round-Robin" (distribute bookings among available team members) scheduling modes.
- **Guest Permission Controls**: Fine-grained control over what guests can do with booked events (modify, invite others, see attendees) and calendar visibility (public, private, or default).
- **Dynamic Configuration**: Adjust your timezone, working hours, available days, and monitored calendars globally or per event type directly through the integrated Settings screen.
- **Owner-Only Access**: Secure access to configuration via the script owner's Google account session.
- **Simple Booking Process**: Users can select a date and time slot, then fill out a straightforward form with their name, email, phone, and an optional note.
Expand Down Expand Up @@ -55,6 +56,14 @@ Someday includes a built-in **Settings** screen for easy configuration.
**Event Types**:
- **Custom Meeting Types**: Create unlimited event types (e.g. "15 Min Discovery", "1 Hour Review").
- **Flexible Durations**: Set specific lengths for each meeting type (up to 24 hours).
- **Guest Permissions**: Control what guests can do with booked events:
- **Modify Event**: Allow/prevent guests from changing event details, time, or location
- **Invite Others**: Allow/prevent guests from adding additional attendees
- **See Other Guests**: Allow/prevent guests from viewing the attendee list
- **Calendar Visibility**: Choose how events appear in Google Calendar:
- **Default**: Uses your calendar's default visibility setting
- **Public**: Event details are publicly visible to everyone
- **Private**: Shows only as "Busy" without revealing event details
- **Smart Overrides**: Override global Work Hours, Available Days, Monitored Calendars, and Scheduling Strategy for specific event types.
- **Per-Event Strategies**: Set different scheduling strategies for different event types (e.g., Round-Robin for sales calls, Collective for team meetings).
- **Direct Links**: Copy a unique booking URL for any event type to share directly.
Expand Down
1 change: 0 additions & 1 deletion backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ interface EventType {
DAYS_IN_ADVANCE?: number;
CALENDARS?: string[];
schedulingStrategy?: 'collective' | 'round_robin';
// Guest permissions
guestsCanModify?: boolean;
guestsCanInviteOthers?: boolean;
guestsCanSeeOtherGuests?: boolean;
// Meeting visibility
visibility?: 'default' | 'public' | 'private';
}

const CONFIG = {
Expand Down Expand Up @@ -252,6 +258,19 @@ function bookTimeslot(
status: "confirmed",
}
);

// Apply guest permissions (defaults: modify=false, invite=false, see=true)
event.setGuestsCanModify(eventType.guestsCanModify ?? false);
event.setGuestsCanInviteOthers(eventType.guestsCanInviteOthers ?? false);
event.setGuestsCanSeeGuests(eventType.guestsCanSeeOtherGuests ?? true);

// Apply visibility setting
if (eventType.visibility === 'public') {
event.setVisibility(CalendarApp.Visibility.PUBLIC);
} else if (eventType.visibility === 'private') {
event.setVisibility(CalendarApp.Visibility.PRIVATE);
}
// 'default' visibility doesn't require explicit setting
return `Timeslot booked successfully`;
} catch (e) {
const error = e as Error;
Expand Down
100 changes: 98 additions & 2 deletions frontend/src/components/ConfigScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,7 @@ export function ConfigScreen({ onBack }: { onBack: () => void }) {
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-sm text-muted-foreground pointer-events-none">minutes</span>
</div>
</div>
<div className="flex flex-col gap-4">
<div className="space-y-2">
<div className="flex items-start gap-4">
<Switch
id={`selectable-${index}`}
Expand All @@ -593,7 +593,103 @@ export function ConfigScreen({ onBack }: { onBack: () => void }) {
<span className="block text-[10px] text-muted-foreground">If disabled, this type can only be booked via direct links</span>
</div>
</div>

</div>
<div className="space-y-2">
<div className="space-y-1">
<Label className="text-sm font-medium">Guest Permissions</Label>
<p className="text-xs text-muted-foreground">
Control what guests can do with this event.
</p>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="w-full justify-between font-normal">
<span className="truncate">
{(() => {
const permissions = [];
if (et.guestsCanModify) permissions.push('Modify');
if (et.guestsCanInviteOthers) permissions.push('Invite');
if (et.guestsCanSeeOtherGuests ?? true) permissions.push('See guests');
return permissions.length > 0 ? permissions.join(', ') : 'See guests only';
})()}
</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-[var(--radix-dropdown-menu-trigger-width)]">
<DropdownMenuCheckboxItem
checked={et.guestsCanModify ?? false}
onCheckedChange={(checked) => updateEventType(index, { guestsCanModify: checked })}
onSelect={(e) => e.preventDefault()}
>
<div className="flex flex-col gap-1">
<span>Allow guests to modify event</span>
<span className="text-xs text-muted-foreground">Change time, location, details</span>
</div>
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={et.guestsCanInviteOthers ?? false}
onCheckedChange={(checked) => updateEventType(index, { guestsCanInviteOthers: checked })}
onSelect={(e) => e.preventDefault()}
>
<div className="flex flex-col gap-1">
<span>Allow guests to invite others</span>
<span className="text-xs text-muted-foreground">Add additional attendees</span>
</div>
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={et.guestsCanSeeOtherGuests ?? true}
onCheckedChange={(checked) => updateEventType(index, { guestsCanSeeOtherGuests: checked })}
onSelect={(e) => e.preventDefault()}
>
<div className="flex flex-col gap-1">
<span>Allow guests to see other guests</span>
<span className="text-xs text-muted-foreground">View attendee list</span>
</div>
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="space-y-2">
<div className="space-y-1">
<Label className="text-sm font-medium">Calendar Visibility</Label>
<p className="text-xs text-muted-foreground">
How the event appears in Google Calendar.
</p>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="w-full justify-between font-normal">
<span className="truncate">
{et.visibility === 'public' ? 'Public' : et.visibility === 'private' ? 'Private' : 'Default'}
</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-[var(--radix-dropdown-menu-trigger-width)]">
<DropdownMenuItem onClick={() => updateEventType(index, { visibility: 'default' })}>
<div className="flex flex-col gap-1">
<span>Default</span>
<span className="text-xs text-muted-foreground">Uses calendar's default visibility setting</span>
</div>
{(!et.visibility || et.visibility === 'default') && <Check className="ml-auto h-4 w-4" />}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => updateEventType(index, { visibility: 'public' })}>
<div className="flex flex-col gap-1">
<span>Public</span>
<span className="text-xs text-muted-foreground">Event details visible to everyone</span>
</div>
{et.visibility === 'public' && <Check className="ml-auto h-4 w-4" />}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => updateEventType(index, { visibility: 'private' })}>
<div className="flex flex-col gap-1">
<span>Private</span>
<span className="text-xs text-muted-foreground">Shows as "Busy" without event details</span>
</div>
{et.visibility === 'private' && <Check className="ml-auto h-4 w-4" />}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>


Expand Down
6 changes: 6 additions & 0 deletions frontend/src/models/EventType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ export interface EventType {
DAYS_IN_ADVANCE?: number;
CALENDARS?: string[];
schedulingStrategy?: 'collective' | 'round_robin';
// Guest permissions
guestsCanModify?: boolean;
guestsCanInviteOthers?: boolean;
guestsCanSeeOtherGuests?: boolean;
// Meeting visibility
visibility?: 'default' | 'public' | 'private';
}

export interface Config {
Expand Down