- 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)
194 lines
6.7 KiB
TypeScript
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 "Neues Verschleißteil" 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>
|
|
)
|
|
}
|
|
|