Add "Choose common schedule toggle" to team events (#5343)
* Add Choose common schedule togglee * Fix types * Add translations * Improve variable name * Move setting to config so that all such lightweight boolean settings can exist here * Update apps/web/public/static/locales/en/common.json Co-authored-by: Alex van Andel <me@alexvanandel.com> Co-authored-by: Alex van Andel <me@alexvanandel.com>pull/5261/head^2
parent
328f00a9b5
commit
d751cca0f4
|
@ -9,7 +9,7 @@ import { weekdayNames } from "@calcom/lib/weekday";
|
|||
import { trpc } from "@calcom/trpc/react";
|
||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||
import { Icon } from "@calcom/ui";
|
||||
import { Badge } from "@calcom/ui/v2";
|
||||
import { Badge, SettingsToggle } from "@calcom/ui/v2";
|
||||
import Button from "@calcom/ui/v2/core/Button";
|
||||
import Select from "@calcom/ui/v2/core/form/select";
|
||||
import { SkeletonText } from "@calcom/ui/v2/core/skeleton";
|
||||
|
@ -97,91 +97,114 @@ const format = (date: Date, hour12: boolean) =>
|
|||
new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss"))
|
||||
);
|
||||
|
||||
export const AvailabilityTab = () => {
|
||||
export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
||||
const { t, i18n } = useLocale();
|
||||
const { watch } = useFormContext<FormValues>();
|
||||
const me = useMeQuery();
|
||||
const timeFormat = me?.data?.timeFormat;
|
||||
|
||||
const EventTypeSchedule = () => {
|
||||
const me = useMeQuery();
|
||||
const timeFormat = me?.data?.timeFormat;
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="min-w-4 mb-2">
|
||||
<label htmlFor="availability" className="mt-0 flex text-sm font-medium text-neutral-700">
|
||||
{t("availability")}
|
||||
</label>
|
||||
</div>
|
||||
<Controller
|
||||
name="schedule"
|
||||
render={({ field }) => (
|
||||
<AvailabilitySelect
|
||||
value={field.value}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
onChange={(selected) => {
|
||||
field.onChange(selected?.value || null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-4 rounded border p-4 py-6 pt-2 md:p-8">
|
||||
<ol className="table border-collapse text-sm">
|
||||
{weekdayNames(i18n.language, 1, "long").map((day, index) => {
|
||||
const isAvailable = !!filterDays(index).length;
|
||||
return (
|
||||
<li key={day} className="my-6 flex border-transparent last:mb-2">
|
||||
<span
|
||||
className={classNames(
|
||||
"w-20 font-medium sm:w-32",
|
||||
!isAvailable && "text-gray-500 opacity-50"
|
||||
)}>
|
||||
{day}
|
||||
</span>
|
||||
{isLoading ? (
|
||||
<SkeletonText className="block h-5 w-60" />
|
||||
) : isAvailable ? (
|
||||
<div className="space-y-3 text-right">
|
||||
{filterDays(index).map((dayRange, i) => (
|
||||
<div key={i} className="flex items-center leading-4">
|
||||
<span className="w-16 sm:w-28 sm:text-left">
|
||||
{format(dayRange.startTime, timeFormat === 12)}
|
||||
</span>
|
||||
<span className="ml-4">-</span>
|
||||
<div className="ml-6">{format(dayRange.endTime, timeFormat === 12)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className="ml-6 text-gray-500 opacity-50 sm:ml-0">{t("unavailable")}</span>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
<hr />
|
||||
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
|
||||
<span className="flex items-center justify-center text-sm text-gray-600 sm:justify-start">
|
||||
<Icon.FiGlobe className="mr-2" />
|
||||
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
|
||||
</span>
|
||||
<Button
|
||||
href={`/availability/${schedule?.schedule.id}`}
|
||||
color="minimal"
|
||||
EndIcon={Icon.FiExternalLink}
|
||||
target="_blank"
|
||||
className="justify-center border sm:border-0"
|
||||
rel="noopener noreferrer">
|
||||
{t("edit_availability")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const scheduleId = watch("schedule");
|
||||
const { isLoading, data: schedule } = trpc.useQuery(["viewer.availability.schedule", { scheduleId }]);
|
||||
|
||||
const filterDays = (dayNum: number) =>
|
||||
schedule?.schedule.availability.filter((item) => item.days.includes((dayNum + 1) % 7)) || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="min-w-4 mb-2">
|
||||
<label htmlFor="availability" className="mt-0 flex text-sm font-medium text-neutral-700">
|
||||
{t("availability")}
|
||||
</label>
|
||||
</div>
|
||||
<Controller
|
||||
name="schedule"
|
||||
render={({ field }) => (
|
||||
<AvailabilitySelect
|
||||
value={field.value}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
onChange={(selected) => {
|
||||
field.onChange(selected?.value || null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
if (!isTeamEvent) {
|
||||
return <EventTypeSchedule />;
|
||||
}
|
||||
|
||||
<div className="space-y-4 rounded border p-4 py-6 pt-2 md:p-8">
|
||||
<ol className="table border-collapse text-sm">
|
||||
{weekdayNames(i18n.language, 1, "long").map((day, index) => {
|
||||
const isAvailable = !!filterDays(index).length;
|
||||
return (
|
||||
<li key={day} className="my-6 flex border-transparent last:mb-2">
|
||||
<span
|
||||
className={classNames(
|
||||
"w-20 font-medium sm:w-32",
|
||||
!isAvailable && "text-gray-500 opacity-50"
|
||||
)}>
|
||||
{day}
|
||||
</span>
|
||||
{isLoading ? (
|
||||
<SkeletonText className="block h-5 w-60" />
|
||||
) : isAvailable ? (
|
||||
<div className="space-y-3 text-right">
|
||||
{filterDays(index).map((dayRange, i) => (
|
||||
<div key={i} className="flex items-center leading-4">
|
||||
<span className="w-16 sm:w-28 sm:text-left">
|
||||
{format(dayRange.startTime, timeFormat === 12)}
|
||||
</span>
|
||||
<span className="ml-4 sm:ml-0">-</span>
|
||||
<div className="ml-6">{format(dayRange.endTime, timeFormat === 12)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className="ml-6 text-gray-500 opacity-50 sm:ml-0">{t("unavailable")}</span>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
<hr />
|
||||
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
|
||||
<span className="flex items-center justify-center text-sm text-gray-600 sm:justify-start">
|
||||
<Icon.FiGlobe className="mr-2" />
|
||||
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
|
||||
</span>
|
||||
<Button
|
||||
href={`/availability/${schedule?.schedule.id}`}
|
||||
color="minimal"
|
||||
EndIcon={Icon.FiExternalLink}
|
||||
target="_blank"
|
||||
className="justify-center border sm:border-0"
|
||||
rel="noopener noreferrer">
|
||||
{t("edit_availability")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
return (
|
||||
<Controller
|
||||
name="metadata.config.useHostSchedulesForTeamEvent"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<SettingsToggle
|
||||
checked={!value}
|
||||
onCheckedChange={(checked) => {
|
||||
onChange(!checked);
|
||||
}}
|
||||
title={t("choose_common_schedule_team_event")}
|
||||
description={t("choose_common_schedule_team_event_description")}>
|
||||
<EventTypeSchedule />
|
||||
</SettingsToggle>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -192,7 +192,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
teamMembers={teamMembers}
|
||||
/>
|
||||
),
|
||||
availability: <AvailabilityTab />,
|
||||
availability: <AvailabilityTab isTeamEvent={!!team} />,
|
||||
team: (
|
||||
<EventTeamTab
|
||||
eventType={eventType}
|
||||
|
|
|
@ -1339,5 +1339,7 @@
|
|||
"number_sms_notifications": "Phone number (SMS\u00a0notifications)",
|
||||
"attendee_email_workflow": "Attendee email",
|
||||
"attendee_email_info": "The person booking's email",
|
||||
"invalid_credential": "Oh no! Looks like permission expired or was revoked. Please reinstall again."
|
||||
"invalid_credential": "Oh no! Looks like permission expired or was revoked. Please reinstall again.",
|
||||
"choose_common_schedule_team_event": "Choose a common schedule",
|
||||
"choose_common_schedule_team_event_description": "Enable this if you want to use a common schedule between hosts. When disabled, each host will be booked based on their default schedule."
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import logger from "@calcom/lib/logger";
|
|||
import { checkLimit } from "@calcom/lib/server";
|
||||
import { performance } from "@calcom/lib/server/perfObserver";
|
||||
import prisma, { availabilityUserSelect } from "@calcom/prisma";
|
||||
import { stringToDayjs } from "@calcom/prisma/zod-utils";
|
||||
import { EventTypeMetaDataSchema, stringToDayjs } from "@calcom/prisma/zod-utils";
|
||||
import { BookingLimit, EventBusyDetails } from "@calcom/types/Calendar";
|
||||
|
||||
import { getBusyTimes } from "./getBusyTimes";
|
||||
|
@ -27,14 +27,15 @@ const availabilitySchema = z
|
|||
})
|
||||
.refine((data) => !!data.username || !!data.userId, "Either username or userId should be filled in.");
|
||||
|
||||
const getEventType = (id: number) =>
|
||||
prisma.eventType.findUnique({
|
||||
const getEventType = async (id: number) => {
|
||||
const eventType = await prisma.eventType.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
id: true,
|
||||
seatsPerTimeSlot: true,
|
||||
bookingLimits: true,
|
||||
timeZone: true,
|
||||
metadata: true,
|
||||
schedule: {
|
||||
select: {
|
||||
availability: true,
|
||||
|
@ -50,6 +51,14 @@ const getEventType = (id: number) =>
|
|||
},
|
||||
},
|
||||
});
|
||||
if (!eventType) {
|
||||
return eventType;
|
||||
}
|
||||
return {
|
||||
...eventType,
|
||||
metadata: EventTypeMetaDataSchema.parse(eventType.metadata),
|
||||
};
|
||||
};
|
||||
|
||||
type EventType = Awaited<ReturnType<typeof getEventType>>;
|
||||
|
||||
|
@ -200,13 +209,15 @@ export async function getUserAvailability(
|
|||
});
|
||||
}
|
||||
}
|
||||
const schedule = eventType?.schedule
|
||||
? { ...eventType?.schedule }
|
||||
: {
|
||||
...currentUser.schedules.filter(
|
||||
(schedule) => !currentUser.defaultScheduleId || schedule.id === currentUser.defaultScheduleId
|
||||
)[0],
|
||||
};
|
||||
|
||||
const schedule =
|
||||
!eventType?.metadata?.config?.useHostSchedulesForTeamEvent && eventType?.schedule
|
||||
? { ...eventType?.schedule }
|
||||
: {
|
||||
...currentUser.schedules.filter(
|
||||
(schedule) => !currentUser.defaultScheduleId || schedule.id === currentUser.defaultScheduleId
|
||||
)[0],
|
||||
};
|
||||
|
||||
const startGetWorkingHours = performance.now();
|
||||
|
||||
|
|
|
@ -32,6 +32,11 @@ export const EventTypeMetaDataSchema = z
|
|||
giphyThankYouPage: z.string().optional(),
|
||||
apps: z.object(appDataSchemas).partial().optional(),
|
||||
additionalNotesRequired: z.boolean().optional(),
|
||||
config: z
|
||||
.object({
|
||||
useHostSchedulesForTeamEvent: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.nullable();
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import logger from "@calcom/lib/logger";
|
|||
import { performance } from "@calcom/lib/server/perfObserver";
|
||||
import getTimeSlots from "@calcom/lib/slots";
|
||||
import prisma, { availabilityUserSelect } from "@calcom/prisma";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { EventBusyDate } from "@calcom/types/Calendar";
|
||||
import { TimeRange } from "@calcom/types/schedule";
|
||||
|
||||
|
@ -104,7 +105,7 @@ export const slotsRouter = createRouter().query("getSchedule", {
|
|||
});
|
||||
|
||||
async function getEventType(ctx: { prisma: typeof prisma }, input: z.infer<typeof getScheduleSchema>) {
|
||||
return ctx.prisma.eventType.findUnique({
|
||||
const eventType = await ctx.prisma.eventType.findUnique({
|
||||
where: {
|
||||
id: input.eventTypeId,
|
||||
},
|
||||
|
@ -124,6 +125,7 @@ async function getEventType(ctx: { prisma: typeof prisma }, input: z.infer<typeo
|
|||
periodEndDate: true,
|
||||
periodCountCalendarDays: true,
|
||||
periodDays: true,
|
||||
metadata: true,
|
||||
schedule: {
|
||||
select: {
|
||||
availability: true,
|
||||
|
@ -144,6 +146,14 @@ async function getEventType(ctx: { prisma: typeof prisma }, input: z.infer<typeo
|
|||
},
|
||||
},
|
||||
});
|
||||
if (!eventType) {
|
||||
return eventType;
|
||||
}
|
||||
|
||||
return {
|
||||
...eventType,
|
||||
metadata: EventTypeMetaDataSchema.parse(eventType.metadata),
|
||||
};
|
||||
}
|
||||
|
||||
async function getDynamicEventType(ctx: { prisma: typeof prisma }, input: z.infer<typeof getScheduleSchema>) {
|
||||
|
|
|
@ -44,8 +44,8 @@ function SettingsToggle({
|
|||
</div>
|
||||
</div>
|
||||
{children && (
|
||||
<div className="mt-4 lg:ml-14" ref={animateRef}>
|
||||
{checked && children}
|
||||
<div className="lg:ml-14" ref={animateRef}>
|
||||
{checked && <div className="mt-4">{children}</div>}
|
||||
</div>
|
||||
)}
|
||||
</fieldset>
|
||||
|
|
Loading…
Reference in New Issue