From 03f583b0219a780b55621243eac25f9611d27850 Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Mon, 14 Jun 2021 18:53:20 +0000 Subject: [PATCH] Save WIP --- calendso.yaml | 9 +- components/Schedule.model.tsx | 6 ++ components/modal/DateOverrideModal.tsx | 7 ++ components/modal/SetTimesModal.tsx | 100 +++++++++++++++++ components/ui/Scheduler.tsx | 101 ++++++++++++++++++ components/ui/WeekdaySelect.tsx | 39 +++++++ lib/schedule.model.tsx | 7 ++ pages/api/availability/schedule.ts | 45 ++++++++ pages/api/availability/schedule/[type].ts | 0 .../availability/schedule/[type]/timezone.ts | 18 ++++ pages/api/availability/week.ts | 30 ++++++ pages/availability/event/[type].tsx | 49 +++++++-- prisma/schema.prisma | 17 ++- styles/components/table.css | 4 + styles/globals.css | 20 ++++ 15 files changed, 441 insertions(+), 11 deletions(-) create mode 100644 components/Schedule.model.tsx create mode 100644 components/modal/DateOverrideModal.tsx create mode 100644 components/modal/SetTimesModal.tsx create mode 100644 components/ui/Scheduler.tsx create mode 100644 components/ui/WeekdaySelect.tsx create mode 100644 lib/schedule.model.tsx create mode 100644 pages/api/availability/schedule.ts create mode 100644 pages/api/availability/schedule/[type].ts create mode 100644 pages/api/availability/schedule/[type]/timezone.ts create mode 100644 pages/api/availability/week.ts diff --git a/calendso.yaml b/calendso.yaml index 8f052f87..bb6d6866 100644 --- a/calendso.yaml +++ b/calendso.yaml @@ -28,6 +28,8 @@ tags: description: Manage integrations - name: User description: Manage the user's profile and settings + - name: Team + description: Group users into teams paths: /api/auth/signin: get: @@ -144,4 +146,9 @@ paths: description: Updates a user's profile. summary: Updates a user's profile tags: - - User \ No newline at end of file + - User + /api/availability/schedule: + path: + description: "Updates a schedule" + tags: + - Availability \ No newline at end of file diff --git a/components/Schedule.model.tsx b/components/Schedule.model.tsx new file mode 100644 index 00000000..d100f13d --- /dev/null +++ b/components/Schedule.model.tsx @@ -0,0 +1,6 @@ +import {Dayjs} from "dayjs"; + +interface Schedule { + startDate: Dayjs; + endDate: Dayjs; +} \ No newline at end of file diff --git a/components/modal/DateOverrideModal.tsx b/components/modal/DateOverrideModal.tsx new file mode 100644 index 00000000..282f6125 --- /dev/null +++ b/components/modal/DateOverrideModal.tsx @@ -0,0 +1,7 @@ + + +/*export default function DateOverrideModal(props) { + return ( + + ); +}*/ \ No newline at end of file diff --git a/components/modal/SetTimesModal.tsx b/components/modal/SetTimesModal.tsx new file mode 100644 index 00000000..55a48b56 --- /dev/null +++ b/components/modal/SetTimesModal.tsx @@ -0,0 +1,100 @@ +import {ClockIcon} from "@heroicons/react/outline"; +import {useRef} from "react"; + +export default function SetTimesModal(props) { + + const isNew = props.isNew || false; + const {startDate, endDate} = props.schedule; + + const startHoursRef = useRef(); + const startMinsRef = useRef(); + const endHoursRef = useRef(); + const endMinsRef = useRef(); + + function updateStartEndTimesHandler(event) { + event.preventDefault(); + + const enteredStartHours = parseInt(startHoursRef.current.value); + const enteredStartMins = parseInt(startMinsRef.current.value); + const enteredEndHours = parseInt(endHoursRef.current.value); + const enteredEndMins = parseInt(endMinsRef.current.value); + + props.onChange({ + startDate: startDate.minute(enteredStartMins).hour(enteredStartHours), + endDate: endDate.minute(enteredEndMins).hour(enteredEndHours), + }); + + props.onExit(0); + } + + return ( +
+
+ + + + +
+
+
+ +
+
+ +
+

+ Set your work schedule +

+
+
+
+
+
+ +
+ + +
+ : +
+ + +
+
+
+ +
+ + +
+ : +
+ + +
+
+
+ + +
+
+
+
+
); +} \ No newline at end of file diff --git a/components/ui/Scheduler.tsx b/components/ui/Scheduler.tsx new file mode 100644 index 00000000..7e5f73a4 --- /dev/null +++ b/components/ui/Scheduler.tsx @@ -0,0 +1,101 @@ +import React, {useEffect, useState} from "react"; +import TimezoneSelect from "react-timezone-select"; +import {PencilAltIcon, TrashIcon} from "@heroicons/react/outline"; +import {WeekdaySelect} from "./WeekdaySelect"; +import SetTimesModal from "../modal/SetTimesModal"; +import Schedule from '../../lib/schedule.model'; +import dayjs, {Dayjs} from "dayjs"; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; +dayjs.extend(utc); +dayjs.extend(timezone); + +export const Scheduler = (props) => { + + const [ showSetTimesModal, setShowSetTimesModal ]: boolean = useState(false); + const [ schedules, setSchedules ]: Schedule[] = useState( + props.schedules.map( (schedule, idx) => ({ + startDate: dayjs(schedule.startDate), + endDate: dayjs(schedule.startDate).startOf('day').add(schedule.length, 'minutes'), + key: idx + }) ) + ); + + const [ timeZone, setTimeZone ] = useState(props.timeZone); + const [ selectedSchedule, setSelectedSchedule ]: Schedule | null = useState(null); + + const addNewSchedule = () => { + setSelectedSchedule({ + startDate: dayjs().startOf('day').add(0, 'minutes'), + endDate: dayjs().startOf('day').add(1439, 'minutes'), + }); + setShowSetTimesModal(true); + } + + const upsertSchedule = (changed: Schedule) => { + if (changed.key) { + schedules.splice( + schedules.findIndex( (schedule) => changed.key === schedule.key ), 1, changed + ) + setSchedules([].concat(schedules)); // update + } + else { + console.log(changed); + setSchedules(schedules.concat([changed])); // insert + } + } + + const removeSchedule = (toRemove: Schedule) => { + schedules.splice(schedules.findIndex( (schedule) => schedule.key === toRemove.key ), 1); + setSchedules([].concat(schedules)); + }; + + return ( +
+
+
+
+ +
+ +
+
+
    + {schedules.length > 0 && schedules.map( (schedule) => +
  • +
    + + +
    + +
  • )} +
+
+ +
+
+ {/*

Add date overrides

+

+ Add dates when your availability changes from your weekly hours +

+ */} +
+
+ {showSetTimesModal && + setShowSetTimesModal(false)} /> + } + {/*{showDateOverrideModal && + + }*/} +
+ ); +} \ No newline at end of file diff --git a/components/ui/WeekdaySelect.tsx b/components/ui/WeekdaySelect.tsx new file mode 100644 index 00000000..49e8d9a8 --- /dev/null +++ b/components/ui/WeekdaySelect.tsx @@ -0,0 +1,39 @@ +import React, {useState} from "react"; + +export const WeekdaySelect = (props) => { + + const [ activeDays, setActiveDays ] = useState([false, true, true, true, true, true, false]); + const days = [ 'S', 'M', 'T', 'W', 'T', 'F', 'S' ]; + + const toggleDay = (e, idx: number) => { + e.preventDefault(); + activeDays[idx] = !activeDays[idx]; + console.log(activeDays); + setActiveDays([].concat(activeDays)); + } + + return ( +
+
+ {days.map( (day, idx) => activeDays[idx] ? + + : + + )} +
+
); +} \ No newline at end of file diff --git a/lib/schedule.model.tsx b/lib/schedule.model.tsx new file mode 100644 index 00000000..03b3e444 --- /dev/null +++ b/lib/schedule.model.tsx @@ -0,0 +1,7 @@ +import {Dayjs} from "dayjs"; + +export default interface Schedule { + key: number; + startDate: Dayjs; + endDate: Dayjs; +} \ No newline at end of file diff --git a/pages/api/availability/schedule.ts b/pages/api/availability/schedule.ts new file mode 100644 index 00000000..e30b9fff --- /dev/null +++ b/pages/api/availability/schedule.ts @@ -0,0 +1,45 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getSession } from 'next-auth/client'; +import prisma from '../../../lib/prisma'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const session = await getSession({req: req}); + + if (!session) { + res.status(401).json({message: "Not authenticated"}); + return; + } + + PUT /api/availability/schedule/{id}/timezone + { + "timeZone": "Europe/London" + } + + PATCH /api/availability/schedule { + "schedules": [ + { + + } + ], + "overrides": { + + } + } + + if (req.method == "PATCH") { + const startMins = req.body.start; + const endMins = req.body.end; + + const updateDay = await prisma.schedule.update({ + where: { + id: session.user.id, + }, + data: { + startTime: startMins, + endTime: endMins + }, + }); + + res.status(200).json({message: 'Start and end times updated successfully'}); + } +} \ No newline at end of file diff --git a/pages/api/availability/schedule/[type].ts b/pages/api/availability/schedule/[type].ts new file mode 100644 index 00000000..e69de29b diff --git a/pages/api/availability/schedule/[type]/timezone.ts b/pages/api/availability/schedule/[type]/timezone.ts new file mode 100644 index 00000000..1274c828 --- /dev/null +++ b/pages/api/availability/schedule/[type]/timezone.ts @@ -0,0 +1,18 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import prisma from '../../../lib/prisma'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const { user } = req.query + + const schedules = await prisma.schedule.find({ + where: { + eventTypeId: req.query.type, + }, + select: { + credentials: true, + timeZone: true + } + }); + + return res.status(202).send(null); +} diff --git a/pages/api/availability/week.ts b/pages/api/availability/week.ts new file mode 100644 index 00000000..d52c55d7 --- /dev/null +++ b/pages/api/availability/week.ts @@ -0,0 +1,30 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getSession } from 'next-auth/client'; +import prisma from '../../../lib/prisma'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const session = await getSession({req: req}); + + if (!session) { + res.status(401).json({message: "Not authenticated"}); + return; + } + + if (req.method == "PATCH") { + + const startMins = req.body.start; + const endMins = req.body.end; + + const updateWeek = await prisma.schedule.update({ + where: { + id: session.user.id, + }, + data: { + startTime: startMins, + endTime: endMins + }, + }); + + res.status(200).json({message: 'Start and end times updated successfully'}); + } +} \ No newline at end of file diff --git a/pages/availability/event/[type].tsx b/pages/availability/event/[type].tsx index ed575f25..f0881adc 100644 --- a/pages/availability/event/[type].tsx +++ b/pages/availability/event/[type].tsx @@ -7,6 +7,8 @@ import prisma from '../../../lib/prisma'; import { LocationType } from '../../../lib/location'; import Shell from '../../../components/Shell'; import { useSession, getSession } from 'next-auth/client'; +import {Scheduler} from "../../../components/ui/Scheduler"; + import { LocationMarkerIcon, PlusCircleIcon, @@ -14,6 +16,12 @@ import { PhoneIcon, } from '@heroicons/react/outline'; +import dayjs, {Dayjs} from "dayjs"; +import utc from 'dayjs/plugin/utc'; +dayjs.extend(utc); +import timezone from 'dayjs/plugin/timezone'; +dayjs.extend(timezone); + export default function EventType(props) { const router = useRouter(); @@ -32,6 +40,8 @@ export default function EventType(props) { return

Loading...

; } + console.log(props); + async function updateEventTypeHandler(event) { event.preventDefault(); @@ -142,7 +152,7 @@ export default function EventType(props) {
-
+
@@ -232,7 +242,7 @@ export default function EventType(props) {
-
+
- - Cancel +
+
+

How do you want to offer your availability for this event type?

+ +
+ Cancel + +
+
@@ -332,7 +349,10 @@ export async function getServerSideProps(context) { email: session.user.email, }, select: { - username: true + username: true, + timeZone: true, + startTime: true, + endTime: true, } }); @@ -351,10 +371,21 @@ export async function getServerSideProps(context) { } }); + const utcOffset = dayjs().tz(user.timeZone).utcOffset(); + + const schedules = [ + { + key: 0, + startDate: dayjs.utc().startOf('day').add(user.startTime - utcOffset, 'minutes').format(), + length: user.endTime, + } + ]; + return { - props: { - user, - eventType - }, + props: { + user, + eventType, + schedules + }, } } \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 88a8187d..da60fe97 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -21,6 +21,7 @@ model EventType { user User? @relation(fields: [userId], references: [id]) userId Int? bookings Booking[] + availability Interval[] } model Credential { @@ -49,6 +50,8 @@ model User { credentials Credential[] teams Membership[] bookings Booking[] + availability Interval[] + @@map(name: "users") } @@ -119,4 +122,16 @@ model Booking { createdAt DateTime @default(now()) updatedAt DateTime? -} \ No newline at end of file +} + +model Interval { + id Int @default(autoincrement()) @id + label String? + user User? @relation(fields: [userId], references: [id]) + userId Int? + eventType EventType? @relation(fields: [eventTypeId], references: [id]) + eventTypeId Int? + startTime DateTime + length Int + isOverride Boolean @default(false) +} diff --git a/styles/components/table.css b/styles/components/table.css index a2d88616..29f8c610 100644 --- a/styles/components/table.css +++ b/styles/components/table.css @@ -1,4 +1,8 @@ table tbody tr:nth-child(odd) { @apply bg-gray-50; +} + +.highlight-odd > *:nth-child(odd) { + @apply bg-gray-50; } \ No newline at end of file diff --git a/styles/globals.css b/styles/globals.css index 926cc766..9dc466a4 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -24,4 +24,24 @@ body { #timeZone input:focus { box-shadow: none; +} + +.weekdaySelect { + font-family: "Courier New", sans-serif; +} + +.weekdaySelect button.active:first-child { + margin-left: -1px !important; +} + +.weekdaySelect button:not(.active) { + padding-left: calc(0.5rem + 0px); + margin-right: 1px; +} + +.weekdaySelect button.active + button.active { + border-color: rgba(3, 169, 244, var(--tw-border-opacity)) + rgba(3, 169, 244, var(--tw-border-opacity)) + rgba(3, 169, 244, var(--tw-border-opacity)) + white; } \ No newline at end of file