Feature: Kilometerstand-Feld für Fahrräder und verbesserte Warnungen
- 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
This commit is contained in:
@@ -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 (
|
||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-orange-100 text-orange-800">
|
||||
<span
|
||||
className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${bgColor} ${textColor}`}
|
||||
>
|
||||
⚠️ {count}
|
||||
</span>
|
||||
)
|
||||
|
||||
@@ -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) {
|
||||
)}
|
||||
</div>
|
||||
{needsServiceParts.length > 0 && (
|
||||
<AlertBadge count={needsServiceParts.length} />
|
||||
<AlertBadge
|
||||
count={needsServiceParts.length}
|
||||
variant={overdueParts.length > 0 ? 'critical' : 'warning'}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -64,7 +71,15 @@ export default function BikeCard({ bike, onDelete }: BikeCardProps) {
|
||||
<p className="text-sm text-gray-500">
|
||||
{activeParts.length} aktive Verschleißteile
|
||||
</p>
|
||||
{needsServiceParts.length > 0 && (
|
||||
<p className="text-sm text-gray-500">
|
||||
{bike.currentMileage.toLocaleString('de-DE')} km
|
||||
</p>
|
||||
{overdueParts.length > 0 && (
|
||||
<p className="text-sm text-red-600 font-medium mt-1">
|
||||
{overdueParts.length} überfällig!
|
||||
</p>
|
||||
)}
|
||||
{needsServiceParts.length > 0 && overdueParts.length === 0 && (
|
||||
<p className="text-sm text-orange-600 font-medium mt-1">
|
||||
{needsServiceParts.length} benötigen Wartung
|
||||
</p>
|
||||
|
||||
@@ -25,8 +25,16 @@ export default function BikeDetail({ bike, onUpdate }: BikeDetailProps) {
|
||||
</Link>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-4">{bike.name}</h1>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h1 className="text-3xl font-bold text-gray-900">{bike.name}</h1>
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-gray-500">Aktueller Kilometerstand</p>
|
||||
<p className="text-2xl font-bold text-gray-900">
|
||||
{bike.currentMileage.toLocaleString('de-DE')} km
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 text-sm">
|
||||
{bike.brand && (
|
||||
<div>
|
||||
<span className="text-gray-500">Marke:</span>
|
||||
@@ -81,7 +89,12 @@ export default function BikeDetail({ bike, onUpdate }: BikeDetailProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<WearPartList bikeId={bike.id} parts={bike.wearParts} onUpdate={onUpdate} />
|
||||
<WearPartList
|
||||
bikeId={bike.id}
|
||||
parts={bike.wearParts}
|
||||
bikeCurrentMileage={bike.currentMileage}
|
||||
onUpdate={onUpdate}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
|
||||
@@ -14,6 +14,7 @@ export default function BikeForm({ onSuccess, onCancel }: BikeFormProps) {
|
||||
brand: '',
|
||||
model: '',
|
||||
purchaseDate: '',
|
||||
currentMileage: 0,
|
||||
notes: '',
|
||||
})
|
||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||
@@ -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) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Kaufdatum
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={formData.purchaseDate}
|
||||
onChange={(e) =>
|
||||
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"
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Kaufdatum
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={formData.purchaseDate}
|
||||
onChange={(e) =>
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Aktueller Kilometerstand (km)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.currentMileage}
|
||||
onChange={(e) =>
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -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<string | null>(null)
|
||||
@@ -54,7 +56,7 @@ export default function WearPartList({
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{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({
|
||||
<h3 className="text-xl font-semibold text-gray-900">
|
||||
{part.type}
|
||||
</h3>
|
||||
{serviceStatus.status !== 'OK' && (
|
||||
<AlertBadge count={1} />
|
||||
{(serviceStatus.status !== 'OK' || serviceStatus.isOverdue) && (
|
||||
<AlertBadge
|
||||
count={1}
|
||||
variant={serviceStatus.status === 'OVERDUE' ? 'critical' : 'warning'}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
className={`px-2 py-1 rounded text-xs font-medium ${
|
||||
@@ -122,16 +127,25 @@ export default function WearPartList({
|
||||
? 'text-green-600'
|
||||
: serviceStatus.status === 'WARNING'
|
||||
? 'text-orange-600'
|
||||
: 'text-red-600'
|
||||
: serviceStatus.status === 'CRITICAL'
|
||||
? 'text-red-600'
|
||||
: 'text-red-800 font-bold'
|
||||
}`}
|
||||
>
|
||||
{serviceStatus.status === 'OK'
|
||||
? 'OK'
|
||||
: serviceStatus.status === 'WARNING'
|
||||
? 'Warnung'
|
||||
: 'Kritisch'}
|
||||
: serviceStatus.status === 'CRITICAL'
|
||||
? 'Kritisch'
|
||||
: 'Überfällig!'}
|
||||
</span>
|
||||
</div>
|
||||
{serviceStatus.isOverdue && (
|
||||
<div className="mb-2 p-2 bg-red-100 border border-red-300 rounded text-sm text-red-800">
|
||||
⚠️ Wartung ist um {Math.abs(serviceStatus.remainingKm).toFixed(0)} km überfällig!
|
||||
</div>
|
||||
)}
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full ${
|
||||
@@ -139,13 +153,19 @@ export default function WearPartList({
|
||||
? 'bg-green-500'
|
||||
: serviceStatus.status === 'WARNING'
|
||||
? 'bg-orange-500'
|
||||
: 'bg-red-500'
|
||||
: serviceStatus.status === 'CRITICAL'
|
||||
? 'bg-red-500'
|
||||
: 'bg-red-700'
|
||||
}`}
|
||||
style={{ width: `${serviceStatus.percentageUsed}%` }}
|
||||
style={{
|
||||
width: `${Math.min(100, Math.max(0, serviceStatus.percentageUsed))}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{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`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user