-
Notifications
You must be signed in to change notification settings - Fork 0
feat(eventtypes): implement clean-room Team Event Types UI with Round-Robin & Collective scheduling #65
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(eventtypes): implement clean-room Team Event Types UI with Round-Robin & Collective scheduling #65
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| "use client"; | ||
|
|
||
| import type { EventTypeSetupProps } from "@calcom/features/eventtypes/lib/types"; | ||
| import { EventTeamTab, type GenericTeamMember } from "./EventTeamTab"; | ||
|
|
||
| export interface EventTeamAssignmentTabWebWrapperProps { | ||
| eventType: EventTypeSetupProps["eventType"]; | ||
| team: EventTypeSetupProps["team"]; | ||
| teamMembers: GenericTeamMember[]; | ||
| orgId?: number | null; | ||
| } | ||
|
|
||
| export function EventTeamAssignmentTabWebWrapper({ | ||
| eventType, | ||
| team, | ||
| teamMembers, | ||
| orgId, | ||
| }: EventTeamAssignmentTabWebWrapperProps) { | ||
| return ( | ||
| <EventTeamTab | ||
| eventType={eventType} | ||
| team={team} | ||
| teamMembers={teamMembers} | ||
| orgId={orgId} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| export default EventTeamAssignmentTabWebWrapper; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,287 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "use client"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import AssignAllTeamMembers from "@calcom/features/eventtypes/components/AssignAllTeamMembers"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import CheckedTeamSelect, { type CheckedSelectOption } from "@calcom/features/eventtypes/components/CheckedTeamSelect"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { EventTypeSetupProps, FormValues } from "@calcom/features/eventtypes/lib/types"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useLocale } from "@calcom/lib/hooks/useLocale"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { SchedulingType } from "@calcom/prisma/enums"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import classNames from "@calcom/ui/classNames"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Badge } from "@calcom/ui/components/badge"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Label, SettingsToggle } from "@calcom/ui/components/form"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { RefreshCwIcon, UsersIcon, LayersIcon, ShieldCheckIcon, UserCheckIcon } from "lucide-react"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import React, { useMemo, useState } from "react"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Controller, useFormContext } from "react-hook-form"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export type GenericTeamMember = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id?: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| label?: string | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name?: string | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| email: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| avatar?: string | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| avatarUrl?: string | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defaultScheduleId?: number | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface EventTeamTabProps { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| eventType: EventTypeSetupProps["eventType"]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| team: EventTypeSetupProps["team"]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| teamMembers: GenericTeamMember[]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| orgId?: number | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function EventTeamTab({ eventType, team, teamMembers = [], orgId }: EventTeamTabProps) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { t } = useLocale(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const form = useFormContext<FormValues>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const watchSchedulingType = form.watch("schedulingType") || eventType.schedulingType || SchedulingType.ROUND_ROBIN; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const watchHosts = form.watch("hosts") || []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const watchIsRRWeightsEnabled = form.watch("isRRWeightsEnabled") ?? false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const watchAssignAll = form.watch("assignAllTeamMembers") ?? false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [assignAllTeamMembers, setAssignAllTeamMembers] = useState(watchAssignAll); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+40
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should avoid duplicating form state in local React state (
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Convert TeamMembers to options for CheckedTeamSelect | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const memberOptions: CheckedSelectOption[] = useMemo(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return teamMembers.map((member) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const val = member.value ?? String(member.id ?? ""); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const lbl = member.label ?? member.name ?? member.email; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const avt = member.avatar ?? member.avatarUrl ?? ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value: val, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| label: lbl, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| avatar: avt, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defaultScheduleId: member.defaultScheduleId ?? null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| groupId: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [teamMembers]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Convert current selected hosts in form to CheckedSelectOption[] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const selectedHostOptions: CheckedSelectOption[] = useMemo(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return watchHosts.map((host) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const member = teamMembers.find( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (m) => (m.value !== undefined && Number(m.value) === host.userId) || (m.id !== undefined && m.id === host.userId) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value: String(host.userId), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| label: member?.label ?? member?.name ?? member?.email ?? `User #${host.userId}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| avatar: member?.avatar ?? member?.avatarUrl ?? "", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defaultScheduleId: host.scheduleId ?? member?.defaultScheduleId ?? null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| priority: host.priority ?? 2, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| weight: host.weight ?? 100, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isFixed: host.isFixed ?? false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| groupId: host.groupId ?? null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [watchHosts, teamMembers]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleHostsChange = (newOptions: readonly CheckedSelectOption[]) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const updatedHosts = newOptions.map((opt) => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userId: Number(opt.value), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isFixed: opt.isFixed ?? false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| priority: opt.priority ?? 2, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| weight: opt.weight ?? 100, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scheduleId: opt.defaultScheduleId ?? null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| groupId: opt.groupId ?? null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| form.setValue("hosts", updatedHosts, { shouldDirty: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+78
to
+89
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Defensive programming: |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleSelectSchedulingType = (type: SchedulingType) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| form.setValue("schedulingType", type, { shouldDirty: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const schedulingTypeCards = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: SchedulingType.ROUND_ROBIN, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title: "Round-Robin", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: "Distribute incoming bookings and leads among available team members (evenly or weighted).", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| icon: RefreshCwIcon, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| badge: "Most Popular", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: SchedulingType.COLLECTIVE, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title: "Collective", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: "Allow clients to book a group meeting when ALL selected team members are free at once.", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| icon: UsersIcon, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| badge: "All-Hands", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: SchedulingType.MANAGED, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title: "Managed Event", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: "Organization admin template distributed to all member calendars with locked settings.", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| icon: LayersIcon, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| badge: "Enterprise", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="space-y-6"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Section 1: Scheduling Type Strategy Selection */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="rounded-xl border border-subtle bg-default p-5 shadow-xs"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <h2 className="text-base font-semibold text-default">Team Scheduling Strategy</h2> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p className="mt-1 text-sm text-subtle"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Select how bookings for this team event type will be assigned among team members. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="mt-4 grid grid-cols-1 gap-3.5 sm:grid-cols-3"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {schedulingTypeCards.map((card) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isSelected = watchSchedulingType === card.type; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const IconComponent = card.icon; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| key={card.type} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => handleSelectSchedulingType(card.type)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className={classNames( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "flex flex-col justify-between rounded-xl border p-4 text-left transition", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isSelected | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? "border-primary bg-primary/5 ring-2 ring-primary ring-offset-1" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : "border-subtle bg-default hover:border-emphasis hover:bg-subtle" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex items-center justify-between"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className={classNames( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "flex h-9 w-9 items-center justify-center rounded-lg", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isSelected ? "bg-primary text-white" : "bg-cal-muted text-default" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <IconComponent className="h-5 w-5" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {isSelected ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Badge variant="blue">Active</Badge> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <span className="text-[11px] text-subtle font-medium">{card.badge}</span> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <h3 className="mt-3 font-semibold text-sm text-default">{card.title}</h3> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p className="mt-1 line-clamp-3 text-xs text-subtle">{card.description}</p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Section 2: Round-Robin Advanced Distribution Controls */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {watchSchedulingType === SchedulingType.ROUND_ROBIN && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="rounded-xl border border-subtle bg-default p-5 shadow-xs space-y-4"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex items-center gap-2"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <UserCheckIcon className="h-4 w-4 text-primary" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <h2 className="text-base font-semibold text-default">Round-Robin Assignment Settings</h2> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="divide-y divide-subtle"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Toggle: Weighted Lead Distribution */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="py-3"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Controller | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name="isRRWeightsEnabled" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| control={form.control} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| render={({ field: { value, onChange } }) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <SettingsToggle | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title="Enable Weighted Distribution" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description="Assign custom percentages/weights to hosts (e.g. Senior Rep 70%, Junior Rep 30%)." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| checked={value ?? false} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onCheckedChange={(checked) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange(checked); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| form.setValue("isRRWeightsEnabled", checked, { shouldDirty: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+182
to
+196
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Toggle: Reschedule with Same Host */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="py-3"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Controller | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name="rescheduleWithSameRoundRobinHost" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| control={form.control} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| render={({ field: { value, onChange } }) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <SettingsToggle | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title="Reschedule with Same Host" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description="When an attendee reschedules, automatically reassign them to the same team member." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| checked={value ?? false} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onCheckedChange={(checked) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange(checked); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| form.setValue("rescheduleWithSameRoundRobinHost", checked, { shouldDirty: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+201
to
+215
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Toggle: Enable Per-Host Locations */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="py-3"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Controller | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name="enablePerHostLocations" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| control={form.control} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| render={({ field: { value, onChange } }) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <SettingsToggle | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title="Allow Per-Host Meeting Locations" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description="Each assigned team member can provide their personal Zoom, Google Meet, or Phone location." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| checked={value ?? false} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onCheckedChange={(checked) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange(checked); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| form.setValue("enablePerHostLocations", checked, { shouldDirty: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+220
to
+234
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Section 3: Team Hosts Assignment */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="rounded-xl border border-subtle bg-default p-5 shadow-xs"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="mb-4 flex flex-col justify-between gap-3 sm:flex-row sm:items-center"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <h2 className="text-base font-semibold text-default">Assigned Hosts</h2> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p className="mt-1 text-sm text-subtle"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Choose which team members participate in this event type and customize their priorities. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <AssignAllTeamMembers | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assignAllTeamMembers={assignAllTeamMembers} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setAssignAllTeamMembers={setAssignAllTeamMembers} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onActive={() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const allHosts: CheckedSelectOption[] = memberOptions.map((opt) => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...opt, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| priority: 2, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| weight: 100, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isFixed: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+254
to
+259
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Preserve existing host configuration when assign-all is enabled. Lines 254-259 reset every existing host to Merge existing Proposed fix- const allHosts: CheckedSelectOption[] = memberOptions.map((opt) => ({
- ...opt,
- priority: 2,
- weight: 100,
- isFixed: false,
- }));
+ const existingHosts = new Map(selectedHostOptions.map((host) => [host.value, host]));
+ const allHosts: CheckedSelectOption[] = memberOptions.map((member) => {
+ const existing = existingHosts.get(member.value);
+ return {
+ ...member,
+ priority: existing?.priority ?? 2,
+ weight: existing?.weight ?? 100,
+ isFixed: existing?.isFixed ?? false,
+ defaultScheduleId: existing?.defaultScheduleId ?? member.defaultScheduleId,
+ groupId: existing?.groupId ?? member.groupId,
+ };
+ });📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| handleHostsChange(allHosts); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onInactive={() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| handleHostsChange([]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+250
to
+265
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we removed the local state |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Label className="text-xs font-semibold uppercase tracking-wider text-subtle mb-2"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Select Team Members | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </Label> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <CheckedTeamSelect | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| options={memberOptions} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value={selectedHostOptions} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange={handleHostsChange} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win Clear assign-all when the host list is edited manually. When assign-all is enabled, Set Proposed fix- onChange={handleHostsChange}
+ onChange={(options) => {
+ setAssignAllTeamMembers(false);
+ form.setValue("assignAllTeamMembers", false, { shouldDirty: true });
+ handleHostsChange(options);
+ }}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isRRWeightsEnabled={watchIsRRWeightsEnabled} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| groupId={null} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| placeholder="Search and add team members..." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default EventTeamTab; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Internationalization (i18n): The
tfunction fromuseLocaleis imported and initialized but never used. There are multiple hardcoded English strings throughout the component (e.g., "Team Scheduling Strategy", "Round-Robin", "Collective", "Managed Event", and their descriptions/badges). These should be wrapped int(...)to support localization.