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:
Alex van Andel 2022-09-09 15:47:17 +01:00 committed by GitHub
parent 0cab371db4
commit 770c35039d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 153 additions and 20 deletions

View File

@ -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>
</>
);
};

View File

@ -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,

View File

@ -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",