switch to prisma

This commit is contained in:
Thomas Ruoff
2022-10-11 11:43:32 +02:00
parent 41342475ba
commit 1ef9b14e95
28 changed files with 764 additions and 780 deletions

View File

@@ -1,17 +1,18 @@
import React from 'react'
import Link from 'next/link'
import { daysFormatFrontend } from '../helpers/date'
import { BookingDocument } from '../db/booking'
import {Booking} from '@prisma/client';
import { dateFormatFrontend } from '../helpers/date'
export default function BookingTable({
booking,
}: {
booking: BookingDocument
booking: Booking
}) {
const data = [
{ name: 'Status', value: booking.status },
{ name: 'Buchungszeitraum', value: daysFormatFrontend(booking.days) },
{ name: 'Buchungszeitraum', value: `${dateFormatFrontend(new Date(booking.startDate))}-${dateFormatFrontend(new Date(booking.endDate))}` },
{ name: 'Organisation', value: booking.org },
{
name: 'Addresse',
@@ -48,7 +49,7 @@ export default function BookingTable({
</h3>
<p className="mt-1 max-w-2xl text-sm text-gray-500">
Last updated{' '}
{new Date(booking.updatedAt as string).toLocaleString(
{booking.updatedAt.toLocaleString(
new Intl.Locale('de')
)}
</p>

View File

@@ -1,20 +1,18 @@
import React, { useEffect, useReducer } from 'react'
import { Prisma } from '@prisma/client'
import { useRouter } from 'next/router'
import { clearBookingData, loadBookingData } from '../helpers/storage'
import { log } from '../helpers/log'
import { createBooking } from '../helpers/booking'
import { Booking } from '../db/booking'
export type BookFormData = Omit<Booking, 'uuid' | 'calendarEventId'>
type BookingProviderState = {
postData?: boolean
postDataError?: string
postDataSuccess?: boolean
formData: BookFormData
formData: Prisma.BookingCreateInput,
formDataChanged: string[]
booking?: Booking
booking?: Prisma.BookingCreateInput,
dataStored: boolean
dataStoredLoaded: boolean
}

View File

@@ -1,91 +0,0 @@
import * as mongoose from 'mongoose'
import { BILL_STATUS, MILAGE_TARIFS } from './enums'
import { getBillTotal } from '../helpers/bill'
export type AdditionalCost = {
name: string
value: number
}
export type Bill = {
milageStart: number
milageEnd: number
milage?: number
tarif: MILAGE_TARIFS
status: BILL_STATUS
additionalCosts: AdditionalCost[]
}
export type BillDocument = Bill &
mongoose.SchemaTimestampsConfig &
mongoose.Document
export type BillModel = mongoose.Model<BillDocument>
const BillSchema = new mongoose.Schema<BillDocument>(
{
milageStart: {
type: Number,
required: true,
validate: {
validator: function (v: number): boolean {
const bill = this as BillDocument
return v <= bill.milageEnd
},
message: (props: { value: Number }) =>
`${props.value} is bigger than milageEnd!`,
},
},
milageEnd: {
type: Number,
required: true,
validate: {
validator: function (v: number): boolean {
const bill = this as BillDocument
return v >= bill.milageStart
},
message: (props: { value: Number }) =>
`${props.value} is smaller than milageStart!`,
},
},
tarif: {
type: String,
enum: Object.values(MILAGE_TARIFS),
default: MILAGE_TARIFS.EXTERN,
required: true,
},
additionalCosts: [
{
name: { type: String, required: true },
value: { type: Number, required: true },
},
],
status: {
type: String,
enum: Object.values(BILL_STATUS),
default: BILL_STATUS.UNINVOICED,
},
},
{
timestamps: true,
toJSON: { virtuals: true, getters: true },
toObject: { virtuals: true, getters: true },
}
)
BillSchema.virtual('milage').get(function (): number {
const bill = this as BillDocument
return bill.milageEnd - bill.milageStart
})
BillSchema.virtual('total').get(function (): number {
const bill = this as BillDocument
return getBillTotal(bill)
})
export default (mongoose.models.Bill ||
mongoose.model<BillDocument, BillModel>('Bill', BillSchema)) as BillModel

View File

@@ -1,168 +0,0 @@
import * as mongoose from 'mongoose'
import { v4 as uuidv4 } from 'uuid'
import {
dateFormatBackend,
getDays,
nowInTz,
dateParseBackend,
} from '../helpers/date'
import { createCalendarEvent, deleteCalendarEvent } from '../lib/googlecalendar'
import { Bill } from './bill'
import { BOOKING_STATUS, VALIDATION_ERRORS } from './enums'
export type Booking = {
uuid: string
name: string
email: string
phone: string
street: string
zip: string
city: string
bill?: Bill
// format YYYY-MM-DD
startDate: string
// format YYYY-MM-DD
endDate: string
status?: BOOKING_STATUS
purpose?: string
org?: string
destination?: string
days?: string[]
calendarEventId: string
}
export type BookingDocument = Booking &
mongoose.Document &
mongoose.SchemaTimestampsConfig
export type BookingModel = mongoose.Model<BookingDocument> & {
findBookedDays(uuidsToIngore?: string[]): Promise<string[]>
}
const BookingSchema = new mongoose.Schema<BookingDocument>(
{
// need a seperate uuid to be able to target a booking anonimously
uuid: {
type: String,
default: uuidv4,
index: true,
},
name: { type: String, required: true },
email: { type: String, required: true, minlength: 5 },
phone: { type: String, required: false },
street: { type: String, required: true },
zip: { type: String, required: true },
city: { type: String, required: true },
bill: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Bill',
required: false,
},
startDate: {
type: String,
required: true,
validator: function (value: string): boolean {
return !!dateParseBackend(value)
},
},
endDate: {
type: String,
required: true,
validator: function (value: string): boolean {
return !!dateParseBackend(value)
},
},
days: {
type: [String],
required: true,
validate: {
validator: async function (days: string[]): Promise<boolean> {
const booking = this as Booking
const uuid = booking.uuid && [booking.uuid]
const bookedDays = await BookingModel.findBookedDays(uuid)
const doubleBookedDays = days.filter((day: string): boolean =>
bookedDays.includes(day)
)
return doubleBookedDays.length === 0
},
message: (props: { value: string[] }): string =>
`At least one day is of ${props.value.join(',')} is already booked`,
type: VALIDATION_ERRORS.AT_LEAST_ONE_DAY_BOOKED,
},
},
status: {
type: String,
enum: Object.values(BOOKING_STATUS),
required: true,
default: BOOKING_STATUS.REQUESTED,
},
purpose: { type: String, required: false },
org: { type: String, required: false },
destination: { type: String, required: false },
calendarEventId: { type: String, required: false },
},
{
timestamps: true,
toJSON: { virtuals: true, getters: true },
toObject: { virtuals: true, getters: true },
}
)
BookingSchema.pre('validate', function (next: () => void): void {
const booking = this as BookingDocument
booking.days = getDays({
startDate: new Date(booking.startDate),
endDate: new Date(booking.endDate),
})
next()
})
BookingSchema.pre('save', async function (next: () => void): Promise<void> {
const booking = this as BookingDocument
if (!booking.calendarEventId) {
// create calendar event before saving to database
await createCalendarEvent(booking)
} else if (
[BOOKING_STATUS.CANCELED, BOOKING_STATUS.REJECTED].includes(booking.status)
) {
// event has been canceled or rejected, delete calendar event again to free up the slot
await deleteCalendarEvent(booking)
}
next()
})
BookingSchema.static(
'findBookedDays',
async function (uuidsToIngore: string[] = []): Promise<string[]> {
const model = this as BookingModel
const now = nowInTz()
const bookedDays = await model
.find(
{
status: { $in: [BOOKING_STATUS.REQUESTED, BOOKING_STATUS.CONFIRMED] },
uuid: { $nin: uuidsToIngore },
// dateFormatBackend uses YYYY-MM-DD, which is startOfDay anyway
endDate: { $gt: dateFormatBackend(now) },
},
'days'
)
.exec()
return bookedDays
.map((b) => b.days)
.flat()
.sort()
}
)
const BookingModel = (mongoose.models.Booking ||
mongoose.model<BookingDocument, BookingModel>(
'Booking',
BookingSchema
)) as BookingModel
export default BookingModel

View File

@@ -1,16 +1,3 @@
export enum BOOKING_STATUS {
REQUESTED = 'requested',
CONFIRMED = 'confirmed',
REJECTED = 'rejected',
CANCELED = 'canceled',
}
export enum BILL_STATUS {
UNINVOICED = 'uninvoiced',
INVOICED = 'invoiced',
PAID = 'paid',
}
export enum MILAGE_TARIFS {
INTERN = 'intern',
EXTERN = 'extern',

View File

@@ -1,129 +1,121 @@
import * as mongoose from 'mongoose'
import BookingModel, { Booking, BookingDocument } from './booking'
import BillModel, { Bill } from './bill'
import { BookingStatus, Booking, PrismaClient, Prisma } from '@prisma/client'
const prisma = new PrismaClient()
import { getBookedDays as calendarGetBookedDays } from '../lib/googlecalendar'
import { BOOKING_STATUS } from './enums'
import { uniqueFilter } from '../helpers/array'
export const MONGO_URI = process.env.MONGO_URI
mongoose.connect(process.env.MONGO_URI, {
serverSelectionTimeoutMS: 3000,
})
import { dateFormatBackend, getDays, nowInTz } from '../helpers/date'
export async function getBookedDays(
uuidsToIngore?: string[]
): Promise<string[]> {
const [bookedInDatabase, bookedInCalendar] = await Promise.all([
BookingModel.findBookedDays(uuidsToIngore),
) {
const [bookingsInDbRaw, bookingsInCalendar] = await Promise.all([
prisma.booking.findMany({
where: {
uuid: { notIn: uuidsToIngore },
startDate: { gte: dateFormatBackend(nowInTz()) },
status: { notIn: [BookingStatus.REJECTED, BookingStatus.CANCELED] }
},
select: {
startDate: true,
endDate: true,
},
}),
calendarGetBookedDays(),
])
return [...bookedInDatabase, ...bookedInCalendar].filter(uniqueFilter).sort()
const bookingsInDb = bookingsInDbRaw.map(booking => getDays(booking)).flat();
return [...bookingsInDb, ...bookingsInCalendar].filter(uniqueFilter).sort()
}
export async function getBookingByUUID(uuid: string): Promise<BookingDocument> {
return BookingModel.findOne({ uuid })
export function getBookingByUUID(uuid: string) {
// TODO: can we ignore canceled and rejected ones ?
return prisma.booking.findUniqueOrThrow({
where: {
uuid,
},
include: {
bill: true
}
})
}
export async function getBookings({
status = [BOOKING_STATUS.CONFIRMED, BOOKING_STATUS.REQUESTED],
export function getBookings({
status = [
BookingStatus.REQUESTED,
BookingStatus.CONFIRMED,
],
startDateGreaterThan = '2000-01-01T00:00:00Z',
}: { status?: BOOKING_STATUS[]; startDateGreaterThan?: string } = {}): Promise<
BookingDocument[]
> {
return await BookingModel.find({
status: { $in: status },
startDate: { $gte: startDateGreaterThan },
})
.sort({ startDate: -1 })
.exec()
}
: { status?: BookingStatus[]; startDateGreaterThan?: string } = {}) {
return prisma.booking.findMany({
where: {
startDate: { gte: startDateGreaterThan },
status: { notIn: status }
},
});
}
export async function createBooking({
startDate,
endDate,
purpose,
org,
destination,
name,
email,
phone,
street,
zip,
city,
}: Booking): Promise<Booking> {
const booking = new BookingModel({
startDate,
endDate,
purpose,
org,
destination,
name,
email,
phone,
street,
zip,
city,
})
await booking.save()
return booking.toJSON<Booking>()
export function createBooking(data: Booking) {
return prisma.booking.create({
data
});
}
export async function patchBooking(
uuid: string,
data: Prisma.BookingCreateInput
) {
const current = await prisma.booking.update({
where: { uuid },
data
});
const previous = { ...current, ...data }
return { current, previous }
}
export function createBill(
bookingUUID: string,
bookingData: Booking
): Promise<{ current: Booking; previous: Booking }> {
const booking = await getBookingByUUID(bookingUUID)
const oldBooking = booking.toJSON<Booking>()
booking.set(bookingData)
await booking.save()
return { current: booking.toJSON<Booking>(), previous: oldBooking }
data: Prisma.BillCreateInput,
) {
return prisma.bill.create({
data: {
...data,
booking: {
connect: { uuid: bookingUUID }
}
}
});
}
export async function createBill(
export function patchBill(
bookingUUID: string,
billData: Bill
): Promise<Bill> {
const booking = await getBookingByUUID(bookingUUID)
const bill = new BillModel()
bill.set(billData)
await bill.save()
booking.bill = bill._id
await booking.save()
return bill.toJSON<Bill>()
data: Prisma.BillUncheckedUpdateInput,
) {
const { id, ...rest } = data;
return prisma.bill.update({
where: {
},
data: {
...rest,
booking: {
connect: { uuid: bookingUUID }
}
}
});
}
export async function patchBill(
bookingUUID: string,
billData: Bill
): Promise<Bill> {
const booking = await getBookingByUUID(bookingUUID)
const bill =
(booking.bill && (await BillModel.findById(booking.bill))) ||
(await BillModel.create({}))
export async function getMilageMax() {
const { milageEnd } = await prisma.bill.findFirst({
select: { milageEnd: true },
orderBy: [
{
milageEnd: 'desc',
},
],
});
bill.set(billData)
await bill.save()
if (booking.bill !== bill._id) {
booking.bill = bill._id
await booking.save()
}
return bill.toJSON<Bill>()
}
export async function getMilageMax(): Promise<number> {
const billMaxMilageEnd = await BillModel.findOne({})
.sort('-milageEnd')
.select('milageEnd')
.exec()
return billMaxMilageEnd?.milageEnd || 0
return milageEnd || 0;
}

View File

@@ -17,6 +17,6 @@ export function getNextBigger<T>(array: T[], pivot: T): T {
return array.sort().find((day) => day > pivot)
}
export function uniqueFilter(value, index, self) {
export function uniqueFilter<T>(value: T, index: number, self: T[]) {
return self.indexOf(value) === index
}

View File

@@ -1,79 +1,27 @@
import { MILAGE_TARIFS } from '../db/enums'
import { AdditionalCost, Bill } from '../db/bill'
import { Bill, Prisma } from '@prisma/client'
import fetch from './fetch'
function roundToCent(amount: number): number {
return Math.round(amount * 100) / 100
}
export function getMilageCosts({
tarif,
km,
}: {
tarif: MILAGE_TARIFS
km: number
}): number {
if (tarif === MILAGE_TARIFS.NOCHARGE) {
return 0
}
if (km <= 0) {
return 0
}
let rate: number
if (tarif === MILAGE_TARIFS.EXTERN) {
if (km <= 200) {
rate = 0.42
} else if (km <= 1000) {
rate = 0.25
} else if (km <= 2000) {
rate = 0.2
} else {
rate = 0.18
}
}
if (tarif === MILAGE_TARIFS.INTERN) {
if (km <= 200) {
rate = 0.37
} else if (km <= 1000) {
rate = 0.22
} else if (km <= 2000) {
rate = 0.15
} else {
rate = 0.13
}
}
if (rate === undefined) {
throw Error('Unable to determine rate')
}
return roundToCent(km * rate)
}
export function getBillTotal({
tarif,
milage,
additionalCosts,
}: {
tarif: MILAGE_TARIFS
tarif: Prisma.Decimal
milage?: number
additionalCosts: AdditionalCost[]
}): number {
const milageCosts = getMilageCosts({ tarif, km: milage })
additionalCosts: Prisma.AdditionalCostsCreateInput[]
}): Prisma.Decimal {
const milageCosts = tarif.mul(milage)
const additionalCostsSum = additionalCosts
.map(({ value }) => value)
.reduce((acc: number, value: number) => acc + value, 0)
.reduce((acc, {value} ) => (value as Prisma.Decimal).plus(acc), new Prisma.Decimal(0))
return roundToCent(milageCosts + additionalCostsSum)
return additionalCostsSum.add(milageCosts).toDecimalPlaces(2);
}
export async function createBill(
bookingUuid: string,
bill: Bill
bill: Prisma.BillCreateInput
): Promise<Bill> {
return fetch(`/api/bookings/${bookingUuid}/bill`, {
method: 'POST',
@@ -83,7 +31,7 @@ export async function createBill(
export async function patchBill(
bookingUuid: string,
bill: Bill
bill: Prisma.BillUpdateInput
): Promise<Bill> {
return fetch(`/api/bookings/${bookingUuid}/bill`, {
method: 'POST',

View File

@@ -1,23 +1,22 @@
import { BookFormData } from '../context/book'
import { BOOKING_STATUS } from '../db/enums'
import { Prisma, BookingStatus } from '@prisma/client'
import fetch from './fetch'
export function getBookingStatus(status: BOOKING_STATUS) {
export function getBookingStatus(status: BookingStatus) {
switch (status) {
case BOOKING_STATUS.REQUESTED:
case BookingStatus.REQUESTED:
return 'Angefragt'
case BOOKING_STATUS.CONFIRMED:
case BookingStatus.CONFIRMED:
return 'Bestätigt'
case BOOKING_STATUS.REJECTED:
case BookingStatus.REJECTED:
return 'Abgewiesen'
case BOOKING_STATUS.CANCELED:
case BookingStatus.CANCELED:
return 'Storniert'
default:
return 'Unbekannt - bitte kontaktieren Sie uns!'
}
}
export async function createBooking(formData: BookFormData) {
export async function createBooking(formData: Prisma.BookingCreateInput) {
return fetch('/api/bookings', {
method: 'POST',
body: formData,
@@ -27,13 +26,13 @@ export async function createBooking(formData: BookFormData) {
export async function cancelBooking(uuid: string) {
return fetch(`/api/bookings/${uuid}`, {
method: 'PATCH',
body: { status: BOOKING_STATUS.CANCELED },
body: { status: BookingStatus.CANCELED },
})
}
export async function patchBooking(
uuid: string,
bookingData: Partial<BookFormData>
bookingData: Prisma.BookingUpdateInput
) {
return fetch(`/api/bookings/${uuid}`, {
method: 'PATCH',

View File

@@ -1,4 +1,4 @@
import { parse, format, addDays, subDays } from 'date-fns'
import { parse, format, addDays, subDays, differenceInDays } from 'date-fns'
import { utcToZonedTime } from 'date-fns-tz'
const FRONTEND_FORMAT = 'dd.MM.yyyy'
@@ -23,18 +23,18 @@ export function getDays({
endDate,
endDateExclusive = false,
}: {
startDate: Date
endDate: Date
startDate: string,
endDate: string,
endDateExclusive?: boolean
}): string[] {
let currentDay = new Date(startDate.getTime())
let currentDay = new Date(startDate);
const days = [dateFormatBackend(currentDay)]
if (!endDate) {
return days
}
const inclusiveEndDate = endDateExclusive ? subDays(endDate, 1) : endDate
const inclusiveEndDate = endDateExclusive ? subDays(new Date(endDate), 1) : new Date(endDate)
while (currentDay < inclusiveEndDate) {
currentDay = addDays(currentDay, 1)
@@ -84,3 +84,8 @@ export function nowInTz(timezone = 'Europe/Berlin'): Date {
export function getNextDay(date: Date) {
return addDays(date, 1)
}
export function getDayCount({ startDate, endDate }: { startDate: string, endDate: string }) {
// TODO: check if this actually works as expected
return differenceInDays(new Date(startDate), new Date(endDate)) + 1 // add one as it only counts full days;
}

View File

@@ -1,8 +1,7 @@
import { createEvents, createEvent, EventStatus } from 'ics'
import { Booking } from '../db/booking'
import { BOOKING_STATUS } from '../db/enums'
import { Booking, BookingStatus} from '@prisma/client';
import { getBaseURL } from './url'
import { daysFormatFrontend } from './date'
import { dateFormatFrontend, getDayCount } from './date'
function convertDay(value: string): [number, number, number] {
const parts = value.split('-')
@@ -16,9 +15,9 @@ export function generateCalendarEntry(booking: Booking): string {
const { error, value } = createEvent({
productId: 'app.vercel.pfadi-bussle/ics',
title: `Pfadi-Bussle Buchung`,
start: convertDay(booking.days[0]),
start: convertDay(booking.startDate),
startOutputType: 'local',
duration: { days: booking.days.length },
duration: { days: getDayCount(booking) },
location: 'Mömpelgardgasse 25, 72348 Rosenfeld, Deutschland',
geo: { lat: 48.287044, lon: 8.726361 },
description: `Gebucht auf ${booking.name}
@@ -26,7 +25,7 @@ export function generateCalendarEntry(booking: Booking): string {
Buchungs-Link: ${getBaseURL()}/bookings/${booking.uuid}
`,
status:
booking.status === BOOKING_STATUS.CONFIRMED
booking.status === BookingStatus.CONFIRMED
? ('CONFIRMED' as EventStatus)
: ('TENTATIVE' as EventStatus),
})
@@ -54,12 +53,12 @@ export function generateBookedCalendar(bookings: Booking[]): string {
} => ({
productId: 'app.vercel.pfadi-bussle/ics',
calName: 'Pfadi-Bussle Buchungen',
start: convertDay(booking.days[0]),
start: convertDay(booking.startDate),
startOutputType: 'local',
duration: { days: booking.days.length },
duration: { days: getDayCount(booking) },
title: `Buchung ${booking.name}`,
description: `Name: ${booking.name}
Zeitraum: ${daysFormatFrontend(booking.days)}
Zeitraum: ${dateFormatFrontend(new Date(booking.startDate))}-${dateFormatFrontend(new Date(booking.endDate))}
Email: ${booking.email}
Telefon: ${booking.phone}
@@ -67,7 +66,7 @@ Telefon: ${booking.phone}
Link: ${getBaseURL()}/admin/bookings/${booking.uuid}
`,
status:
booking.status === BOOKING_STATUS.CONFIRMED
booking.status === BookingStatus.CONFIRMED
? ('CONFIRMED' as EventStatus)
: ('TENTATIVE' as EventStatus),
})

View File

@@ -1,7 +1,7 @@
import { Booking } from '../db/booking'
import { Booking, Prisma } from '@prisma/client';
import { getBaseURL } from '../helpers/url'
import { log } from '../helpers/log'
import { daysFormatFrontend } from './date'
import { dateFormatFrontend } from './date'
import { generateCalendarEntry } from './ical'
import sgMail from '@sendgrid/mail'
@@ -34,10 +34,11 @@ Tel. 0151/212 253 62
${getBaseURL()}
`
function getReceivedBookingBookerText(booking: Booking): string {
function getReceivedBookingBookerText(booking: Prisma.BookingCreateInput): string {
return `Hallo liebe/r ${booking.name},
Vielen Dank für Deine Buchungsanfrage zum ${daysFormatFrontend(booking.days)}!
Vielen Dank für Deine Buchungsanfrage vom
${dateFormatFrontend(new Date(booking.startDate))} bis ${dateFormatFrontend(new Date(booking.endDate))}
Nach Prüfung bestätigen wir die Buchung bald per E-Mail!
@@ -54,9 +55,9 @@ ${footer}
function getBookingConfirmedText(booking: Booking): string {
return `Hallo liebe/r ${booking.name},
deine Buchunganfrage zum ${daysFormatFrontend(
booking.days
)} bestätigen wir gerne!
deine Buchunganfrage vom
${dateFormatFrontend(new Date(booking.startDate))} bis ${dateFormatFrontend(new Date(booking.endDate))}
bestätigen wir gerne!
Bitte melde dich spätestens 7 Tage vor dem Buchungstermin per E-Mail oder Telefon
um eine Schlüsselübergabe zu vereinbaren.
@@ -71,9 +72,9 @@ ${footer}
function getBookingRejectedText(booking: Booking): string {
return `Hallo liebe/r ${booking.name},
es tut uns leid, aber deine Buchungsanfrage zum ${daysFormatFrontend(
booking.days
)} konnten wir leider nicht bestätigen.
es tut uns leid, aber deine Buchungsanfrage vom
${dateFormatFrontend(new Date(booking.startDate))} bis ${dateFormatFrontend(new Date(booking.endDate))}
konnten wir leider nicht bestätigen.
Willst du das Bussle an einem anderen Termin buchen? Dann stelle bitte nochmal
eine Buchungsanfrage auf ${getBaseURL()}.
@@ -85,7 +86,9 @@ ${footer}
function getBookingCanceledText(booking: Booking): string {
return `Hallo liebe/r ${booking.name},
deine Buchungsanfrage zum ${daysFormatFrontend(booking.days)} wurde storniert.
deine Buchungsanfrage vom
${dateFormatFrontend(new Date(booking.startDate))} bis ${dateFormatFrontend(new Date(booking.endDate))}
wurde storniert.
Willst du das Bussle an einem anderen Termin buchen? Dann stelle bitte nochmal
eine Buchungsanfrage auf ${getBaseURL()}.
@@ -109,7 +112,7 @@ export async function sendReceivedBookingAdminMail(
await sendMail({
to: [{ email: ADMIN_EMAIL }],
from: { email: FROM_EMAIL, name: 'Pfadi-Bussle Wart' },
subject: `Buchung für ${booking.days} eingegangen!`,
subject: `Buchung für ${booking.startDate}-${booking.endDate} eingegangen!`,
textPlainContent: getReceivedBookingAdminText(booking),
})
} catch (error) {
@@ -118,7 +121,7 @@ export async function sendReceivedBookingAdminMail(
}
export async function sendReceivedBookingBookerMail(
booking: Booking
booking: Prisma.BookingCreateInput
): Promise<void> {
try {
await sendMail({

View File

@@ -1,4 +1,4 @@
import { Booking } from '../db/booking'
import { Booking } from '@prisma/client'
import { log } from '../helpers/log'
const BOOKING_DATA_KEY = 'pfadiBussleBookingData'

View File

@@ -1,39 +1,36 @@
import { Booking } from '@prisma/client';
import { startOfYear } from 'date-fns'
import { nowInTz } from '../helpers/date'
import { getBookingByUUID, getBookings } from '../db/index'
export type ServerSideBooking = {
props: {
booking: object
booking: Booking
}
}
export type ServerSideRecentBooking = {
props: {
bookings: object[]
bookings: Booking[]
}
}
export const getServerSideRecentBookings =
async (): Promise<ServerSideRecentBooking> => {
async () => {
const bookings = await getBookings({
startDateGreaterThan: startOfYear(nowInTz()).toISOString(),
})
// TODO: hack, not sure why _id is not serilizable
const bookingsJSON = JSON.parse(
JSON.stringify(bookings.map((b) => b.toJSON()))
) as object[]
return {
props: {
bookings: bookingsJSON,
bookings: bookings,
},
}
}
export const getServerSideBooking = async (
context: any
): Promise<ServerSideBooking> => {
) => {
const {
res,
params: { uuid: uuids },
@@ -47,11 +44,7 @@ export const getServerSideBooking = async (
return { props: { booking: null } }
}
await booking.populate('bill')
// TODO: hack, not sure why _id is not serilizable
const bookingJSON = JSON.parse(JSON.stringify(booking.toJSON())) as object
return {
props: { booking: bookingJSON },
props: { booking },
}
}

View File

@@ -1,6 +1,6 @@
import { Booking } from '@prisma/client';
import { google } from 'googleapis'
import { getBaseURL } from '../helpers/url'
import { Booking } from '../db/booking'
import { getDays, getNextDay, dateFormatBackend } from '../helpers/date'
import { log } from '../helpers/log'
@@ -39,15 +39,15 @@ export async function getBookedDays() {
.filter((event) => !!event.start.date)
.flatMap((event) =>
getDays({
startDate: new Date(event.start.date),
endDate: new Date(event.end.date),
startDate: event.start.date,
endDate: event.end.date,
endDateExclusive: true,
})
)
)
}
function getSummary(booking: Partial<Booking>): string {
function getSummary(booking: Booking): string {
let summary = ''
if (booking.org) {
@@ -59,13 +59,13 @@ function getSummary(booking: Partial<Booking>): string {
return summary
}
function getDescription(booking: Booking): string {
function getDescription(booking: Booking) {
const bookingUrl = `${getBaseURL()}/admin/booking/${booking.uuid}`
return `Managelink ${bookingUrl}`
}
export async function createCalendarEvent(booking: Booking): Promise<Booking> {
export async function createCalendarEvent(booking: Booking) {
const exclusiveEndDate = dateFormatBackend(
getNextDay(new Date(booking.endDate))
)
@@ -85,7 +85,7 @@ export async function createCalendarEvent(booking: Booking): Promise<Booking> {
return booking
}
export async function deleteCalendarEvent(booking: Booking) {
export async function deleteCalendarEvent(booking: { calendarEventId: string }) {
await calendar.events.delete({
calendarId,
eventId: booking.calendarEventId,
@@ -96,11 +96,11 @@ export async function deleteCalendarEvent(booking: Booking) {
booking.calendarEventId = null
}
//export async function patchCalendarEvent(booking: { calendarEventId: string } & Partial<Booking> ) : Promise<Booking> {
// const response = await calendar.events.patch({
// calendarId,
// eventId: booking.calendarEventId,
// });
//
// return booking;
//}
export async function patchCalendarEvent(booking: Booking ) {
await calendar.events.patch({
calendarId,
eventId: booking.calendarEventId,
});
return booking;
}

377
package-lock.json generated
View File

@@ -8,7 +8,8 @@
"name": "pfadi-bussle",
"version": "0.1.0",
"dependencies": {
"@next-auth/mongodb-adapter": "1.1.1",
"@next-auth/prisma-adapter": "^1.0.4",
"@prisma/client": "^4.4.0",
"@sendgrid/mail": "7.7.0",
"autoprefixer": "10.4.12",
"classnames": "2.3.2",
@@ -39,9 +40,10 @@
"postcss-flexbugs-fixes": "5.0.2",
"postcss-preset-env": "7.8.2",
"prettier": "2.7.1",
"prisma": "^4.4.0",
"swr": "1.3.0",
"tailwindcss": "3.1.8",
"ts-jest": "29.0.3"
"ts-jest": "^29.0.3"
}
},
"node_modules/@ampproject/remapping": {
@@ -643,9 +645,9 @@
"dev": true
},
"node_modules/@csstools/postcss-cascade-layers": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.0.tgz",
"integrity": "sha512-XpA7g2KViA2ia23A5kZ/EQw+Sy308kLbvMlDPjFZmojwaJ9DYdJuwujFcDGK9v1QhHRmMEHbV2brVSQSLkN/7A==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.1.tgz",
"integrity": "sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA==",
"dev": true,
"dependencies": {
"@csstools/selector-specificity": "^2.0.2",
@@ -953,9 +955,9 @@
"dev": true
},
"node_modules/@eslint/eslintrc/node_modules/globals": {
"version": "13.15.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz",
"integrity": "sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==",
"version": "13.17.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz",
"integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==",
"dev": true,
"dependencies": {
"type-fest": "^0.20.2"
@@ -992,9 +994,9 @@
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.10.5",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.5.tgz",
"integrity": "sha512-XVVDtp+dVvRxMoxSiSfasYaG02VEe1qH5cKgMQJWhol6HwzbcqoCMJi8dAGoYAO57jhUyhI6cWuRiTcRaDaYug==",
"version": "0.10.7",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz",
"integrity": "sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==",
"dev": true,
"dependencies": {
"@humanwhocodes/object-schema": "^1.2.1",
@@ -1419,12 +1421,12 @@
"react": ">=16"
}
},
"node_modules/@next-auth/mongodb-adapter": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@next-auth/mongodb-adapter/-/mongodb-adapter-1.1.1.tgz",
"integrity": "sha512-X5O4U4l2M8nyp/B3qF5GOr/JJw2ShKgWfTZRa80Y5CUzTPPmf09ggL5v5UwCmz9l2RIv2GUxO8hK4qrcaZvDRw==",
"node_modules/@next-auth/prisma-adapter": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@next-auth/prisma-adapter/-/prisma-adapter-1.0.4.tgz",
"integrity": "sha512-jIOM6CzCbl2/Mzbx9kb2IjtHoJOeRN9wtQgLk4EUm5bhneSVGv1rtz5TDskvp2UfCa+EK9nDmug+lje41z80Gg==",
"peerDependencies": {
"mongodb": "^4.1.1",
"@prisma/client": ">=2.26.0 || >=3",
"next-auth": "^4"
}
},
@@ -1700,6 +1702,38 @@
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/@prisma/client": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.4.0.tgz",
"integrity": "sha512-ciKOP246x1xwr04G9ajHlJ4pkmtu9Q6esVyqVBO0QJihaKQIUvbPjClp17IsRJyxqNpFm4ScbOc/s9DUzKHINQ==",
"hasInstallScript": true,
"dependencies": {
"@prisma/engines-version": "4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6"
},
"engines": {
"node": ">=14.17"
},
"peerDependencies": {
"prisma": "*"
},
"peerDependenciesMeta": {
"prisma": {
"optional": true
}
}
},
"node_modules/@prisma/engines": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.4.0.tgz",
"integrity": "sha512-Fpykccxlt9MHrAs/QpPGpI2nOiRxuLA+LiApgA59ibbf24YICZIMWd3SI2YD+q0IAIso0jCGiHhirAIbxK3RyQ==",
"devOptional": true,
"hasInstallScript": true
},
"node_modules/@prisma/engines-version": {
"version": "4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6.tgz",
"integrity": "sha512-P5v/PuEIJLYXZUZBvOLPqoyCW+m6StNqHdiR6te++gYVODpPdLakks5HVx3JaZIY+LwR02juJWFlwpc9Eog/ug=="
},
"node_modules/@rushstack/eslint-patch": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.1.3.tgz",
@@ -2717,9 +2751,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001407",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001407.tgz",
"integrity": "sha512-4ydV+t4P7X3zH83fQWNDX/mQEzYomossfpViCOx9zHBSMV+rIe3LFqglHHtVyvNl1FhTNxPxs3jei82iqOW04w==",
"version": "1.0.30001418",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz",
"integrity": "sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg==",
"funding": [
{
"type": "opencollective",
@@ -2846,14 +2880,17 @@
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
},
"node_modules/cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/co": {
@@ -3225,9 +3262,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.4.256",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.256.tgz",
"integrity": "sha512-x+JnqyluoJv8I0U9gVe+Sk2st8vF0CzMt78SXxuoWCooLLY2k5VerIBdpvG7ql6GKI4dzNnPjmqgDJ76EdaAKw=="
"version": "1.4.276",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.276.tgz",
"integrity": "sha512-EpuHPqu8YhonqLBXHoU6hDJCD98FCe6KDoet3/gY1qsQ6usjJoHqBH2YIVs8FXaAtHwVL8Uqa/fsYao/vq9VWQ=="
},
"node_modules/emittery": {
"version": "0.10.2",
@@ -3257,22 +3294,22 @@
}
},
"node_modules/es-abstract": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.2.tgz",
"integrity": "sha512-XxXQuVNrySBNlEkTYJoDNFe5+s2yIOpzq80sUHEdPdQr0S5nTLz4ZPPPswNIpKseDDUS5yghX1gfLIHQZ1iNuQ==",
"version": "1.20.4",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz",
"integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.2",
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
"function.prototype.name": "^1.1.5",
"get-intrinsic": "^1.1.2",
"get-intrinsic": "^1.1.3",
"get-symbol-description": "^1.0.0",
"has": "^1.0.3",
"has-property-descriptors": "^1.0.0",
"has-symbols": "^1.0.3",
"internal-slot": "^1.0.3",
"is-callable": "^1.2.4",
"is-callable": "^1.2.7",
"is-negative-zero": "^2.0.2",
"is-regex": "^1.1.4",
"is-shared-array-buffer": "^1.0.2",
@@ -3282,6 +3319,7 @@
"object-keys": "^1.1.1",
"object.assign": "^4.1.4",
"regexp.prototype.flags": "^1.4.3",
"safe-regex-test": "^1.0.0",
"string.prototype.trimend": "^1.0.5",
"string.prototype.trimstart": "^1.0.5",
"unbox-primitive": "^1.0.2"
@@ -3644,9 +3682,9 @@
"dev": true
},
"node_modules/eslint-plugin-react": {
"version": "7.31.8",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.8.tgz",
"integrity": "sha512-5lBTZmgQmARLLSYiwI71tiGVTLUuqXantZM6vlSY39OaDSV0M7+32K5DnLkmFrwTe+Ksz0ffuLUC91RUviVZfw==",
"version": "7.31.10",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.10.tgz",
"integrity": "sha512-e4N/nc6AAlg4UKW/mXeYWd3R++qUano5/o+t+wnWxIf+bLsOaH3a4q74kX3nDjYym3VBN4HyO9nEn1GcAqgQOA==",
"dev": true,
"dependencies": {
"array-includes": "^3.1.5",
@@ -3695,34 +3733,18 @@
"node": ">=0.10.0"
}
},
"node_modules/eslint-plugin-react/node_modules/estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
"engines": {
"node": ">=4.0"
}
},
"node_modules/eslint-plugin-react/node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/eslint-plugin-react/node_modules/resolve": {
"version": "2.0.0-next.3",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz",
"integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==",
"version": "2.0.0-next.4",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz",
"integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==",
"dev": true,
"dependencies": {
"is-core-module": "^2.2.0",
"path-parse": "^1.0.6"
"is-core-module": "^2.9.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -3983,9 +4005,9 @@
}
},
"node_modules/estraverse": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
"integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
"engines": {
"node": ">=4.0"
@@ -4092,7 +4114,7 @@
"node_modules/exit": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
"integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
"integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==",
"dev": true,
"engines": {
"node": ">= 0.8.0"
@@ -4952,9 +4974,9 @@
}
},
"node_modules/is-callable": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz",
"integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==",
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
"dev": true,
"engines": {
"node": ">= 0.4"
@@ -8453,6 +8475,23 @@
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
"dev": true
},
"node_modules/prisma": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-4.4.0.tgz",
"integrity": "sha512-l/QKLmLcKJQFuc+X02LyICo0NWTUVaNNZ00jKJBqwDyhwMAhboD1FWwYV50rkH4Wls0RviAJSFzkC2ZrfawpfA==",
"devOptional": true,
"hasInstallScript": true,
"dependencies": {
"@prisma/engines": "4.4.0"
},
"bin": {
"prisma": "build/index.js",
"prisma2": "build/index.js"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/prompts": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
@@ -8467,13 +8506,13 @@
}
},
"node_modules/prop-types": {
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.8.1"
"react-is": "^16.13.1"
}
},
"node_modules/property-expr": {
@@ -8690,7 +8729,7 @@
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
"engines": {
"node": ">=0.10.0"
@@ -8807,6 +8846,20 @@
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/safe-regex-test": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
"integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.2",
"get-intrinsic": "^1.1.3",
"is-regex": "^1.1.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/saslprep": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz",
@@ -9622,9 +9675,9 @@
}
},
"node_modules/update-browserslist-db": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.9.tgz",
"integrity": "sha512-/xsqn21EGVdXI3EXSum1Yckj3ZVZugqyOZQ/CxYPBD/R+ko9NSUScf8tFF4dOKY+2pvSSJA/S+5B8s4Zr4kyvg==",
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz",
"integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==",
"funding": [
{
"type": "opencollective",
@@ -9922,12 +9975,12 @@
}
},
"node_modules/yargs": {
"version": "17.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.4.1.tgz",
"integrity": "sha512-WSZD9jgobAg3ZKuCQZSa3g9QOJeCCqLoLAykiWgmXnDo9EPnn4RPf5qVTtzgOx66o6/oqhcA5tHtJXpG8pMt3g==",
"version": "17.6.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.0.tgz",
"integrity": "sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==",
"dev": true,
"dependencies": {
"cliui": "^7.0.2",
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
@@ -10439,9 +10492,9 @@
"dev": true
},
"@csstools/postcss-cascade-layers": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.0.tgz",
"integrity": "sha512-XpA7g2KViA2ia23A5kZ/EQw+Sy308kLbvMlDPjFZmojwaJ9DYdJuwujFcDGK9v1QhHRmMEHbV2brVSQSLkN/7A==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.1.tgz",
"integrity": "sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA==",
"dev": true,
"requires": {
"@csstools/selector-specificity": "^2.0.2",
@@ -10598,9 +10651,9 @@
"dev": true
},
"globals": {
"version": "13.15.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz",
"integrity": "sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==",
"version": "13.17.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz",
"integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==",
"dev": true,
"requires": {
"type-fest": "^0.20.2"
@@ -10624,9 +10677,9 @@
}
},
"@humanwhocodes/config-array": {
"version": "0.10.5",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.5.tgz",
"integrity": "sha512-XVVDtp+dVvRxMoxSiSfasYaG02VEe1qH5cKgMQJWhol6HwzbcqoCMJi8dAGoYAO57jhUyhI6cWuRiTcRaDaYug==",
"version": "0.10.7",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz",
"integrity": "sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==",
"dev": true,
"requires": {
"@humanwhocodes/object-schema": "^1.2.1",
@@ -10957,10 +11010,10 @@
"@types/react": ">=16"
}
},
"@next-auth/mongodb-adapter": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@next-auth/mongodb-adapter/-/mongodb-adapter-1.1.1.tgz",
"integrity": "sha512-X5O4U4l2M8nyp/B3qF5GOr/JJw2ShKgWfTZRa80Y5CUzTPPmf09ggL5v5UwCmz9l2RIv2GUxO8hK4qrcaZvDRw==",
"@next-auth/prisma-adapter": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@next-auth/prisma-adapter/-/prisma-adapter-1.0.4.tgz",
"integrity": "sha512-jIOM6CzCbl2/Mzbx9kb2IjtHoJOeRN9wtQgLk4EUm5bhneSVGv1rtz5TDskvp2UfCa+EK9nDmug+lje41z80Gg==",
"requires": {}
},
"@next/env": {
@@ -11102,6 +11155,25 @@
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.0.1.tgz",
"integrity": "sha512-mMyQ9vjpuFqePkfe5bZVIf/H3Dmk6wA8Kjxff9RcO4kqzJo+Ek9pGKwZHpeMr7Eku0QhLXMCd7fNCSnEnRMubg=="
},
"@prisma/client": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.4.0.tgz",
"integrity": "sha512-ciKOP246x1xwr04G9ajHlJ4pkmtu9Q6esVyqVBO0QJihaKQIUvbPjClp17IsRJyxqNpFm4ScbOc/s9DUzKHINQ==",
"requires": {
"@prisma/engines-version": "4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6"
}
},
"@prisma/engines": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.4.0.tgz",
"integrity": "sha512-Fpykccxlt9MHrAs/QpPGpI2nOiRxuLA+LiApgA59ibbf24YICZIMWd3SI2YD+q0IAIso0jCGiHhirAIbxK3RyQ==",
"devOptional": true
},
"@prisma/engines-version": {
"version": "4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6.tgz",
"integrity": "sha512-P5v/PuEIJLYXZUZBvOLPqoyCW+m6StNqHdiR6te++gYVODpPdLakks5HVx3JaZIY+LwR02juJWFlwpc9Eog/ug=="
},
"@rushstack/eslint-patch": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.1.3.tgz",
@@ -11874,9 +11946,9 @@
"dev": true
},
"caniuse-lite": {
"version": "1.0.30001407",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001407.tgz",
"integrity": "sha512-4ydV+t4P7X3zH83fQWNDX/mQEzYomossfpViCOx9zHBSMV+rIe3LFqglHHtVyvNl1FhTNxPxs3jei82iqOW04w=="
"version": "1.0.30001418",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz",
"integrity": "sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg=="
},
"ccount": {
"version": "2.0.1",
@@ -11953,13 +12025,13 @@
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
},
"cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
"requires": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
}
},
@@ -12217,9 +12289,9 @@
}
},
"electron-to-chromium": {
"version": "1.4.256",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.256.tgz",
"integrity": "sha512-x+JnqyluoJv8I0U9gVe+Sk2st8vF0CzMt78SXxuoWCooLLY2k5VerIBdpvG7ql6GKI4dzNnPjmqgDJ76EdaAKw=="
"version": "1.4.276",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.276.tgz",
"integrity": "sha512-EpuHPqu8YhonqLBXHoU6hDJCD98FCe6KDoet3/gY1qsQ6usjJoHqBH2YIVs8FXaAtHwVL8Uqa/fsYao/vq9VWQ=="
},
"emittery": {
"version": "0.10.2",
@@ -12243,22 +12315,22 @@
}
},
"es-abstract": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.2.tgz",
"integrity": "sha512-XxXQuVNrySBNlEkTYJoDNFe5+s2yIOpzq80sUHEdPdQr0S5nTLz4ZPPPswNIpKseDDUS5yghX1gfLIHQZ1iNuQ==",
"version": "1.20.4",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz",
"integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==",
"dev": true,
"requires": {
"call-bind": "^1.0.2",
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
"function.prototype.name": "^1.1.5",
"get-intrinsic": "^1.1.2",
"get-intrinsic": "^1.1.3",
"get-symbol-description": "^1.0.0",
"has": "^1.0.3",
"has-property-descriptors": "^1.0.0",
"has-symbols": "^1.0.3",
"internal-slot": "^1.0.3",
"is-callable": "^1.2.4",
"is-callable": "^1.2.7",
"is-negative-zero": "^2.0.2",
"is-regex": "^1.1.4",
"is-shared-array-buffer": "^1.0.2",
@@ -12268,6 +12340,7 @@
"object-keys": "^1.1.1",
"object.assign": "^4.1.4",
"regexp.prototype.flags": "^1.4.3",
"safe-regex-test": "^1.0.0",
"string.prototype.trimend": "^1.0.5",
"string.prototype.trimstart": "^1.0.5",
"unbox-primitive": "^1.0.2"
@@ -12638,9 +12711,9 @@
}
},
"eslint-plugin-react": {
"version": "7.31.8",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.8.tgz",
"integrity": "sha512-5lBTZmgQmARLLSYiwI71tiGVTLUuqXantZM6vlSY39OaDSV0M7+32K5DnLkmFrwTe+Ksz0ffuLUC91RUviVZfw==",
"version": "7.31.10",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.10.tgz",
"integrity": "sha512-e4N/nc6AAlg4UKW/mXeYWd3R++qUano5/o+t+wnWxIf+bLsOaH3a4q74kX3nDjYym3VBN4HyO9nEn1GcAqgQOA==",
"dev": true,
"requires": {
"array-includes": "^3.1.5",
@@ -12668,31 +12741,15 @@
"esutils": "^2.0.2"
}
},
"estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true
},
"prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"requires": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"resolve": {
"version": "2.0.0-next.3",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz",
"integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==",
"version": "2.0.0-next.4",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz",
"integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==",
"dev": true,
"requires": {
"is-core-module": "^2.2.0",
"path-parse": "^1.0.6"
"is-core-module": "^2.9.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
}
},
"semver": {
@@ -12787,9 +12844,9 @@
}
},
"estraverse": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
"integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true
},
"estree-util-attach-comments": {
@@ -12867,7 +12924,7 @@
"exit": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
"integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
"integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==",
"dev": true
},
"expect": {
@@ -13486,9 +13543,9 @@
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ=="
},
"is-callable": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz",
"integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==",
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
"dev": true
},
"is-core-module": {
@@ -15831,6 +15888,15 @@
}
}
},
"prisma": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-4.4.0.tgz",
"integrity": "sha512-l/QKLmLcKJQFuc+X02LyICo0NWTUVaNNZ00jKJBqwDyhwMAhboD1FWwYV50rkH4Wls0RviAJSFzkC2ZrfawpfA==",
"devOptional": true,
"requires": {
"@prisma/engines": "4.4.0"
}
},
"prompts": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
@@ -15842,13 +15908,13 @@
}
},
"prop-types": {
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"requires": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.8.1"
"react-is": "^16.13.1"
}
},
"property-expr": {
@@ -15992,7 +16058,7 @@
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true
},
"resolve": {
@@ -16064,6 +16130,17 @@
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safe-regex-test": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
"integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==",
"dev": true,
"requires": {
"call-bind": "^1.0.2",
"get-intrinsic": "^1.1.3",
"is-regex": "^1.1.4"
}
},
"saslprep": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz",
@@ -16641,9 +16718,9 @@
}
},
"update-browserslist-db": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.9.tgz",
"integrity": "sha512-/xsqn21EGVdXI3EXSum1Yckj3ZVZugqyOZQ/CxYPBD/R+ko9NSUScf8tFF4dOKY+2pvSSJA/S+5B8s4Zr4kyvg==",
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz",
"integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==",
"requires": {
"escalade": "^3.1.1",
"picocolors": "^1.0.0"
@@ -16861,12 +16938,12 @@
"dev": true
},
"yargs": {
"version": "17.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.4.1.tgz",
"integrity": "sha512-WSZD9jgobAg3ZKuCQZSa3g9QOJeCCqLoLAykiWgmXnDo9EPnn4RPf5qVTtzgOx66o6/oqhcA5tHtJXpG8pMt3g==",
"version": "17.6.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.0.tgz",
"integrity": "sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==",
"dev": true,
"requires": {
"cliui": "^7.0.2",
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",

View File

@@ -11,7 +11,8 @@
"test": "jest"
},
"dependencies": {
"@next-auth/mongodb-adapter": "1.1.1",
"@next-auth/prisma-adapter": "^1.0.4",
"@prisma/client": "^4.4.0",
"@sendgrid/mail": "7.7.0",
"autoprefixer": "10.4.12",
"classnames": "2.3.2",
@@ -38,13 +39,14 @@
"eslint-config-next": "12.3.1",
"eslint-config-prettier": "8.5.0",
"jest": "29.1.2",
"ts-jest": "29.0.3",
"postcss": "8.4.17",
"postcss-flexbugs-fixes": "5.0.2",
"postcss-preset-env": "7.8.2",
"prettier": "2.7.1",
"prisma": "^4.4.0",
"swr": "1.3.0",
"tailwindcss": "3.1.8"
"tailwindcss": "3.1.8",
"ts-jest": "^29.0.3"
},
"jest": {
"transform": {

View File

@@ -1,10 +1,10 @@
import React, { useEffect, useState } from 'react'
import Input from '../../../../components/input'
import Select from '../../../../components/select'
import { Booking } from '../../../../db/booking'
import { BILL_STATUS, MILAGE_TARIFS } from '../../../../db/enums'
import { Booking, Bill, BillStatus, AdditionalCosts, Prisma } from '@prisma/client'
import { MILAGE_TARIFS } from '../../../../db/enums'
import { getMilageMax } from '../../../../db/index'
import { daysFormatFrontend } from '../../../../helpers/date'
import { dateFormatFrontend } from '../../../../helpers/date'
import { log } from '../../../../helpers/log'
import { getBillTotal, createBill, patchBill } from '../../../../helpers/bill'
import { getBookingStatus } from '../../../../helpers/booking'
@@ -29,7 +29,7 @@ const milageTarifOptions = Object.values(MILAGE_TARIFS).map((tarif) => {
)
})
const billStatusOptions = Object.values(BILL_STATUS).map((status) => {
const billStatusOptions = Object.values(BillStatus).map((status) => {
return (
<option value={status} key={status}>
{getBillStatusLabel(status)}
@@ -50,13 +50,13 @@ function getTarifLabel(tarif: MILAGE_TARIFS) {
}
}
function getBillStatusLabel(status: BILL_STATUS) {
function getBillStatusLabel(status: BillStatus) {
switch (status) {
case BILL_STATUS.UNINVOICED:
case BillStatus.UNINVOICED:
return 'Nicht gestellt'
case BILL_STATUS.INVOICED:
case BillStatus.INVOICED:
return 'Gestellt'
case BILL_STATUS.PAID:
case BillStatus.PAID:
return 'Bezahlt'
default:
return 'Unbekannt!!!'
@@ -67,7 +67,7 @@ function BookingBillPage({
booking: bookingProp,
milageMax,
}: {
booking: Booking
booking: Booking & { bill: Bill }
milageMax: number
}) {
const [booking, setBooking] = useState(bookingProp)
@@ -75,11 +75,11 @@ function BookingBillPage({
booking?.bill?.milageStart || milageMax
)
const [milageEnd, setMilageEnd] = useState(booking?.bill?.milageEnd)
const [tarif, setTarif] = useState(
booking?.bill?.tarif || MILAGE_TARIFS.EXTERN
const [tarif, setTarif] = useState<Prisma.Decimal>(
booking?.bill?.tarif
)
const [status, setStatus] = useState(booking?.bill?.status)
const [additionalCosts, setAdditionalCosts] = useState([])
const [additionalCosts, setAdditionalCosts] = useState<Prisma.AdditionalCostsCreateInput[]>([])
const [storingInProgress, setStoringInProgress] = useState(false)
const [storingError, setStoringError] = useState(null)
const milage =
@@ -99,10 +99,9 @@ function BookingBillPage({
const bill = await saveBill(booking.uuid, {
milageStart,
milageEnd,
milage,
tarif,
status,
additionalCosts,
additionalCosts: { create: additionalCosts },
})
booking.bill = bill
@@ -118,7 +117,7 @@ function BookingBillPage({
event: React.MouseEvent<HTMLButtonElement>
) {
event.preventDefault()
setAdditionalCosts([...additionalCosts, { name: '', value: 0 }])
setAdditionalCosts([...additionalCosts, { name: '', value: new Prisma.Decimal(0) } as AdditionalCosts])
}
const onRemoveAdditionalCost = function(
@@ -138,7 +137,7 @@ function BookingBillPage({
<form className="w-full" onSubmit={onSubmit}>
<div>
<strong>Buchungszeitraum:</strong>{' '}
{daysFormatFrontend(booking.days)}
{dateFormatFrontend(new Date(booking.startDate))}-{dateFormatFrontend(new Date(booking.endDate))}
</div>
<div>
<strong>Bucher:</strong> {booking.name}
@@ -171,8 +170,8 @@ function BookingBillPage({
<Select
label="Rate"
name="tarif"
value={tarif}
onChange={(e) => setTarif(e.target.value as MILAGE_TARIFS)}
value={tarif.toString()}
onChange={(e) => setTarif(new Prisma.Decimal(e.target.value))}
>
{milageTarifOptions}
</Select>
@@ -211,7 +210,7 @@ function BookingBillPage({
newAdditonalCosts[index] = {
value: newAdditonalCosts[index].value,
name: event.target.value,
}
} as AdditionalCosts
setAdditionalCosts(newAdditonalCosts)
}}
/>
@@ -219,13 +218,13 @@ function BookingBillPage({
label={`Betrag`}
name={`additionalCostValue${index}`}
key={`additionalCostValue${index}`}
value={additionalCosts[index].value}
value={additionalCosts[index].value.toString()}
type="number"
onChange={(event) => {
const newAdditonalCosts = [...additionalCosts]
newAdditonalCosts[index] = {
name: newAdditonalCosts[index].name,
value: Number(event.target.value),
value: new Prisma.Decimal(event.target.value),
}
setAdditionalCosts(newAdditonalCosts)
}}
@@ -234,13 +233,13 @@ function BookingBillPage({
</>
)
})}
<Input label="Summe" name="total" readOnly value={total} />
<Input label="Summe" name="total" readOnly value={total.toString()} />
</div>
<Select
label="Status"
name={status}
value={status}
onChange={(e) => setStatus(e.target.value as BILL_STATUS)}
onChange={(e) => setStatus(e.target.value as BillStatus)}
>
{billStatusOptions}
</Select>

View File

@@ -1,13 +1,12 @@
import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { BookingStatus, Booking } from '@prisma/client'
import Link from 'next/link'
import Calendar from '../../../../components/calendar'
import { getServerSideBooking } from '../../../../lib/getServerSideProps'
import { Booking } from '../../../../db/booking'
import { getBookingStatus, patchBooking } from '../../../../helpers/booking'
import { daysFormatFrontend } from '../../../../helpers/date'
import { log } from '../../../../helpers/log'
import { BOOKING_STATUS } from '../../../../db/enums'
export const getServerSideProps = getServerSideBooking
@@ -25,7 +24,7 @@ function ShowBookingAdmin({ booking: bookingProp }: { booking: Booking }) {
setStoringBookingError(null)
setStoringBooking(true)
const updatedBooking = await patchBooking(booking.uuid, {
status: confirmed ? BOOKING_STATUS.CONFIRMED : BOOKING_STATUS.REJECTED,
status: confirmed ? BookingStatus.CONFIRMED : BookingStatus.REJECTED,
})
setBooking(updatedBooking)
} catch (error) {
@@ -40,7 +39,7 @@ function ShowBookingAdmin({ booking: bookingProp }: { booking: Booking }) {
<h2 className="text-3xl">Buchung {booking.uuid}</h2>
<Calendar start={booking.startDate} end={booking.endDate} />
<div>
<strong>Buchungszeitraum:</strong> {daysFormatFrontend(booking.days)}
<strong>Buchungszeitraum:</strong> {daysFormatFrontend([booking.startDate])}-{daysFormatFrontend([booking.endDate])}
</div>
<div>
<strong>Bucher:</strong> {booking.name}

View File

@@ -1,64 +1,51 @@
import { NextApiRequest, NextApiResponse } from 'next'
import NextAuth from 'next-auth'
import EmailProvider from 'next-auth/providers/email'
import GitHubProvider from 'next-auth/providers/github'
import { MongoDBAdapter } from '@next-auth/mongodb-adapter'
import { MONGO_URI } from '../../../db'
import { MongoClient } from 'mongodb'
import { PrismaAdapter } from "@next-auth/prisma-adapter"
import { PrismaClient } from "@prisma/client"
let client: MongoClient
const prisma = new PrismaClient()
const ADMIN_EMAIL = process.env.ADMIN_EMAIL
const GITHUB_USERS_GRANTED = ['111471']
async function getMongoClient() {
if (!client) {
client = new MongoClient(MONGO_URI)
await client.connect()
}
return client
}
export default async function auth(req: NextApiRequest, res: NextApiResponse) {
return await NextAuth(req, res, {
secret: process.env.NEXTAUTH_SECRET,
adapter: MongoDBAdapter(getMongoClient()),
providers: [
GitHubProvider({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
}),
EmailProvider({
server: {
host: 'smtp.sendgrid.net',
port: 587,
auth: {
user: 'apikey',
pass: process.env.SENDGRID_API_KEY,
},
export default NextAuth({
secret: process.env.NEXTAUTH_SECRET,
adapter: PrismaAdapter(prisma),
providers: [
GitHubProvider({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
}),
EmailProvider({
server: {
host: 'smtp.sendgrid.net',
port: 587,
auth: {
user: 'apikey',
pass: process.env.SENDGRID_API_KEY,
},
from: process.env.FROM_EMAIL,
}),
],
callbacks: {
async signIn({ account, email }) {
// if user sigin requested magic link via EmailProvider
if (account.provider === 'email') {
if (email.verificationRequest) {
// only allow admins by email entered
return account.providerAccountId === ADMIN_EMAIL
}
// if user accesses with magic link, also only allow admin
return account.providerAccountId === ADMIN_EMAIL
} else if (account.provider === 'github') {
// only one and only one user
return GITHUB_USERS_GRANTED.includes(account.providerAccountId)
}
return false
},
from: process.env.FROM_EMAIL,
}),
],
callbacks: {
async signIn({ account, email }) {
// if user sigin requested magic link via EmailProvider
if (account.provider === 'email') {
if (email.verificationRequest) {
// only allow admins by email entered
return account.providerAccountId === ADMIN_EMAIL
}
// if user accesses with magic link, also only allow admin
return account.providerAccountId === ADMIN_EMAIL
} else if (account.provider === 'github') {
// only one and only one user
return GITHUB_USERS_GRANTED.includes(account.providerAccountId)
}
return false
},
})
}
},
});

View File

@@ -1,5 +1,5 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { Bill } from '../../../../db/bill'
import { Prisma } from '@prisma/client';
import { createBill, patchBill } from '../../../../db/index'
import { log } from '../../../../helpers/log'
@@ -13,7 +13,7 @@ export default async function billHandler(
} = req
const bookingUUID = Array.isArray(uuids) ? uuids[0] : uuids
let bill: Bill
let bill: Prisma.BillUpdateInput
switch (method) {
case 'POST':

View File

@@ -1,6 +1,5 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { Booking } from '../../../../db/booking'
import { BOOKING_STATUS } from '../../../../db/enums'
import { Prisma, BookingStatus } from '@prisma/client';
import { patchBooking } from '../../../../db/index'
import {
sendBookingConfirmed,
@@ -10,28 +9,27 @@ import {
import { log } from '../../../../helpers/log'
function changedStatus(
previous: Booking,
current: Partial<Booking>,
status: BOOKING_STATUS
previous: Prisma.BookingUpdateInput,
current: Prisma.BookingUpdateInput,
status: BookingStatus
): boolean {
return (
[BOOKING_STATUS.REQUESTED].includes(previous.status) &&
BookingStatus.REQUESTED === previous.status &&
current.status === status
)
}
function wasRejected(previous: Booking, current: Partial<Booking>): boolean {
return changedStatus(previous, current, BOOKING_STATUS.REJECTED)
function wasRejected(previous: Prisma.BookingUpdateInput, current: Prisma.BookingUpdateInput): boolean {
return changedStatus(previous, current, BookingStatus.REJECTED)
}
function wasConfirmed(previous: Booking, current: Partial<Booking>): boolean {
return changedStatus(previous, current, BOOKING_STATUS.CONFIRMED)
function wasConfirmed(previous: Prisma.BookingUpdateInput, current: Prisma.BookingUpdateInput): boolean {
return changedStatus(previous, current, BookingStatus.CONFIRMED)
}
function wasCanceled(previous: Booking, current: Partial<Booking>): boolean {
function wasCanceled(previous: Prisma.BookingUpdateInput, current: Prisma.BookingUpdateInput): boolean {
return (
[BOOKING_STATUS.REQUESTED, BOOKING_STATUS.CONFIRMED].includes(
previous.status
) && current.status === BOOKING_STATUS.CANCELED
[BookingStatus.REQUESTED, BookingStatus.CONFIRMED].find(s => s === previous.status)
&& current.status === BookingStatus.CANCELED
)
}
@@ -48,12 +46,12 @@ export default async function userHandler(
switch (method) {
case 'PATCH':
if (!Object.values(BOOKING_STATUS).includes(req.body.status)) {
if (!Object.values(BookingStatus).includes(req.body.status)) {
res
.status(400)
.end(
`The attribute status can only be: ${Object.values(
BOOKING_STATUS
BookingStatus
).join(', ')}`
)
break

View File

@@ -1,6 +1,5 @@
import { Error } from 'mongoose'
import { NextApiRequest, NextApiResponse } from 'next'
import { Booking } from '../../../db/booking'
import { Booking, Prisma } from '@prisma/client';
import { createBooking } from '../../../db/index'
import { log } from '../../../helpers/log'
import {
@@ -14,17 +13,14 @@ export default async function userHandler(
): Promise<void> {
const { method } = req
let booking: Booking
let booking: Booking;
switch (method) {
case 'POST':
try {
booking = await createBooking(req.body)
} catch (e) {
if (e instanceof Error.ValidationError) {
res.status(400).json({ message: e.message, errors: e.errors })
return
}
// TODO: add validation for booking on same day
log.error('Failed to store booking', e)
res.status(500).end(`Internal Server Error...Guru is meditating...`)
return

View File

@@ -1,8 +1,7 @@
import React, { useEffect, useState } from 'react'
import { getServerSideBooking } from '../../../lib/getServerSideProps'
import { Booking } from '../../../db/booking'
import { BOOKING_STATUS } from '../../../db/enums'
import { daysFormatFrontend } from '../../../helpers/date'
import { Booking, BookingStatus } from '@prisma/client'
import { dateFormatFrontend } from '../../../helpers/date'
import { log } from '../../../helpers/log'
import { getBookingStatus, cancelBooking } from '../../../helpers/booking'
@@ -45,12 +44,12 @@ export default function ShowBooking({
<strong>Buchungsstatus:</strong> {getBookingStatus(booking.status)}
</div>
<div>
<strong>Buchungszeitraum:</strong> {daysFormatFrontend(booking.days)}
<strong>Buchungszeitraum:</strong> {dateFormatFrontend(new Date(booking.startDate))}-{dateFormatFrontend(new Date(booking.endDate))}
</div>
{storingBookingError && (
<div className="error-message flex-grow">{storingBookingError}</div>
)}
{[BOOKING_STATUS.CONFIRMED, BOOKING_STATUS.REQUESTED].includes(
{([BookingStatus.CONFIRMED, BookingStatus.REQUESTED] as BookingStatus[]).includes(
booking.status
) && (
<div className="my-6">

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'
import Link from 'next/link'
import { Booking } from '../../../db/booking'
import { Booking } from '@prisma/client'
import { loadBookingData, storeBookingData } from '../../../helpers/storage'
import { getServerSideBooking } from '../../../lib/getServerSideProps'

View File

@@ -0,0 +1,148 @@
-- CreateEnum
CREATE TYPE "BillStatus" AS ENUM ('UNINVOICED', 'INVOICED', 'PAID');
-- CreateEnum
CREATE TYPE "BookingStatus" AS ENUM ('REQUESTED', 'CONFIRMED', 'REJECTED', 'CANCELED');
-- CreateTable
CREATE TABLE "AdditionalCosts" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"value" DECIMAL(65,30) NOT NULL,
CONSTRAINT "AdditionalCosts_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Bill" (
"id" SERIAL NOT NULL,
"createdAt" DATE NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATE NOT NULL,
"milageEnd" INTEGER NOT NULL,
"milageStart" INTEGER NOT NULL,
"status" "BillStatus" NOT NULL DEFAULT 'UNINVOICED',
"tarif" DECIMAL(65,30) NOT NULL,
CONSTRAINT "Bill_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Booking" (
"id" TEXT NOT NULL,
"uuid" TEXT NOT NULL,
"createdAt" DATE NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATE NOT NULL,
"billId" INTEGER NOT NULL,
"calendarEventId" TEXT,
"city" TEXT NOT NULL,
"destination" TEXT NOT NULL,
"email" TEXT NOT NULL,
"endDate" TEXT NOT NULL,
"name" TEXT NOT NULL,
"org" TEXT NOT NULL,
"phone" TEXT NOT NULL,
"purpose" TEXT NOT NULL,
"startDate" TEXT NOT NULL,
"status" "BookingStatus" NOT NULL DEFAULT 'REQUESTED',
"street" TEXT NOT NULL,
"zip" TEXT NOT NULL,
CONSTRAINT "Booking_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Account" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Session" (
"id" TEXT NOT NULL,
"sessionToken" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"name" TEXT,
"email" TEXT,
"emailVerified" TIMESTAMP(3),
"image" TEXT,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "VerificationToken" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL
);
-- CreateTable
CREATE TABLE "_AdditionalCostsToBill" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "Booking_uuid_key" ON "Booking"("uuid");
-- CreateIndex
CREATE UNIQUE INDEX "Booking_billId_key" ON "Booking"("billId");
-- CreateIndex
CREATE INDEX "uuid_1" ON "Booking"("uuid");
-- CreateIndex
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
-- CreateIndex
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");
-- CreateIndex
CREATE UNIQUE INDEX "_AdditionalCostsToBill_AB_unique" ON "_AdditionalCostsToBill"("A", "B");
-- CreateIndex
CREATE INDEX "_AdditionalCostsToBill_B_index" ON "_AdditionalCostsToBill"("B");
-- AddForeignKey
ALTER TABLE "Booking" ADD CONSTRAINT "Booking_billId_fkey" FOREIGN KEY ("billId") REFERENCES "Bill"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_AdditionalCostsToBill" ADD CONSTRAINT "_AdditionalCostsToBill_A_fkey" FOREIGN KEY ("A") REFERENCES "AdditionalCosts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_AdditionalCostsToBill" ADD CONSTRAINT "_AdditionalCostsToBill_B_fkey" FOREIGN KEY ("B") REFERENCES "Bill"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

110
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,110 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgres"
url = env("POSTGRES_URI")
}
model AdditionalCosts {
id Int @id @default(autoincrement())
name String
value Decimal
Bill Bill[]
}
enum BillStatus {
UNINVOICED
INVOICED
PAID
}
model Bill {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @db.Date
updatedAt DateTime @updatedAt @db.Date
milageEnd Int
milageStart Int
status BillStatus @default(UNINVOICED)
tarif Decimal
additionalCosts AdditionalCosts[]
booking Booking?
}
enum BookingStatus {
REQUESTED
CONFIRMED
REJECTED
CANCELED
}
model Booking {
id String @id @default(uuid())
uuid String @unique @default(uuid())
createdAt DateTime @default(now()) @db.Date
updatedAt DateTime @updatedAt @db.Date
bill Bill? @relation(fields: [billId], references: [id], onDelete: Cascade)
billId Int @unique
calendarEventId String?
city String
destination String
email String
endDate String
name String
org String
phone String
purpose String
startDate String
status BookingStatus @default(REQUESTED)
street String
zip String
@@index([uuid], map: "uuid_1")
}
// next-auth
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}