Files
WearPartTracker/app/components/WearPartList.tsx
Denis Urs Rudolph de193bc783 Initial commit: Fahrrad Verschleißteile Tracker
- Next.js SPA mit Bun Runtime
- Prisma mit SQLite Datenbank
- Vollständige CRUD-Operationen für Fahrräder, Verschleißteile und Wartungshistorie
- Warnsystem für bevorstehende Wartungen
- Statistik-Features (Gesamtkosten, durchschnittliche Lebensdauer)
- Zod-Validierung für alle API-Requests
- Umfassende Test-Suite (41 Tests)
2025-12-05 22:17:50 +01:00

194 lines
6.7 KiB
TypeScript

'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[] })[]
onUpdate: () => void
}
export default function WearPartList({
bikeId,
parts,
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)
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' && (
<AlertBadge count={1} />
)}
<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'
: 'text-red-600'
}`}
>
{serviceStatus.status === 'OK'
? 'OK'
: serviceStatus.status === 'WARNING'
? 'Warnung'
: 'Kritisch'}
</span>
</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'
: 'bg-red-500'
}`}
style={{ width: `${serviceStatus.percentageUsed}%` }}
/>
</div>
<p className="text-xs text-gray-500 mt-1">
{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>
)
}