mirror of
https://github.com/tomru/pfadi-bussle.git
synced 2026-03-03 06:27:11 +01:00
313 lines
9.7 KiB
TypeScript
313 lines
9.7 KiB
TypeScript
import React, { useEffect, useState } from 'react'
|
|
import Footer from '../../../../components/footer'
|
|
import Header from '../../../../components/header'
|
|
import Input from '../../../../components/input'
|
|
import Select from '../../../../components/select'
|
|
import { AdditionalCost, Bill } from '../../../../db/bill'
|
|
import { Booking } from '../../../../db/booking'
|
|
import { BILL_STATUS, MILAGE_TARIFS } from '../../../../db/enums'
|
|
import { getMilageMax } from '../../../../db/index'
|
|
import { dateFormatFrontend } from '../../../../helpers/date'
|
|
import { getBillTotal } from '../../../../helpers/bill'
|
|
import { getBookingStatus } from '../../../../helpers/booking'
|
|
import withSession, {
|
|
isAdminSession,
|
|
redirectToLogin,
|
|
} from '../../../../lib/session'
|
|
import { getServerSideBooking } from '../../../../lib/getServerSideProps'
|
|
|
|
export const getServerSideProps = withSession(async (context) => {
|
|
const { req, res } = context
|
|
|
|
const adminUser = isAdminSession(req)
|
|
|
|
if (!adminUser) {
|
|
redirectToLogin(req, res)
|
|
return { props: {} }
|
|
}
|
|
|
|
const milageMax = await getMilageMax()
|
|
const serverSideBookingProps = await getServerSideBooking(context)
|
|
return {
|
|
props: {
|
|
...serverSideBookingProps.props.booking,
|
|
milageMax,
|
|
user: adminUser,
|
|
},
|
|
}
|
|
})
|
|
|
|
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!!!'
|
|
}
|
|
}
|
|
|
|
async function saveBill(
|
|
booking: Booking,
|
|
bill: {
|
|
milageStart: number
|
|
milageEnd: number
|
|
milage?: number
|
|
tarif: MILAGE_TARIFS
|
|
additionalCosts: AdditionalCost[]
|
|
status: BILL_STATUS
|
|
}
|
|
): Promise<Bill> {
|
|
const response = await fetch(`/api/admin/booking/${booking.uuid}/bill`, {
|
|
method: !!booking.bill ? 'PATCH' : 'POST',
|
|
mode: 'cors',
|
|
cache: 'no-cache',
|
|
credentials: 'same-origin',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
referrerPolicy: 'no-referrer',
|
|
body: JSON.stringify(bill),
|
|
})
|
|
return response.json()
|
|
}
|
|
|
|
export default function BookingBillPage({
|
|
booking: bookingProp,
|
|
milageMax,
|
|
}: {
|
|
booking: Booking
|
|
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 bill = await saveBill(booking, {
|
|
milageStart,
|
|
milageEnd,
|
|
milage,
|
|
tarif,
|
|
status,
|
|
additionalCosts,
|
|
})
|
|
|
|
booking.bill = bill
|
|
setBooking(booking)
|
|
} catch (error) {
|
|
setStoringError('Buchung konnte nicht gespeichert werden!')
|
|
console.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 (
|
|
<div className="mx-3 flex flex-col min-h-screen">
|
|
<Header />
|
|
<main className="flex-grow">
|
|
<h2 className="text-3xl">Abrechnung</h2>
|
|
{booking && (
|
|
<form className="form" onSubmit={onSubmit}>
|
|
<div>
|
|
<strong>Buchungszeitraum:</strong>{' '}
|
|
{dateFormatFrontend(new Date(booking.startDate))} -{' '}
|
|
{dateFormatFrontend(new Date(booking.endDate))}
|
|
</div>
|
|
<div>
|
|
<strong>Bucher:</strong> {booking.booker.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>
|
|
)}
|
|
</main>
|
|
|
|
<Footer />
|
|
</div>
|
|
)
|
|
}
|