Feature/add availability tab (#4314)
* Add Availability Tab * Some i18n fixups * Update apps/web/components/v2/eventtype/AvailabilityTab.tsx Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> * Update apps/web/components/v2/eventtype/AvailabilityTab.tsx Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> * Update apps/web/components/v2/eventtype/AvailabilityTab.tsx Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> * Update apps/web/components/v2/eventtype/AvailabilityTab.tsx Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> * Fixed lint * Oops * Use classnames for SkeletonText instead of width,height * Ditches QueryCell Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
parent
0cab371db4
commit
770c35039d
|
@ -0,0 +1,144 @@
|
|||
import { FormValues } from "pages/v2/event-types/[type]";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { weekdayNames } from "@calcom/lib/weekday";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Icon } from "@calcom/ui";
|
||||
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";
|
||||
|
||||
import { AvailabilitySelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||
|
||||
type AvailabilityOption = {
|
||||
label: string;
|
||||
value: number;
|
||||
};
|
||||
|
||||
const AvailabilitySelect = ({
|
||||
className = "",
|
||||
...props
|
||||
}: {
|
||||
className?: string;
|
||||
name: string;
|
||||
value: number;
|
||||
onBlur: () => void;
|
||||
onChange: (value: AvailabilityOption | null) => void;
|
||||
}) => {
|
||||
const { data, isLoading } = trpc.useQuery(["viewer.availability.list"]);
|
||||
if (isLoading) {
|
||||
return <AvailabilitySelectSkeletonLoader />;
|
||||
}
|
||||
|
||||
const schedules = data?.schedules || [];
|
||||
|
||||
const options = schedules.map((schedule) => ({
|
||||
value: schedule.id,
|
||||
label: schedule.name,
|
||||
}));
|
||||
|
||||
const value = options.find((option) =>
|
||||
props.value
|
||||
? option.value === props.value
|
||||
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
|
||||
);
|
||||
|
||||
return (
|
||||
<Select
|
||||
options={options}
|
||||
isSearchable={false}
|
||||
onChange={props.onChange}
|
||||
className={classNames("block w-full min-w-0 flex-1 rounded-sm text-sm", className)}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const format = (date: Date) =>
|
||||
Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "numeric" }).format(
|
||||
new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss"))
|
||||
);
|
||||
|
||||
export const AvailabilityTab = () => {
|
||||
const { t, i18n } = useLocale();
|
||||
const { watch } = useFormContext<FormValues>();
|
||||
|
||||
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>
|
||||
|
||||
<div className="space-y-4 rounded border p-8 py-6 pt-2">
|
||||
<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-32 font-medium", !isAvailable && "text-gray-500 opacity-50")}>
|
||||
{day}
|
||||
</span>
|
||||
{isLoading ? (
|
||||
<SkeletonText className="block h-5 w-60" />
|
||||
) : isAvailable ? (
|
||||
<div className="space-y-3">
|
||||
{filterDays(index).map((dayRange, i) => (
|
||||
<div key={i} className="flex items-center leading-4">
|
||||
<span className="w-28">{format(dayRange.startTime)}</span>
|
||||
<span className="">-</span>
|
||||
<div className="ml-6">{format(dayRange.endTime)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className=" text-gray-500 opacity-50">{t("unavailable")}</span>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
<hr />
|
||||
<div className="flex justify-between">
|
||||
<span className="flex items-center text-sm text-gray-600">
|
||||
<Icon.FiGlobe className="mr-2" />
|
||||
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
|
||||
</span>
|
||||
<Button
|
||||
href={`/availability/${scheduleId}`}
|
||||
color="minimal"
|
||||
EndIcon={Icon.FiExternalLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
{t("edit_availability")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -23,6 +23,7 @@ import { getSession } from "@lib/auth";
|
|||
import { HttpError } from "@lib/core/http/error";
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import { AvailabilityTab } from "@components/v2/eventtype/AvailabilityTab";
|
||||
// These can't really be moved into calcom/ui due to the fact they use infered getserverside props typings
|
||||
import { EventAdvancedTab } from "@components/v2/eventtype/EventAdvancedTab";
|
||||
import { EventAppsTab } from "@components/v2/eventtype/EventAppsTab";
|
||||
|
@ -35,6 +36,8 @@ import EventWorkflowsTab from "@components/v2/eventtype/EventWorkfowsTab";
|
|||
|
||||
import { getTranslation } from "@server/lib/i18n";
|
||||
|
||||
const TABS_WITHOUT_ACTION_BUTTONS = ["workflows", "availability"];
|
||||
|
||||
export type FormValues = {
|
||||
title: string;
|
||||
eventTitle: string;
|
||||
|
@ -136,7 +139,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
locations: eventType.locations || [],
|
||||
recurringEvent: eventType.recurringEvent || null,
|
||||
description: eventType.description ?? undefined,
|
||||
schedule: eventType.schedule?.id,
|
||||
schedule: eventType.schedule || undefined,
|
||||
hidden: eventType.hidden,
|
||||
periodDates: {
|
||||
startDate: periodDates.startDate,
|
||||
|
@ -155,8 +158,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
teamMembers={teamMembers}
|
||||
/>
|
||||
),
|
||||
/* TODO: Actually make this tab */
|
||||
availability: null,
|
||||
availability: <AvailabilityTab />,
|
||||
team: (
|
||||
<EventTeamTab
|
||||
eventType={eventType}
|
||||
|
@ -229,7 +231,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
}}
|
||||
className="space-y-6">
|
||||
{tabMap[tabName]}
|
||||
{tabName !== "workflows" && (
|
||||
{!TABS_WITHOUT_ACTION_BUTTONS.includes(tabName) && (
|
||||
<div className="mt-4 flex justify-end space-x-2 rtl:space-x-reverse">
|
||||
<Button href="/event-types" color="secondary" tabIndex={-1}>
|
||||
{t("cancel")}
|
||||
|
@ -272,6 +274,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
email: true,
|
||||
plan: true,
|
||||
locale: true,
|
||||
defaultScheduleId: true,
|
||||
});
|
||||
|
||||
const rawEventType = await prisma.eventType.findFirst({
|
||||
|
@ -314,7 +317,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
hidden: true,
|
||||
locations: true,
|
||||
eventName: true,
|
||||
availability: true,
|
||||
customInputs: true,
|
||||
timeZone: true,
|
||||
periodType: true,
|
||||
|
@ -408,6 +410,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
const { locations, metadata, ...restEventType } = rawEventType;
|
||||
const eventType = {
|
||||
...restEventType,
|
||||
schedule: rawEventType.schedule?.id || rawEventType.users[0].defaultScheduleId,
|
||||
recurringEvent: parseRecurringEvent(restEventType.recurringEvent),
|
||||
locations: locations as unknown as LocationObject[],
|
||||
metadata: (metadata || {}) as JSONObject,
|
||||
|
@ -435,23 +438,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
(credentials.find((integration) => integration.type === "stripe_payment")?.key as unknown as StripeData)
|
||||
?.default_currency || "usd";
|
||||
|
||||
type Availability = typeof eventType["availability"];
|
||||
const getAvailability = (availability: Availability) =>
|
||||
availability?.length
|
||||
? availability.map((schedule) => ({
|
||||
...schedule,
|
||||
startTime: new Date(new Date().toDateString() + " " + schedule.startTime.toTimeString()).valueOf(),
|
||||
endTime: new Date(new Date().toDateString() + " " + schedule.endTime.toTimeString()).valueOf(),
|
||||
}))
|
||||
: null;
|
||||
|
||||
const availability = getAvailability(eventType.availability) || [];
|
||||
availability.sort((a, b) => a.startTime - b.startTime);
|
||||
|
||||
const eventTypeObject = Object.assign({}, eventType, {
|
||||
periodStartDate: eventType.periodStartDate?.toString() ?? null,
|
||||
periodEndDate: eventType.periodEndDate?.toString() ?? null,
|
||||
availability,
|
||||
});
|
||||
|
||||
const teamMembers = eventTypeObject.team
|
||||
|
@ -472,7 +461,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
session,
|
||||
eventType: eventTypeObject,
|
||||
locationOptions,
|
||||
availability,
|
||||
team: eventTypeObject.team || null,
|
||||
teamMembers,
|
||||
hasPaymentIntegration,
|
||||
|
|
|
@ -628,6 +628,7 @@
|
|||
"billing": "Billing",
|
||||
"manage_your_billing_info": "Manage your billing information and cancel your subscription.",
|
||||
"availability": "Availability",
|
||||
"edit_availability": "Edit availability",
|
||||
"configure_availability": "Configure times when you are available for bookings.",
|
||||
"change_weekly_schedule": "Change your weekly schedule",
|
||||
"logo": "Logo",
|
||||
|
|
Loading…
Reference in New Issue
Block a user