remove all bill related stuff

This commit is contained in:
Thomas Ruoff
2025-03-26 23:14:35 +01:00
parent 56721a12c7
commit d5ac2f09dc
9 changed files with 9 additions and 577 deletions

View File

@@ -1,94 +0,0 @@
import * as mongoose from 'mongoose'
import { BILL_STATUS, MILAGE_TARIFS } from './enums'
import { getBillTotal } from '../helpers/bill'
export interface IAdditionalCost {
name: string
value: number
}
export interface IBill {
milageStart: number
milageEnd: number
milage?: number
tarif: MILAGE_TARIFS
status: BILL_STATUS
additionalCosts: IAdditionalCost[]
createdAt?: string
updatedAt?: string
}
export type BillDocument = IBill &
mongoose.SchemaTimestampsConfig &
mongoose.Document
export type BillModel = mongoose.Model<IBill>
const BillSchema = new mongoose.Schema<IBill>(
{
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

@@ -8,18 +8,16 @@ import {
} from '../helpers/date'
import { createCalendarEvent, deleteCalendarEvent } from '../lib/googlecalendar'
import { IBill } from './bill'
import { BOOKING_STATUS, VALIDATION_ERRORS } from './enums'
export interface IBooking {
uuid: string
name: string
email: string
phone: string
phone?: string
street: string
zip: string
city: string
bill?: IBill
// format YYYY-MM-DD
startDate: string
// format YYYY-MM-DD
@@ -29,10 +27,7 @@ export interface IBooking {
org?: string
destination?: string
days?: string[]
calendarEventId: string
createdAt?: string
updatedAt?: string
calendarEventId?: string
toJSON?: () => IBooking
}
@@ -54,11 +49,6 @@ const BookingSchema = new mongoose.Schema(
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,

View File

@@ -5,18 +5,6 @@ export enum BOOKING_STATUS {
CANCELED = 'canceled',
}
export enum BILL_STATUS {
UNINVOICED = 'uninvoiced',
INVOICED = 'invoiced',
PAID = 'paid',
}
export enum MILAGE_TARIFS {
INTERN = 'intern',
EXTERN = 'extern',
NOCHARGE = 'nocharge',
}
export enum VALIDATION_ERRORS {
AT_LEAST_ONE_DAY_BOOKED = 'atLeastOneDayBooked',
}

View File

@@ -1,13 +1,12 @@
import * as mongoose from 'mongoose'
import BookingModel, { IBooking } from './booking'
import BillModel, { IBill } from './bill'
import { getBookedDays as calendarGetBookedDays } from '../lib/googlecalendar'
import { BOOKING_STATUS } from './enums'
import { uniqueFilter } from '../helpers/array'
export const MONGO_URI = process.env.MONGO_URI
mongoose.set('strictQuery', false);
mongoose.set('strictQuery', false)
mongoose.connect(MONGO_URI, {
serverSelectionTimeoutMS: 3000,
@@ -23,7 +22,9 @@ export async function getBookedDays(
return [...bookedInDatabase, ...bookedInCalendar].filter(uniqueFilter).sort()
}
export async function getBookingByUUID(uuid: string): Promise<mongoose.HydratedDocument<IBooking>> {
export async function getBookingByUUID(
uuid: string
): Promise<mongoose.HydratedDocument<IBooking>> {
return BookingModel.findOne({ uuid })
}
@@ -53,7 +54,9 @@ export async function createBooking({
street,
zip,
city,
}: IBooking): Promise<mongoose.FlattenMaps<IBooking & { _id: mongoose.Types.ObjectId }>> {
}: IBooking): Promise<
mongoose.FlattenMaps<IBooking & { _id: mongoose.Types.ObjectId }>
> {
const booking = new BookingModel({
startDate,
endDate,
@@ -83,49 +86,3 @@ export async function patchBooking(
return { current: booking.toJSON(), previous: oldBooking }
}
export async function createBill(
bookingUUID: string,
billData: IBill
): Promise<IBill> {
const booking = await getBookingByUUID(bookingUUID)
const bill = new BillModel()
bill.set(billData)
await bill.save()
booking.bill = bill
await booking.save()
return bill.toJSON()
}
export async function patchBill(
bookingUUID: string,
billData: IBill
): Promise<IBill> {
const booking = await getBookingByUUID(bookingUUID)
const bill =
(booking.bill && (await BillModel.findById(booking.bill))) ||
(await BillModel.create({}))
bill.set(billData)
await bill.save()
if (booking.bill !== bill) {
booking.bill = bill
await booking.save()
}
return bill.toJSON()
}
export async function getMilageMax(): Promise<number> {
const billMaxMilageEnd = await BillModel.findOne({})
.sort('-milageEnd')
.select('milageEnd')
.exec()
return billMaxMilageEnd?.milageEnd || 0
}

View File

@@ -1,92 +0,0 @@
import { MILAGE_TARIFS } from '../db/enums'
import { IAdditionalCost, IBill } from '../db/bill'
import fetch from './fetch'
function roundToCent(amount: number): number {
return Math.round(amount * 100) / 100
}
export function getMilageCosts({
tarif,
km,
}: {
tarif: MILAGE_TARIFS
km: number
}): number {
if (tarif === MILAGE_TARIFS.NOCHARGE) {
return 0
}
if (km <= 0) {
return 0
}
let rate: number
if (tarif === MILAGE_TARIFS.EXTERN) {
if (km <= 200) {
rate = 0.42
} else if (km <= 1000) {
rate = 0.25
} else if (km <= 2000) {
rate = 0.2
} else {
rate = 0.18
}
}
if (tarif === MILAGE_TARIFS.INTERN) {
if (km <= 200) {
rate = 0.37
} else if (km <= 1000) {
rate = 0.22
} else if (km <= 2000) {
rate = 0.15
} else {
rate = 0.13
}
}
if (rate === undefined) {
throw Error('Unable to determine rate')
}
return roundToCent(km * rate)
}
export function getBillTotal({
tarif,
milage,
additionalCosts,
}: {
tarif: MILAGE_TARIFS
milage?: number
additionalCosts: IAdditionalCost[]
}): number {
const milageCosts = getMilageCosts({ tarif, km: milage })
const additionalCostsSum = additionalCosts
.map(({ value }) => value)
.reduce((acc: number, value: number) => acc + value, 0)
return roundToCent(milageCosts + additionalCostsSum)
}
export async function createBill(
bookingUuid: string,
bill: IBill
): Promise<IBill> {
return fetch(`/api/bookings/${bookingUuid}/bill`, {
method: 'POST',
body: bill,
})
}
export async function patchBill(
bookingUuid: string,
bill: IBill
): Promise<IBill> {
return fetch(`/api/bookings/${bookingUuid}/bill`, {
method: 'POST',
body: bill,
})
}

View File

@@ -47,8 +47,6 @@ export const getServerSideBooking = async (
return { props: { booking: null } }
}
await booking.populate('bill')
// TODO: hack, not sure why _id is not serilizable
const bookingJSON = JSON.parse(JSON.stringify(booking.toJSON())) as object
return {

View File

@@ -1,267 +0,0 @@
import React, { useEffect, useState } from 'react'
import Input from '../../../../components/input'
import Select from '../../../../components/select'
import { IBooking } from '../../../../db/booking'
import { BILL_STATUS, MILAGE_TARIFS } from '../../../../db/enums'
import { getMilageMax } from '../../../../db/index'
import { daysFormatFrontend } from '../../../../helpers/date'
import { log } from '../../../../helpers/log'
import { getBillTotal, createBill, patchBill } from '../../../../helpers/bill'
import { getBookingStatus } from '../../../../helpers/booking'
import { getServerSideBooking } from '../../../../lib/getServerSideProps'
import withAuth from '../../../../helpers/withAuth'
export const getServerSideProps = async (context) => {
const milageMax = await getMilageMax()
const serverSideBookingProps = await getServerSideBooking(context)
return {
props: {
...serverSideBookingProps.props,
milageMax,
},
}
}
const milageTarifOptions = Object.values(MILAGE_TARIFS).map((tarif) => {
return (
<option value={tarif} key={tarif}>
{getTarifLabel(tarif)}
</option>
)
})
const billStatusOptions = Object.values(BILL_STATUS).map((status) => {
return (
<option value={status} key={status}>
{getBillStatusLabel(status)}
</option>
)
})
function getTarifLabel(tarif: MILAGE_TARIFS) {
switch (tarif) {
case MILAGE_TARIFS.EXTERN:
return 'Extern'
case MILAGE_TARIFS.INTERN:
return 'Intern'
case MILAGE_TARIFS.NOCHARGE:
return 'Frei von Kosten'
default:
return 'Keine'
}
}
function getBillStatusLabel(status: BILL_STATUS) {
switch (status) {
case BILL_STATUS.UNINVOICED:
return 'Nicht gestellt'
case BILL_STATUS.INVOICED:
return 'Gestellt'
case BILL_STATUS.PAID:
return 'Bezahlt'
default:
return 'Unbekannt!!!'
}
}
function BookingBillPage({
booking: bookingProp,
milageMax,
}: {
booking: IBooking
milageMax: number
}) {
const [booking, setBooking] = useState(bookingProp)
const [milageStart, setMilageStart] = useState(
booking?.bill?.milageStart || milageMax
)
const [milageEnd, setMilageEnd] = useState(booking?.bill?.milageEnd)
const [tarif, setTarif] = useState(
booking?.bill?.tarif || MILAGE_TARIFS.EXTERN
)
const [status, setStatus] = useState(booking?.bill?.status)
const [additionalCosts, setAdditionalCosts] = useState([])
const [storingInProgress, setStoringInProgress] = useState(false)
const [storingError, setStoringError] = useState(null)
const milage =
(0 < milageStart && milageStart < milageEnd && milageEnd - milageStart) || 0
const total = getBillTotal({ tarif, milage, additionalCosts })
// in case the props change, update the internal state
useEffect(() => setBooking(bookingProp), [bookingProp])
const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
setStoringInProgress(true)
setStoringError(null)
try {
const saveBill = !!booking.bill ? createBill : patchBill
const bill = await saveBill(booking.uuid, {
milageStart,
milageEnd,
milage,
tarif,
status,
additionalCosts,
})
booking.bill = bill
setBooking(booking)
} catch (error) {
setStoringError('Buchung konnte nicht gespeichert werden!')
log.error('Failed to store booking', error)
}
setStoringInProgress(false)
}
const onAddAdditionalCost = function (
event: React.MouseEvent<HTMLButtonElement>
) {
event.preventDefault()
setAdditionalCosts([...additionalCosts, { name: '', value: 0 }])
}
const onRemoveAdditionalCost = function (
event: React.MouseEvent<HTMLButtonElement>,
index: number
) {
event.preventDefault()
setAdditionalCosts([
...additionalCosts.slice(0, index),
...additionalCosts.slice(index + 1),
])
}
return (
<>
{booking && (
<form className="w-full" onSubmit={onSubmit}>
<div>
<strong>Buchungszeitraum:</strong>{' '}
{daysFormatFrontend(booking.days)}
</div>
<div>
<strong>Bucher:</strong> {booking.name}
</div>
<div>
<strong>Buchungsstatus:</strong> {getBookingStatus(booking.status)}
</div>
<div>
<Input
label="Anfangskilometer"
name="milageStart"
required
value={milageStart}
type="number"
onChange={(e: React.ChangeEvent<React.ElementRef<'input'>>) =>
setMilageStart(Number(e.target.value))
}
/>
<Input
label="Endkilometer"
name="milageEnd"
required
value={milageEnd}
type="number"
onChange={(e: React.ChangeEvent<React.ElementRef<'input'>>) =>
setMilageEnd(Number(e.target.value))
}
/>
<Input label="Gefahren" name="milage" readOnly value={milage} />
<Select
label="Rate"
name="tarif"
value={tarif}
onChange={(e) => setTarif(e.target.value as MILAGE_TARIFS)}
>
{milageTarifOptions}
</Select>
<div className="mb-3">
<button
className="ibtn btn-gray mr-3"
onClick={onAddAdditionalCost}
title="Zusätzliche Kosten hinzufügen"
>
+
</button>
<label className="flabel inline">Zusätzliche Kosten</label>
</div>
{additionalCosts.map((_, index) => {
return (
<>
<div className="mb-3" key={`label${index}`}>
<button
className="ibtn btn-gray mr-3"
onClick={(event) => onRemoveAdditionalCost(event, index)}
title="Entfernen"
>
-
</button>
<label className="flabel inline">{`Kostenpunkt ${
index + 1
}`}</label>
</div>
<div className="ml-10 mb-3" key={`input{index}`}>
<Input
label={`Name`}
name={`additionalCostName${index}`}
key={`additionalCostName${index}`}
value={additionalCosts[index].name}
onChange={(event) => {
const newAdditonalCosts = [...additionalCosts]
newAdditonalCosts[index] = {
value: newAdditonalCosts[index].value,
name: event.target.value,
}
setAdditionalCosts(newAdditonalCosts)
}}
/>
<Input
label={`Betrag`}
name={`additionalCostValue${index}`}
key={`additionalCostValue${index}`}
value={additionalCosts[index].value}
type="number"
onChange={(event) => {
const newAdditonalCosts = [...additionalCosts]
newAdditonalCosts[index] = {
name: newAdditonalCosts[index].name,
value: Number(event.target.value),
}
setAdditionalCosts(newAdditonalCosts)
}}
/>
</div>
</>
)
})}
<Input label="Summe" name="total" readOnly value={total} />
</div>
<Select
label="Status"
name={status}
value={status}
onChange={(e) => setStatus(e.target.value as BILL_STATUS)}
>
{billStatusOptions}
</Select>
{storingError && (
<div className="error-message flex-grow mt-6">{storingError}</div>
)}
<button
type="submit"
className="btn btn-blue mt-3"
disabled={storingInProgress}
>
Rechnung {!!booking.bill ? 'Updaten' : 'Erstellen'}
</button>
</form>
)}
</>
)
}
BookingBillPage.authenticationRequired = true
export default withAuth(BookingBillPage)

View File

@@ -1,6 +1,5 @@
import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import Link from 'next/link'
import Calendar from '../../../../components/calendar'
import { getServerSideBooking } from '../../../../lib/getServerSideProps'
import { IBooking } from '../../../../db/booking'
@@ -13,7 +12,6 @@ import withAuth from '../../../../helpers/withAuth'
export const getServerSideProps = getServerSideBooking
function ShowBookingAdmin({ booking: bookingProp }: { booking: IBooking }) {
const router = useRouter()
const [booking, setBooking] = useState(bookingProp)
const [storingBooking, setStoringBooking] = useState(false)
const [storingBookingError, setStoringBookingError] = useState(null)
@@ -64,9 +62,6 @@ function ShowBookingAdmin({ booking: bookingProp }: { booking: IBooking }) {
>
Buchung Abweisen
</button>
<Link href={`${router.asPath}/bill`} className="btn btn-gray">
Rechnung
</Link>
</div>
</>
)

View File

@@ -1,43 +0,0 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { IBill } from '../../../../db/bill'
import { createBill, patchBill } from '../../../../db/index'
import { log } from '../../../../helpers/log'
export default async function billHandler(
req: NextApiRequest,
res: NextApiResponse
): Promise<void> {
const {
method,
query: { uuid: uuids },
} = req
const bookingUUID = Array.isArray(uuids) ? uuids[0] : uuids
let bill: IBill
switch (method) {
case 'POST':
try {
bill = await createBill(bookingUUID, req.body)
res.status(200).json(bill)
} catch (e) {
log.error('Failed to store bill', e)
res.status(500).end(`Internal Server Error...Guru is meditating...`)
return
}
break
case 'PATCH':
try {
bill = await patchBill(bookingUUID, req.body)
res.status(200).json(bill)
} catch (e) {
log.error('Failed to patch bill', e)
res.status(500).end(`Internal Server Error...Guru is meditating...`)
return
}
break
default:
res.setHeader('Allow', ['POST', 'PATCH'])
res.status(405).end(`Method ${method} Not Allowed`)
}
}