Files
WearPartTracker/app/components/MaintenanceForm.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

212 lines
6.6 KiB
TypeScript

'use client'
import { useState } from 'react'
import { maintenanceHistorySchema, MaintenanceAction } from '@/lib/validations'
interface MaintenanceFormProps {
partId: string
onSuccess: () => void
onCancel: () => void
}
const maintenanceActions = ['INSTALL', 'REPLACE', 'SERVICE', 'CHECK', 'ADJUST']
export default function MaintenanceForm({
partId,
onSuccess,
onCancel,
}: MaintenanceFormProps) {
const [formData, setFormData] = useState({
date: new Date().toISOString().split('T')[0],
mileage: 0,
action: 'SERVICE',
notes: '',
cost: '',
})
const [errors, setErrors] = useState<Record<string, string>>({})
const [isSubmitting, setIsSubmitting] = useState(false)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setErrors({})
setIsSubmitting(true)
try {
const validatedData = maintenanceHistorySchema.parse({
wearPartId: partId,
date: formData.date,
mileage: Number(formData.mileage),
action: formData.action,
notes: formData.notes || undefined,
cost: formData.cost ? Number(formData.cost) : undefined,
})
const response = await fetch(`/api/parts/${partId}/maintenance`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(validatedData),
})
if (response.ok) {
onSuccess()
setFormData({
date: new Date().toISOString().split('T')[0],
mileage: 0,
action: 'SERVICE',
notes: '',
cost: '',
})
} else {
const errorData = await response.json()
if (errorData.details) {
const zodErrors: Record<string, string> = {}
errorData.details.errors?.forEach((err: any) => {
if (err.path) {
zodErrors[err.path[0]] = err.message
}
})
setErrors(zodErrors)
} else {
alert('Fehler beim Erstellen des Wartungseintrags')
}
}
} catch (error: any) {
if (error.errors) {
const zodErrors: Record<string, string> = {}
error.errors.forEach((err: any) => {
if (err.path) {
zodErrors[err.path[0]] = err.message
}
})
setErrors(zodErrors)
} else {
console.error('Error creating maintenance:', error)
alert('Fehler beim Erstellen des Wartungseintrags')
}
} finally {
setIsSubmitting(false)
}
}
return (
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
<h4 className="font-semibold mb-3">Neuer Wartungseintrag</h4>
<form onSubmit={handleSubmit} className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Datum *
</label>
<input
type="date"
value={formData.date}
onChange={(e) =>
setFormData({ ...formData, date: e.target.value })
}
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required
/>
{errors.date && (
<p className="text-red-600 text-xs mt-1">{errors.date}</p>
)}
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Kilometerstand *
</label>
<input
type="number"
value={formData.mileage}
onChange={(e) =>
setFormData({ ...formData, mileage: Number(e.target.value) })
}
min="0"
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required
/>
{errors.mileage && (
<p className="text-red-600 text-xs mt-1">{errors.mileage}</p>
)}
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Aktion *
</label>
<select
value={formData.action}
onChange={(e) =>
setFormData({ ...formData, action: e.target.value })
}
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required
>
{maintenanceActions.map((action) => (
<option key={action} value={action}>
{action.replace(/_/g, ' ')}
</option>
))}
</select>
{errors.action && (
<p className="text-red-600 text-xs mt-1">{errors.action}</p>
)}
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Kosten ()
</label>
<input
type="number"
step="0.01"
value={formData.cost}
onChange={(e) =>
setFormData({ ...formData, cost: e.target.value })
}
min="0"
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Notizen
</label>
<textarea
value={formData.notes}
onChange={(e) =>
setFormData({ ...formData, notes: e.target.value })
}
rows={2}
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div className="flex gap-2">
<button
type="submit"
disabled={isSubmitting}
className="px-4 py-2 bg-blue-600 text-white text-sm rounded hover:bg-blue-700 transition-colors disabled:opacity-50"
>
{isSubmitting ? 'Erstellen...' : 'Erstellen'}
</button>
<button
type="button"
onClick={onCancel}
className="px-4 py-2 bg-gray-300 text-gray-700 text-sm rounded hover:bg-gray-400 transition-colors"
>
Abbrechen
</button>
</div>
</form>
</div>
)
}