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)
This commit is contained in:
193
app/components/WearPartList.tsx
Normal file
193
app/components/WearPartList.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user