mirror of
https://github.com/tomru/pfadi-bussle.git
synced 2026-03-03 06:27:11 +01:00
use fixed tarifs and add additional costs
This commit is contained in:
@@ -1,10 +1,6 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function Header({
|
||||
label = 'Pfadi Bussle Buchen',
|
||||
}: {
|
||||
label?: string
|
||||
}) {
|
||||
export default function Header({ label = 'Pfadi Bussle' }: { label?: string }) {
|
||||
return (
|
||||
<div className="flex flex-row items-center p-3 my-3 text-white bg-gray-600 rounded-sm">
|
||||
<h1 className="flex-grow text-3xl">{label}</h1>
|
||||
|
||||
23
db/bill.ts
23
db/bill.ts
@@ -1,7 +1,8 @@
|
||||
import * as mongoose from 'mongoose'
|
||||
import { BILL_STATUS, MILAGE_RATES, getMilageRateValue } from './enums'
|
||||
import { BILL_STATUS, MILAGE_TARIFS } from './enums'
|
||||
import { getBillTotal } from '../helpers/bill'
|
||||
|
||||
export interface AdditionalCosts {
|
||||
export interface AdditionalCost {
|
||||
name: string
|
||||
value: number
|
||||
}
|
||||
@@ -12,9 +13,9 @@ export interface BillDocument
|
||||
milageStart: number
|
||||
milageEnd: number
|
||||
milage?: number
|
||||
rate: MILAGE_RATES
|
||||
tarif: MILAGE_TARIFS
|
||||
status: BILL_STATUS
|
||||
additionalCosts: AdditionalCosts[]
|
||||
additionalCosts: AdditionalCost[]
|
||||
}
|
||||
|
||||
export interface BillModel extends mongoose.Model<BillDocument> {}
|
||||
@@ -46,10 +47,10 @@ const BillSchema = new mongoose.Schema<BillDocument>(
|
||||
message: (props) => `${props.value} is smaller than milageStart!`,
|
||||
},
|
||||
},
|
||||
rate: {
|
||||
tarif: {
|
||||
type: String,
|
||||
enum: Object.values(MILAGE_RATES),
|
||||
default: MILAGE_RATES.EXTERN_LTE_200,
|
||||
enum: Object.values(MILAGE_TARIFS),
|
||||
default: MILAGE_TARIFS.EXTERN,
|
||||
required: true,
|
||||
},
|
||||
additionalCosts: [
|
||||
@@ -79,13 +80,7 @@ BillSchema.virtual('milage').get(function () {
|
||||
BillSchema.virtual('total').get(function () {
|
||||
const bill = this as BillDocument
|
||||
|
||||
const milageCosts =
|
||||
Math.round(bill.milage * getMilageRateValue(bill.rate) * 100) / 100
|
||||
const additionalCostSum = bill.additionalCosts
|
||||
.map(({ value }) => value)
|
||||
.reduce((acc, value) => acc + value, 0)
|
||||
|
||||
return milageCosts + additionalCostSum
|
||||
return getBillTotal(bill)
|
||||
})
|
||||
|
||||
export default <BillModel>mongoose.models.Bill ||
|
||||
|
||||
30
db/enums.ts
30
db/enums.ts
@@ -11,30 +11,8 @@ export enum BILL_STATUS {
|
||||
PAID = 'paid',
|
||||
}
|
||||
|
||||
export enum MILAGE_RATES {
|
||||
INTERN_LTE_200 = 'intern_lte_200_km',
|
||||
INTERN_201_1000 = 'intern_201_1000_km',
|
||||
INTERN_1001_2000 = 'intern_1001_2000_km',
|
||||
INTERN_GTE_2001 = 'intern_gte_2001_km',
|
||||
EXTERN_LTE_200 = 'extern_lte_200_km',
|
||||
EXTERN_201_1000 = 'extern_201_1000_km',
|
||||
EXTERN_1001_2000 = 'extern_1001_2000_km',
|
||||
EXTERN_GTE_2001 = 'extern_gte_2001_km',
|
||||
FREE_OF_CHARGE = 'free_of_charge',
|
||||
}
|
||||
|
||||
const rates = {
|
||||
[MILAGE_RATES.INTERN_LTE_200]: 0.37,
|
||||
[MILAGE_RATES.INTERN_201_1000]: 0.22,
|
||||
[MILAGE_RATES.INTERN_1001_2000]: 0.15,
|
||||
[MILAGE_RATES.INTERN_GTE_2001]: 0.13,
|
||||
[MILAGE_RATES.EXTERN_LTE_200]: 0.42,
|
||||
[MILAGE_RATES.EXTERN_201_1000]: 0.25,
|
||||
[MILAGE_RATES.EXTERN_1001_2000]: 0.2,
|
||||
[MILAGE_RATES.EXTERN_GTE_2001]: 0.18,
|
||||
[MILAGE_RATES.FREE_OF_CHARGE]: 0,
|
||||
}
|
||||
|
||||
export function getMilageRateValue(milageRate: MILAGE_RATES): number {
|
||||
return rates[milageRate]
|
||||
export enum MILAGE_TARIFS {
|
||||
INTERN = 'intern',
|
||||
EXTERN = 'extern',
|
||||
NOCHARGE = 'nocharge',
|
||||
}
|
||||
|
||||
65
helpers/bill.ts
Normal file
65
helpers/bill.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { MILAGE_TARIFS } from '../db/enums'
|
||||
import { AdditionalCost } from '../db/bill'
|
||||
|
||||
function roundToCent(amount: number) {
|
||||
return Math.round(amount * 100) / 100
|
||||
}
|
||||
|
||||
export function getMilageCosts(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: AdditionalCost[]
|
||||
}): number {
|
||||
const milageCosts = getMilageCosts(tarif, milage)
|
||||
const additionalCostsSum = additionalCosts
|
||||
.map(({ value }) => value)
|
||||
.reduce((acc: number, value: number) => acc + value, 0)
|
||||
|
||||
return roundToCent(milageCosts + additionalCostsSum)
|
||||
}
|
||||
@@ -4,20 +4,17 @@ import Footer from '../../../components/footer'
|
||||
import Header from '../../../components/header'
|
||||
import Input from '../../../components/input'
|
||||
import Select from '../../../components/select'
|
||||
import { AdditionalCosts, BillDocument } from '../../../db/bill'
|
||||
import { AdditionalCost, BillDocument } from '../../../db/bill'
|
||||
import { BookingDocument } from '../../../db/booking'
|
||||
import {
|
||||
BILL_STATUS,
|
||||
MILAGE_RATES,
|
||||
getMilageRateValue,
|
||||
} from '../../../db/enums'
|
||||
import { BILL_STATUS, MILAGE_TARIFS } from '../../../db/enums'
|
||||
import { getBookingByUUID, getMilageMax } from '../../../db/index'
|
||||
import { dateFormatFrontend } from '../../../helpers/date'
|
||||
import { getBillTotal } from '../../../helpers/bill'
|
||||
|
||||
const milageRateOptions = Object.values(MILAGE_RATES).map((rate) => {
|
||||
const milageTarifOptions = Object.values(MILAGE_TARIFS).map((tarif) => {
|
||||
return (
|
||||
<option value={rate} key={rate}>
|
||||
{getRateLabel(rate)} ({getMilageRateValue(rate)} EUR)
|
||||
<option value={tarif} key={tarif}>
|
||||
{getTarifLabel(tarif)}
|
||||
</option>
|
||||
)
|
||||
})
|
||||
@@ -54,26 +51,14 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
}
|
||||
}
|
||||
|
||||
function getRateLabel(rate: MILAGE_RATES) {
|
||||
switch (rate) {
|
||||
case MILAGE_RATES.INTERN_LTE_200:
|
||||
return 'Intern bis zu 200km'
|
||||
case MILAGE_RATES.INTERN_201_1000:
|
||||
return 'Intern 201-1.000km'
|
||||
case MILAGE_RATES.INTERN_1001_2000:
|
||||
return 'Intern 1.001-2.000km'
|
||||
case MILAGE_RATES.INTERN_GTE_2001:
|
||||
return 'Intern ab 2.001km'
|
||||
case MILAGE_RATES.EXTERN_LTE_200:
|
||||
return 'Extern bis zu 200km'
|
||||
case MILAGE_RATES.EXTERN_201_1000:
|
||||
return 'Extern 201-1.000km'
|
||||
case MILAGE_RATES.EXTERN_1001_2000:
|
||||
return 'Extern 1.001-2.000km'
|
||||
case MILAGE_RATES.EXTERN_GTE_2001:
|
||||
return 'Extern ab 2.001km'
|
||||
case MILAGE_RATES.FREE_OF_CHARGE:
|
||||
return 'Frei'
|
||||
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'
|
||||
}
|
||||
@@ -98,8 +83,8 @@ async function saveBill(
|
||||
milageStart: number
|
||||
milageEnd: number
|
||||
milage?: number
|
||||
rate: MILAGE_RATES
|
||||
additionalCosts?: AdditionalCosts[]
|
||||
tarif: MILAGE_TARIFS
|
||||
additionalCosts: AdditionalCost[]
|
||||
status: BILL_STATUS
|
||||
}
|
||||
): Promise<BillDocument> {
|
||||
@@ -129,17 +114,16 @@ export default function BillPage({
|
||||
booking.bill?.milageStart || milageMax
|
||||
)
|
||||
const [milageEnd, setMilageEnd] = useState(booking.bill?.milageEnd)
|
||||
const [rate, setRate] = useState(
|
||||
booking.bill?.rate || MILAGE_RATES.EXTERN_LTE_200
|
||||
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 =
|
||||
Math.round(milage && rate && milage * getMilageRateValue(rate) * 100) /
|
||||
100 || 0
|
||||
const total = getBillTotal({ tarif, milage, additionalCosts })
|
||||
|
||||
// in case the props change, update the internal state
|
||||
useEffect(() => setBooking(bookingProp), [bookingProp])
|
||||
@@ -154,8 +138,9 @@ export default function BillPage({
|
||||
milageStart,
|
||||
milageEnd,
|
||||
milage,
|
||||
rate,
|
||||
tarif,
|
||||
status,
|
||||
additionalCosts,
|
||||
})
|
||||
|
||||
booking.bill = bill
|
||||
@@ -167,11 +152,29 @@ export default function BillPage({
|
||||
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">Pfadi Bussle Buchung</h2>
|
||||
<h2 className="text-3xl">Abrechnung</h2>
|
||||
<form className="form" onSubmit={onSubmit}>
|
||||
<div>
|
||||
<strong>Buchungszeitraum:</strong>{' '}
|
||||
@@ -205,10 +208,10 @@ export default function BillPage({
|
||||
<Input label="Gefahren" name="milage" readOnly value={milage} />
|
||||
<Select
|
||||
label="Rate"
|
||||
value={rate}
|
||||
onChange={(e) => setRate(e.target.value as MILAGE_RATES)}
|
||||
value={tarif}
|
||||
onChange={(e) => setTarif(e.target.value as MILAGE_TARIFS)}
|
||||
>
|
||||
{milageRateOptions}
|
||||
{milageTarifOptions}
|
||||
</Select>
|
||||
<Select
|
||||
label="Status"
|
||||
@@ -217,7 +220,66 @@ export default function BillPage({
|
||||
>
|
||||
{bookingStatusOptions}
|
||||
</Select>
|
||||
<Input label="Summe" name="milage" readOnly value={total} />
|
||||
<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>
|
||||
{storingError && (
|
||||
<div className="error-message flex-grow">{storingError}</div>
|
||||
|
||||
@@ -94,7 +94,7 @@ export default function ShowBooking({
|
||||
<div className="mx-3 flex flex-col min-h-screen">
|
||||
<Header />
|
||||
<main className="flex-grow">
|
||||
<h2 className="text-3xl">Ihre Pfadi Bussle Buchung</h2>
|
||||
<h2 className="text-3xl">Ihre Buchung</h2>
|
||||
<div>
|
||||
<strong>Buchungsstatus:</strong> {getBookingStatus(booking)}
|
||||
</div>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
}
|
||||
|
||||
.ibtn {
|
||||
@apply py-2 px-2 text-gray-400 w-10 h-10;
|
||||
@apply p-1 text-gray-400 text-xs w-6 h-6 rounded;
|
||||
}
|
||||
|
||||
.ibtn:hover {
|
||||
|
||||
Reference in New Issue
Block a user