From d37676f3c0248e1f877803d57f7d757eff1b445b Mon Sep 17 00:00:00 2001 From: Denis Urs Rudolph Date: Fri, 5 Dec 2025 22:36:58 +0100 Subject: [PATCH] =?UTF-8?q?Feature:=20Kilometerstand-Feld=20f=C3=BCr=20Fah?= =?UTF-8?q?rr=C3=A4der=20und=20verbesserte=20Warnungen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - currentMileage Feld zu Bike-Modell hinzugefügt - calculateServiceStatus verwendet jetzt aktuellen Kilometerstand des Fahrrads - Warnungen für überfällige Wartungen (OVERDUE Status) - Rote Warnung bei überfälligen Teilen - Kilometerstand wird in BikeDetail und BikeCard angezeigt - AlertBadge unterstützt jetzt critical Variante - Verbesserte Statusanzeige mit Überfällig-Hinweis --- app/api/bikes/[id]/route.ts | 1 + app/api/bikes/route.ts | 1 + app/components/AlertBadge.tsx | 13 ++++++++-- app/components/BikeCard.tsx | 23 ++++++++++++++--- app/components/BikeDetail.tsx | 19 +++++++++++--- app/components/BikeForm.tsx | 46 ++++++++++++++++++++++++--------- app/components/WearPartList.tsx | 36 ++++++++++++++++++++------ lib/utils.ts | 19 +++++++++----- lib/validations.ts | 1 + prisma/schema.prisma | 19 +++++++------- 10 files changed, 134 insertions(+), 44 deletions(-) diff --git a/app/api/bikes/[id]/route.ts b/app/api/bikes/[id]/route.ts index 08a22ef..808c1ac 100644 --- a/app/api/bikes/[id]/route.ts +++ b/app/api/bikes/[id]/route.ts @@ -57,6 +57,7 @@ export async function PUT( brand: validatedData.brand, model: validatedData.model, purchaseDate: validatedData.purchaseDate || null, + currentMileage: validatedData.currentMileage ?? 0, notes: validatedData.notes, }, }) diff --git a/app/api/bikes/route.ts b/app/api/bikes/route.ts index 5f259eb..273b310 100644 --- a/app/api/bikes/route.ts +++ b/app/api/bikes/route.ts @@ -42,6 +42,7 @@ export async function POST(request: NextRequest) { brand: validatedData.brand, model: validatedData.model, purchaseDate: validatedData.purchaseDate || null, + currentMileage: validatedData.currentMileage ?? 0, notes: validatedData.notes, }, }) diff --git a/app/components/AlertBadge.tsx b/app/components/AlertBadge.tsx index f607b36..b18fb4c 100644 --- a/app/components/AlertBadge.tsx +++ b/app/components/AlertBadge.tsx @@ -1,12 +1,21 @@ interface AlertBadgeProps { count: number + variant?: 'warning' | 'critical' } -export default function AlertBadge({ count }: AlertBadgeProps) { +export default function AlertBadge({ + count, + variant = 'warning', +}: AlertBadgeProps) { if (count === 0) return null + const bgColor = variant === 'critical' ? 'bg-red-100' : 'bg-orange-100' + const textColor = variant === 'critical' ? 'text-red-800' : 'text-orange-800' + return ( - + ⚠️ {count} ) diff --git a/app/components/BikeCard.tsx b/app/components/BikeCard.tsx index 7a8f68d..b7ad96f 100644 --- a/app/components/BikeCard.tsx +++ b/app/components/BikeCard.tsx @@ -40,8 +40,12 @@ export default function BikeCard({ bike, onDelete }: BikeCardProps) { const activeParts = bike.wearParts.filter((p) => p.status === 'ACTIVE') const needsServiceParts = activeParts.filter((part) => { - const serviceStatus = calculateServiceStatus(part) - return serviceStatus.status !== 'OK' + const serviceStatus = calculateServiceStatus(part, bike.currentMileage) + return serviceStatus.status !== 'OK' || serviceStatus.isOverdue + }) + const overdueParts = activeParts.filter((part) => { + const serviceStatus = calculateServiceStatus(part, bike.currentMileage) + return serviceStatus.isOverdue }) return ( @@ -56,7 +60,10 @@ export default function BikeCard({ bike, onDelete }: BikeCardProps) { )} {needsServiceParts.length > 0 && ( - + 0 ? 'critical' : 'warning'} + /> )} @@ -64,7 +71,15 @@ export default function BikeCard({ bike, onDelete }: BikeCardProps) {

{activeParts.length} aktive Verschleißteile

- {needsServiceParts.length > 0 && ( +

+ {bike.currentMileage.toLocaleString('de-DE')} km +

+ {overdueParts.length > 0 && ( +

+ {overdueParts.length} überfällig! +

+ )} + {needsServiceParts.length > 0 && overdueParts.length === 0 && (

{needsServiceParts.length} benötigen Wartung

diff --git a/app/components/BikeDetail.tsx b/app/components/BikeDetail.tsx index 26d5703..66070d3 100644 --- a/app/components/BikeDetail.tsx +++ b/app/components/BikeDetail.tsx @@ -25,8 +25,16 @@ export default function BikeDetail({ bike, onUpdate }: BikeDetailProps) {
-

{bike.name}

-
+
+

{bike.name}

+
+

Aktueller Kilometerstand

+

+ {bike.currentMileage.toLocaleString('de-DE')} km +

+
+
+
{bike.brand && (
Marke: @@ -81,7 +89,12 @@ export default function BikeDetail({ bike, onUpdate }: BikeDetailProps) {
)} - +
) diff --git a/app/components/BikeForm.tsx b/app/components/BikeForm.tsx index cf77232..f635d08 100644 --- a/app/components/BikeForm.tsx +++ b/app/components/BikeForm.tsx @@ -14,6 +14,7 @@ export default function BikeForm({ onSuccess, onCancel }: BikeFormProps) { brand: '', model: '', purchaseDate: '', + currentMileage: 0, notes: '', }) const [errors, setErrors] = useState>({}) @@ -45,6 +46,7 @@ export default function BikeForm({ onSuccess, onCancel }: BikeFormProps) { brand: '', model: '', purchaseDate: '', + currentMileage: 0, notes: '', }) } else { @@ -131,18 +133,38 @@ export default function BikeForm({ onSuccess, onCancel }: BikeFormProps) {
-
- - - setFormData({ ...formData, purchaseDate: e.target.value }) - } - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-black" - /> +
+
+ + + setFormData({ ...formData, purchaseDate: e.target.value }) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-black" + /> +
+ +
+ + + setFormData({ + ...formData, + currentMileage: Number(e.target.value) || 0, + }) + } + min="0" + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-black" + /> +
diff --git a/app/components/WearPartList.tsx b/app/components/WearPartList.tsx index 943fa3a..fa571a7 100644 --- a/app/components/WearPartList.tsx +++ b/app/components/WearPartList.tsx @@ -11,12 +11,14 @@ import { useState } from 'react' interface WearPartListProps { bikeId: string parts: (WearPart & { maintenanceHistory: MaintenanceHistory[] })[] + bikeCurrentMileage?: number onUpdate: () => void } export default function WearPartList({ bikeId, parts, + bikeCurrentMileage, onUpdate, }: WearPartListProps) { const [expandedPart, setExpandedPart] = useState(null) @@ -54,7 +56,7 @@ export default function WearPartList({ return (
{parts.map((part) => { - const serviceStatus = calculateServiceStatus(part) + const serviceStatus = calculateServiceStatus(part, bikeCurrentMileage) const isExpanded = expandedPart === part.id return ( @@ -68,8 +70,11 @@ export default function WearPartList({

{part.type}

- {serviceStatus.status !== 'OK' && ( - + {(serviceStatus.status !== 'OK' || serviceStatus.isOverdue) && ( + )} {serviceStatus.status === 'OK' ? 'OK' : serviceStatus.status === 'WARNING' ? 'Warnung' - : 'Kritisch'} + : serviceStatus.status === 'CRITICAL' + ? 'Kritisch' + : 'Überfällig!'}
+ {serviceStatus.isOverdue && ( +
+ ⚠️ Wartung ist um {Math.abs(serviceStatus.remainingKm).toFixed(0)} km überfällig! +
+ )}

- {serviceStatus.remainingKm.toFixed(0)} km bis zur Wartung + {serviceStatus.isOverdue + ? `${Math.abs(serviceStatus.remainingKm).toFixed(0)} km überfällig` + : `${serviceStatus.remainingKm.toFixed(0)} km bis zur Wartung`}

diff --git a/lib/utils.ts b/lib/utils.ts index fcdc325..cc19cde 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,20 +1,26 @@ import { WearPart, MaintenanceHistory } from '@prisma/client' export function calculateServiceStatus( - part: WearPart & { maintenanceHistory: MaintenanceHistory[] } + part: WearPart & { maintenanceHistory: MaintenanceHistory[] }, + bikeCurrentMileage?: number ): { - status: 'OK' | 'WARNING' | 'CRITICAL' + status: 'OK' | 'WARNING' | 'CRITICAL' | 'OVERDUE' remainingKm: number percentageUsed: number + isOverdue: boolean } { + // Use bike's current mileage if provided, otherwise use latest maintenance mileage const latestMaintenance = part.maintenanceHistory[0] - const currentMileage = latestMaintenance?.mileage ?? part.installMileage + const currentMileage = bikeCurrentMileage ?? (latestMaintenance?.mileage ?? part.installMileage) const kmSinceInstall = currentMileage - part.installMileage const remainingKm = part.serviceInterval - kmSinceInstall const percentageUsed = (kmSinceInstall / part.serviceInterval) * 100 + const isOverdue = remainingKm < 0 - let status: 'OK' | 'WARNING' | 'CRITICAL' = 'OK' - if (percentageUsed >= 90) { + let status: 'OK' | 'WARNING' | 'CRITICAL' | 'OVERDUE' = 'OK' + if (isOverdue) { + status = 'OVERDUE' + } else if (percentageUsed >= 90) { status = 'CRITICAL' } else if (percentageUsed >= 75) { status = 'WARNING' @@ -23,7 +29,8 @@ export function calculateServiceStatus( return { status, remainingKm: Math.max(0, remainingKm), - percentageUsed: Math.min(100, percentageUsed), + percentageUsed: Math.min(100, Math.max(0, percentageUsed)), + isOverdue, } } diff --git a/lib/validations.ts b/lib/validations.ts index a78a1e0..95caf63 100644 --- a/lib/validations.ts +++ b/lib/validations.ts @@ -66,6 +66,7 @@ export const bikeSchema = z.object({ .refine((val) => val === undefined || val instanceof Date, { message: 'Ungültiges Datumsformat', }), + currentMileage: z.number().int().min(0).optional().default(0), notes: z.string().optional().transform((val) => val?.trim() || undefined), }) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b54bfc9..f4bc87c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -11,15 +11,16 @@ datasource db { } model Bike { - id String @id @default(cuid()) - name String - brand String? - model String? - purchaseDate DateTime? - notes String? - wearParts WearPart[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + name String + brand String? + model String? + purchaseDate DateTime? + currentMileage Int @default(0) + notes String? + wearParts WearPart[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } model WearPart {