Files
WearPartTracker/app/components/WearPartList.tsx
Denis Urs Rudolph d37676f3c0 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
2025-12-05 22:37:10 +01:00

214 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { WearPart } from '@prisma/client'
import { MaintenanceHistory } from '@prisma/client'
import { calculateServiceStatus } from '@/lib/utils'
import { formatDate, formatCurrency } from '@/lib/utils'
import AlertBadge from './AlertBadge'
import MaintenanceTimeline from './MaintenanceTimeline'
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)
const handleDelete = async (partId: string) => {
if (!confirm('Möchten Sie dieses Verschleißteil wirklich löschen?')) {
return
}
try {
const response = await fetch(`/api/parts/${partId}`, {
method: 'DELETE',
})
if (response.ok) {
onUpdate()
} else {
alert('Fehler beim Löschen des Verschleißteils')
}
} catch (error) {
console.error('Error deleting part:', error)
alert('Fehler beim Löschen des Verschleißteils')
}
}
if (parts.length === 0) {
return (
<div className="bg-white rounded-lg shadow-md p-8 text-center text-gray-500">
<p>Noch keine Verschleißteile erfasst.</p>
<p className="mt-2">Klicken Sie auf &quot;Neues Verschleißteil&quot; um zu beginnen.</p>
</div>
)
}
return (
<div className="space-y-4">
{parts.map((part) => {
const serviceStatus = calculateServiceStatus(part, bikeCurrentMileage)
const isExpanded = expandedPart === part.id
return (
<div
key={part.id}
className="bg-white rounded-lg shadow-md p-6"
>
<div className="flex justify-between items-start">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-xl font-semibold text-gray-900">
{part.type}
</h3>
{(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 ${
part.status === 'ACTIVE'
? 'bg-green-100 text-green-800'
: part.status === 'NEEDS_SERVICE'
? 'bg-orange-100 text-orange-800'
: part.status === 'REPLACED'
? 'bg-gray-100 text-gray-800'
: 'bg-red-100 text-red-800'
}`}
>
{part.status}
</span>
</div>
{part.brand && part.model && (
<p className="text-gray-600 mb-2">
{part.brand} {part.model}
</p>
)}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm mt-4">
<div>
<span className="text-gray-500">Installiert:</span>
<p className="font-medium">{formatDate(part.installDate)}</p>
</div>
<div>
<span className="text-gray-500">Installations-KM:</span>
<p className="font-medium">{part.installMileage} km</p>
</div>
<div>
<span className="text-gray-500">Service-Intervall:</span>
<p className="font-medium">{part.serviceInterval} km</p>
</div>
{part.cost && (
<div>
<span className="text-gray-500">Kosten:</span>
<p className="font-medium">{formatCurrency(part.cost)}</p>
</div>
)}
</div>
<div className="mt-4">
<div className="flex items-center gap-2 mb-1">
<span className="text-sm text-gray-500">Status:</span>
<span
className={`text-sm font-medium ${
serviceStatus.status === 'OK'
? 'text-green-600'
: serviceStatus.status === 'WARNING'
? 'text-orange-600'
: serviceStatus.status === 'CRITICAL'
? 'text-red-600'
: 'text-red-800 font-bold'
}`}
>
{serviceStatus.status === 'OK'
? 'OK'
: serviceStatus.status === 'WARNING'
? 'Warnung'
: 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 ${
serviceStatus.status === 'OK'
? 'bg-green-500'
: serviceStatus.status === 'WARNING'
? 'bg-orange-500'
: serviceStatus.status === 'CRITICAL'
? 'bg-red-500'
: 'bg-red-700'
}`}
style={{
width: `${Math.min(100, Math.max(0, serviceStatus.percentageUsed))}%`,
}}
/>
</div>
<p className="text-xs text-gray-500 mt-1">
{serviceStatus.isOverdue
? `${Math.abs(serviceStatus.remainingKm).toFixed(0)} km überfällig`
: `${serviceStatus.remainingKm.toFixed(0)} km bis zur Wartung`}
</p>
</div>
{part.notes && (
<div className="mt-4">
<span className="text-sm text-gray-500">Notizen:</span>
<p className="text-sm mt-1">{part.notes}</p>
</div>
)}
</div>
<div className="flex flex-col gap-2 ml-4">
<button
onClick={() =>
setExpandedPart(isExpanded ? null : part.id)
}
className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors text-sm"
>
{isExpanded ? 'Weniger' : 'Historie'}
</button>
<button
onClick={() => handleDelete(part.id)}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors text-sm"
>
Löschen
</button>
</div>
</div>
{isExpanded && (
<div className="mt-6 pt-6 border-t">
<MaintenanceTimeline
partId={part.id}
history={part.maintenanceHistory}
onUpdate={onUpdate}
/>
</div>
)}
</div>
)
})}
</div>
)
}