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

View File

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

View File

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

View File

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

View File

@@ -1,79 +1,27 @@
import { MILAGE_TARIFS } from '../db/enums' import { Bill, Prisma } from '@prisma/client'
import { AdditionalCost, Bill } from '../db/bill'
import fetch from './fetch' 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({ export function getBillTotal({
tarif, tarif,
milage, milage,
additionalCosts, additionalCosts,
}: { }: {
tarif: MILAGE_TARIFS tarif: Prisma.Decimal
milage?: number milage?: number
additionalCosts: AdditionalCost[] additionalCosts: Prisma.AdditionalCostsCreateInput[]
}): number { }): Prisma.Decimal {
const milageCosts = getMilageCosts({ tarif, km: milage }) const milageCosts = tarif.mul(milage)
const additionalCostsSum = additionalCosts const additionalCostsSum = additionalCosts
.map(({ value }) => value) .reduce((acc, {value} ) => (value as Prisma.Decimal).plus(acc), new Prisma.Decimal(0))
.reduce((acc: number, value: number) => acc + value, 0)
return roundToCent(milageCosts + additionalCostsSum)
return additionalCostsSum.add(milageCosts).toDecimalPlaces(2);
} }
export async function createBill( export async function createBill(
bookingUuid: string, bookingUuid: string,
bill: Bill bill: Prisma.BillCreateInput
): Promise<Bill> { ): Promise<Bill> {
return fetch(`/api/bookings/${bookingUuid}/bill`, { return fetch(`/api/bookings/${bookingUuid}/bill`, {
method: 'POST', method: 'POST',
@@ -83,7 +31,7 @@ export async function createBill(
export async function patchBill( export async function patchBill(
bookingUuid: string, bookingUuid: string,
bill: Bill bill: Prisma.BillUpdateInput
): Promise<Bill> { ): Promise<Bill> {
return fetch(`/api/bookings/${bookingUuid}/bill`, { return fetch(`/api/bookings/${bookingUuid}/bill`, {
method: 'POST', method: 'POST',

View File

@@ -1,23 +1,22 @@
import { BookFormData } from '../context/book' import { Prisma, BookingStatus } from '@prisma/client'
import { BOOKING_STATUS } from '../db/enums'
import fetch from './fetch' import fetch from './fetch'
export function getBookingStatus(status: BOOKING_STATUS) { export function getBookingStatus(status: BookingStatus) {
switch (status) { switch (status) {
case BOOKING_STATUS.REQUESTED: case BookingStatus.REQUESTED:
return 'Angefragt' return 'Angefragt'
case BOOKING_STATUS.CONFIRMED: case BookingStatus.CONFIRMED:
return 'Bestätigt' return 'Bestätigt'
case BOOKING_STATUS.REJECTED: case BookingStatus.REJECTED:
return 'Abgewiesen' return 'Abgewiesen'
case BOOKING_STATUS.CANCELED: case BookingStatus.CANCELED:
return 'Storniert' return 'Storniert'
default: default:
return 'Unbekannt - bitte kontaktieren Sie uns!' return 'Unbekannt - bitte kontaktieren Sie uns!'
} }
} }
export async function createBooking(formData: BookFormData) { export async function createBooking(formData: Prisma.BookingCreateInput) {
return fetch('/api/bookings', { return fetch('/api/bookings', {
method: 'POST', method: 'POST',
body: formData, body: formData,
@@ -27,13 +26,13 @@ export async function createBooking(formData: BookFormData) {
export async function cancelBooking(uuid: string) { export async function cancelBooking(uuid: string) {
return fetch(`/api/bookings/${uuid}`, { return fetch(`/api/bookings/${uuid}`, {
method: 'PATCH', method: 'PATCH',
body: { status: BOOKING_STATUS.CANCELED }, body: { status: BookingStatus.CANCELED },
}) })
} }
export async function patchBooking( export async function patchBooking(
uuid: string, uuid: string,
bookingData: Partial<BookFormData> bookingData: Prisma.BookingUpdateInput
) { ) {
return fetch(`/api/bookings/${uuid}`, { return fetch(`/api/bookings/${uuid}`, {
method: 'PATCH', 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' import { utcToZonedTime } from 'date-fns-tz'
const FRONTEND_FORMAT = 'dd.MM.yyyy' const FRONTEND_FORMAT = 'dd.MM.yyyy'
@@ -23,18 +23,18 @@ export function getDays({
endDate, endDate,
endDateExclusive = false, endDateExclusive = false,
}: { }: {
startDate: Date startDate: string,
endDate: Date endDate: string,
endDateExclusive?: boolean endDateExclusive?: boolean
}): string[] { }): string[] {
let currentDay = new Date(startDate.getTime()) let currentDay = new Date(startDate);
const days = [dateFormatBackend(currentDay)] const days = [dateFormatBackend(currentDay)]
if (!endDate) { if (!endDate) {
return days return days
} }
const inclusiveEndDate = endDateExclusive ? subDays(endDate, 1) : endDate const inclusiveEndDate = endDateExclusive ? subDays(new Date(endDate), 1) : new Date(endDate)
while (currentDay < inclusiveEndDate) { while (currentDay < inclusiveEndDate) {
currentDay = addDays(currentDay, 1) currentDay = addDays(currentDay, 1)
@@ -84,3 +84,8 @@ export function nowInTz(timezone = 'Europe/Berlin'): Date {
export function getNextDay(date: Date) { export function getNextDay(date: Date) {
return addDays(date, 1) 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 { createEvents, createEvent, EventStatus } from 'ics'
import { Booking } from '../db/booking' import { Booking, BookingStatus} from '@prisma/client';
import { BOOKING_STATUS } from '../db/enums'
import { getBaseURL } from './url' import { getBaseURL } from './url'
import { daysFormatFrontend } from './date' import { dateFormatFrontend, getDayCount } from './date'
function convertDay(value: string): [number, number, number] { function convertDay(value: string): [number, number, number] {
const parts = value.split('-') const parts = value.split('-')
@@ -16,9 +15,9 @@ export function generateCalendarEntry(booking: Booking): string {
const { error, value } = createEvent({ const { error, value } = createEvent({
productId: 'app.vercel.pfadi-bussle/ics', productId: 'app.vercel.pfadi-bussle/ics',
title: `Pfadi-Bussle Buchung`, title: `Pfadi-Bussle Buchung`,
start: convertDay(booking.days[0]), start: convertDay(booking.startDate),
startOutputType: 'local', startOutputType: 'local',
duration: { days: booking.days.length }, duration: { days: getDayCount(booking) },
location: 'Mömpelgardgasse 25, 72348 Rosenfeld, Deutschland', location: 'Mömpelgardgasse 25, 72348 Rosenfeld, Deutschland',
geo: { lat: 48.287044, lon: 8.726361 }, geo: { lat: 48.287044, lon: 8.726361 },
description: `Gebucht auf ${booking.name} description: `Gebucht auf ${booking.name}
@@ -26,7 +25,7 @@ export function generateCalendarEntry(booking: Booking): string {
Buchungs-Link: ${getBaseURL()}/bookings/${booking.uuid} Buchungs-Link: ${getBaseURL()}/bookings/${booking.uuid}
`, `,
status: status:
booking.status === BOOKING_STATUS.CONFIRMED booking.status === BookingStatus.CONFIRMED
? ('CONFIRMED' as EventStatus) ? ('CONFIRMED' as EventStatus)
: ('TENTATIVE' as EventStatus), : ('TENTATIVE' as EventStatus),
}) })
@@ -54,12 +53,12 @@ export function generateBookedCalendar(bookings: Booking[]): string {
} => ({ } => ({
productId: 'app.vercel.pfadi-bussle/ics', productId: 'app.vercel.pfadi-bussle/ics',
calName: 'Pfadi-Bussle Buchungen', calName: 'Pfadi-Bussle Buchungen',
start: convertDay(booking.days[0]), start: convertDay(booking.startDate),
startOutputType: 'local', startOutputType: 'local',
duration: { days: booking.days.length }, duration: { days: getDayCount(booking) },
title: `Buchung ${booking.name}`, title: `Buchung ${booking.name}`,
description: `Name: ${booking.name} description: `Name: ${booking.name}
Zeitraum: ${daysFormatFrontend(booking.days)} Zeitraum: ${dateFormatFrontend(new Date(booking.startDate))}-${dateFormatFrontend(new Date(booking.endDate))}
Email: ${booking.email} Email: ${booking.email}
Telefon: ${booking.phone} Telefon: ${booking.phone}
@@ -67,7 +66,7 @@ Telefon: ${booking.phone}
Link: ${getBaseURL()}/admin/bookings/${booking.uuid} Link: ${getBaseURL()}/admin/bookings/${booking.uuid}
`, `,
status: status:
booking.status === BOOKING_STATUS.CONFIRMED booking.status === BookingStatus.CONFIRMED
? ('CONFIRMED' as EventStatus) ? ('CONFIRMED' as EventStatus)
: ('TENTATIVE' 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 { getBaseURL } from '../helpers/url'
import { log } from '../helpers/log' import { log } from '../helpers/log'
import { daysFormatFrontend } from './date' import { dateFormatFrontend } from './date'
import { generateCalendarEntry } from './ical' import { generateCalendarEntry } from './ical'
import sgMail from '@sendgrid/mail' import sgMail from '@sendgrid/mail'
@@ -34,10 +34,11 @@ Tel. 0151/212 253 62
${getBaseURL()} ${getBaseURL()}
` `
function getReceivedBookingBookerText(booking: Booking): string { function getReceivedBookingBookerText(booking: Prisma.BookingCreateInput): string {
return `Hallo liebe/r ${booking.name}, 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! Nach Prüfung bestätigen wir die Buchung bald per E-Mail!
@@ -54,9 +55,9 @@ ${footer}
function getBookingConfirmedText(booking: Booking): string { function getBookingConfirmedText(booking: Booking): string {
return `Hallo liebe/r ${booking.name}, return `Hallo liebe/r ${booking.name},
deine Buchunganfrage zum ${daysFormatFrontend( deine Buchunganfrage vom
booking.days ${dateFormatFrontend(new Date(booking.startDate))} bis ${dateFormatFrontend(new Date(booking.endDate))}
)} bestätigen wir gerne! bestätigen wir gerne!
Bitte melde dich spätestens 7 Tage vor dem Buchungstermin per E-Mail oder Telefon Bitte melde dich spätestens 7 Tage vor dem Buchungstermin per E-Mail oder Telefon
um eine Schlüsselübergabe zu vereinbaren. um eine Schlüsselübergabe zu vereinbaren.
@@ -71,9 +72,9 @@ ${footer}
function getBookingRejectedText(booking: Booking): string { function getBookingRejectedText(booking: Booking): string {
return `Hallo liebe/r ${booking.name}, return `Hallo liebe/r ${booking.name},
es tut uns leid, aber deine Buchungsanfrage zum ${daysFormatFrontend( es tut uns leid, aber deine Buchungsanfrage vom
booking.days ${dateFormatFrontend(new Date(booking.startDate))} bis ${dateFormatFrontend(new Date(booking.endDate))}
)} konnten wir leider nicht bestätigen. konnten wir leider nicht bestätigen.
Willst du das Bussle an einem anderen Termin buchen? Dann stelle bitte nochmal Willst du das Bussle an einem anderen Termin buchen? Dann stelle bitte nochmal
eine Buchungsanfrage auf ${getBaseURL()}. eine Buchungsanfrage auf ${getBaseURL()}.
@@ -85,7 +86,9 @@ ${footer}
function getBookingCanceledText(booking: Booking): string { function getBookingCanceledText(booking: Booking): string {
return `Hallo liebe/r ${booking.name}, 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 Willst du das Bussle an einem anderen Termin buchen? Dann stelle bitte nochmal
eine Buchungsanfrage auf ${getBaseURL()}. eine Buchungsanfrage auf ${getBaseURL()}.
@@ -109,7 +112,7 @@ export async function sendReceivedBookingAdminMail(
await sendMail({ await sendMail({
to: [{ email: ADMIN_EMAIL }], to: [{ email: ADMIN_EMAIL }],
from: { email: FROM_EMAIL, name: 'Pfadi-Bussle Wart' }, 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), textPlainContent: getReceivedBookingAdminText(booking),
}) })
} catch (error) { } catch (error) {
@@ -118,7 +121,7 @@ export async function sendReceivedBookingAdminMail(
} }
export async function sendReceivedBookingBookerMail( export async function sendReceivedBookingBookerMail(
booking: Booking booking: Prisma.BookingCreateInput
): Promise<void> { ): Promise<void> {
try { try {
await sendMail({ await sendMail({

View File

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

View File

@@ -1,39 +1,36 @@
import { Booking } from '@prisma/client';
import { startOfYear } from 'date-fns' import { startOfYear } from 'date-fns'
import { nowInTz } from '../helpers/date' import { nowInTz } from '../helpers/date'
import { getBookingByUUID, getBookings } from '../db/index' import { getBookingByUUID, getBookings } from '../db/index'
export type ServerSideBooking = { export type ServerSideBooking = {
props: { props: {
booking: object booking: Booking
} }
} }
export type ServerSideRecentBooking = { export type ServerSideRecentBooking = {
props: { props: {
bookings: object[] bookings: Booking[]
} }
} }
export const getServerSideRecentBookings = export const getServerSideRecentBookings =
async (): Promise<ServerSideRecentBooking> => { async () => {
const bookings = await getBookings({ const bookings = await getBookings({
startDateGreaterThan: startOfYear(nowInTz()).toISOString(), 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 { return {
props: { props: {
bookings: bookingsJSON, bookings: bookings,
}, },
} }
} }
export const getServerSideBooking = async ( export const getServerSideBooking = async (
context: any context: any
): Promise<ServerSideBooking> => { ) => {
const { const {
res, res,
params: { uuid: uuids }, params: { uuid: uuids },
@@ -47,11 +44,7 @@ export const getServerSideBooking = async (
return { props: { booking: null } } 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 { return {
props: { booking: bookingJSON }, props: { booking },
} }
} }

View File

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

377
package-lock.json generated
View File

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

View File

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

View File

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

View File

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

View File

@@ -1,64 +1,51 @@
import { NextApiRequest, NextApiResponse } from 'next'
import NextAuth from 'next-auth' import NextAuth from 'next-auth'
import EmailProvider from 'next-auth/providers/email' import EmailProvider from 'next-auth/providers/email'
import GitHubProvider from 'next-auth/providers/github' import GitHubProvider from 'next-auth/providers/github'
import { MongoDBAdapter } from '@next-auth/mongodb-adapter' import { PrismaAdapter } from "@next-auth/prisma-adapter"
import { MONGO_URI } from '../../../db' import { PrismaClient } from "@prisma/client"
import { MongoClient } from 'mongodb'
let client: MongoClient const prisma = new PrismaClient()
const ADMIN_EMAIL = process.env.ADMIN_EMAIL const ADMIN_EMAIL = process.env.ADMIN_EMAIL
const GITHUB_USERS_GRANTED = ['111471'] const GITHUB_USERS_GRANTED = ['111471']
async function getMongoClient() { export default NextAuth({
if (!client) { secret: process.env.NEXTAUTH_SECRET,
client = new MongoClient(MONGO_URI) adapter: PrismaAdapter(prisma),
await client.connect() providers: [
} GitHubProvider({
clientId: process.env.GITHUB_CLIENT_ID,
return client clientSecret: process.env.GITHUB_CLIENT_SECRET,
} }),
EmailProvider({
export default async function auth(req: NextApiRequest, res: NextApiResponse) { server: {
return await NextAuth(req, res, { host: 'smtp.sendgrid.net',
secret: process.env.NEXTAUTH_SECRET, port: 587,
adapter: MongoDBAdapter(getMongoClient()), auth: {
providers: [ user: 'apikey',
GitHubProvider({ pass: process.env.SENDGRID_API_KEY,
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 { NextApiRequest, NextApiResponse } from 'next'
import { Bill } from '../../../../db/bill' import { Prisma } from '@prisma/client';
import { createBill, patchBill } from '../../../../db/index' import { createBill, patchBill } from '../../../../db/index'
import { log } from '../../../../helpers/log' import { log } from '../../../../helpers/log'
@@ -13,7 +13,7 @@ export default async function billHandler(
} = req } = req
const bookingUUID = Array.isArray(uuids) ? uuids[0] : uuids const bookingUUID = Array.isArray(uuids) ? uuids[0] : uuids
let bill: Bill let bill: Prisma.BillUpdateInput
switch (method) { switch (method) {
case 'POST': case 'POST':

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import Link from 'next/link' import Link from 'next/link'
import { Booking } from '../../../db/booking' import { Booking } from '@prisma/client'
import { loadBookingData, storeBookingData } from '../../../helpers/storage' import { loadBookingData, storeBookingData } from '../../../helpers/storage'
import { getServerSideBooking } from '../../../lib/getServerSideProps' 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])
}