Fix: Verschleißteil-Formular Datumsvalidierung und Schriftfarbe

- installDate Schema akzeptiert jetzt YYYY-MM-DD Format (HTML date input)
- maintenanceHistorySchema date akzeptiert jetzt YYYY-MM-DD Format
- Transform konvertiert Datumsstrings automatisch zu Date-Objekten
- API-Routes verwenden validierte Date-Objekte direkt
- Alle Formularfelder haben jetzt schwarze Schriftfarbe (text-black)
- Optional-Felder werden getrimmt (brand, model, notes)
This commit is contained in:
Denis Urs Rudolph
2025-12-05 22:30:45 +01:00
parent 5663fec6a6
commit 81edc206e0
6 changed files with 65 additions and 27 deletions

View File

@@ -48,7 +48,7 @@ export async function POST(
type: validatedData.type, type: validatedData.type,
brand: validatedData.brand, brand: validatedData.brand,
model: validatedData.model, model: validatedData.model,
installDate: new Date(validatedData.installDate), installDate: validatedData.installDate,
installMileage: validatedData.installMileage, installMileage: validatedData.installMileage,
serviceInterval: validatedData.serviceInterval, serviceInterval: validatedData.serviceInterval,
status: validatedData.status, status: validatedData.status,

View File

@@ -38,7 +38,7 @@ export async function POST(
const history = await prisma.maintenanceHistory.create({ const history = await prisma.maintenanceHistory.create({
data: { data: {
wearPartId: params.id, wearPartId: params.id,
date: new Date(validatedData.date), date: validatedData.date,
mileage: validatedData.mileage, mileage: validatedData.mileage,
action: validatedData.action, action: validatedData.action,
notes: validatedData.notes, notes: validatedData.notes,

View File

@@ -93,7 +93,7 @@ export default function BikeForm({ onSuccess, onCancel }: BikeFormProps) {
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, name: e.target.value }) setFormData({ ...formData, name: e.target.value })
} }
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-black"
required required
/> />
{errors.name && ( {errors.name && (
@@ -112,7 +112,7 @@ export default function BikeForm({ onSuccess, onCancel }: BikeFormProps) {
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, brand: e.target.value }) setFormData({ ...formData, brand: e.target.value })
} }
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-black"
/> />
</div> </div>
@@ -126,7 +126,7 @@ export default function BikeForm({ onSuccess, onCancel }: BikeFormProps) {
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, model: e.target.value }) setFormData({ ...formData, model: e.target.value })
} }
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-black"
/> />
</div> </div>
</div> </div>
@@ -141,7 +141,7 @@ export default function BikeForm({ onSuccess, onCancel }: BikeFormProps) {
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, purchaseDate: e.target.value }) setFormData({ ...formData, purchaseDate: e.target.value })
} }
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-black"
/> />
</div> </div>
@@ -155,7 +155,7 @@ export default function BikeForm({ onSuccess, onCancel }: BikeFormProps) {
setFormData({ ...formData, notes: e.target.value }) setFormData({ ...formData, notes: e.target.value })
} }
rows={3} rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-black"
/> />
</div> </div>

View File

@@ -105,7 +105,7 @@ export default function MaintenanceForm({
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, date: e.target.value }) 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" className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent text-black"
required required
/> />
{errors.date && ( {errors.date && (
@@ -124,7 +124,7 @@ export default function MaintenanceForm({
setFormData({ ...formData, mileage: Number(e.target.value) }) setFormData({ ...formData, mileage: Number(e.target.value) })
} }
min="0" 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" className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent text-black"
required required
/> />
{errors.mileage && ( {errors.mileage && (
@@ -143,7 +143,7 @@ export default function MaintenanceForm({
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, action: e.target.value }) 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" className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent text-black"
required required
> >
{maintenanceActions.map((action) => ( {maintenanceActions.map((action) => (
@@ -169,7 +169,7 @@ export default function MaintenanceForm({
setFormData({ ...formData, cost: e.target.value }) setFormData({ ...formData, cost: e.target.value })
} }
min="0" 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" className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent text-black"
/> />
</div> </div>
</div> </div>
@@ -184,7 +184,7 @@ export default function MaintenanceForm({
setFormData({ ...formData, notes: e.target.value }) setFormData({ ...formData, notes: e.target.value })
} }
rows={2} 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" className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent text-black"
/> />
</div> </div>

View File

@@ -135,7 +135,7 @@ export default function WearPartForm({
<select <select
value={formData.type} value={formData.type}
onChange={(e) => setFormData({ ...formData, type: e.target.value })} onChange={(e) => setFormData({ ...formData, type: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-black"
required required
> >
{wearPartTypes.map((type) => ( {wearPartTypes.map((type) => (
@@ -160,7 +160,7 @@ export default function WearPartForm({
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, brand: e.target.value }) setFormData({ ...formData, brand: e.target.value })
} }
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-black"
/> />
</div> </div>
@@ -174,7 +174,7 @@ export default function WearPartForm({
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, model: e.target.value }) setFormData({ ...formData, model: e.target.value })
} }
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-black"
/> />
</div> </div>
</div> </div>
@@ -190,7 +190,7 @@ export default function WearPartForm({
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, installDate: e.target.value }) setFormData({ ...formData, installDate: e.target.value })
} }
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-black"
required required
/> />
{errors.installDate && ( {errors.installDate && (
@@ -212,7 +212,7 @@ export default function WearPartForm({
}) })
} }
min="0" min="0"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-black"
required required
/> />
{errors.installMileage && ( {errors.installMileage && (
@@ -238,7 +238,7 @@ export default function WearPartForm({
}) })
} }
min="1" min="1"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-black"
required required
/> />
{errors.serviceInterval && ( {errors.serviceInterval && (
@@ -257,7 +257,7 @@ export default function WearPartForm({
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, status: e.target.value }) setFormData({ ...formData, status: e.target.value })
} }
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-black"
> >
{wearPartStatuses.map((status) => ( {wearPartStatuses.map((status) => (
<option key={status} value={status}> <option key={status} value={status}>
@@ -280,7 +280,7 @@ export default function WearPartForm({
setFormData({ ...formData, cost: e.target.value }) setFormData({ ...formData, cost: e.target.value })
} }
min="0" min="0"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-black"
/> />
</div> </div>
@@ -294,7 +294,7 @@ export default function WearPartForm({
setFormData({ ...formData, notes: e.target.value }) setFormData({ ...formData, notes: e.target.value })
} }
rows={3} rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-black"
/> />
</div> </div>

View File

@@ -72,22 +72,60 @@ export const bikeSchema = z.object({
export const wearPartSchema = z.object({ export const wearPartSchema = z.object({
bikeId: z.string().min(1, 'Fahrrad-ID ist erforderlich'), bikeId: z.string().min(1, 'Fahrrad-ID ist erforderlich'),
type: WearPartType, type: WearPartType,
brand: z.string().optional(), brand: z.string().optional().transform((val) => val?.trim() || undefined),
model: z.string().optional(), model: z.string().optional().transform((val) => val?.trim() || undefined),
installDate: z.string().datetime().or(z.date()), installDate: z
.union([z.date(), z.string()])
.transform((val) => {
if (!val) return new Date() // Default to today if not provided
if (val instanceof Date) {
return isNaN(val.getTime()) ? new Date() : val
}
if (typeof val === 'string') {
// Handle YYYY-MM-DD format (from HTML date input)
if (/^\d{4}-\d{2}-\d{2}$/.test(val)) {
const date = new Date(val + 'T00:00:00.000Z')
return isNaN(date.getTime()) ? new Date() : date
}
// Handle ISO datetime format or any other parseable date string
const date = new Date(val)
return isNaN(date.getTime()) ? new Date() : date
}
return new Date()
})
.pipe(z.date()),
installMileage: z.number().int().min(0).default(0), installMileage: z.number().int().min(0).default(0),
serviceInterval: z.number().int().min(1, 'Service-Intervall muss mindestens 1 km sein'), serviceInterval: z.number().int().min(1, 'Service-Intervall muss mindestens 1 km sein'),
status: WearPartStatus.default('ACTIVE'), status: WearPartStatus.default('ACTIVE'),
cost: z.number().positive().optional(), cost: z.number().positive().optional(),
notes: z.string().optional(), notes: z.string().optional().transform((val) => val?.trim() || undefined),
}) })
export const maintenanceHistorySchema = z.object({ export const maintenanceHistorySchema = z.object({
wearPartId: z.string().min(1, 'Verschleißteil-ID ist erforderlich'), wearPartId: z.string().min(1, 'Verschleißteil-ID ist erforderlich'),
date: z.string().datetime().or(z.date()), date: z
.union([z.date(), z.string()])
.transform((val) => {
if (!val) return new Date() // Default to today if not provided
if (val instanceof Date) {
return isNaN(val.getTime()) ? new Date() : val
}
if (typeof val === 'string') {
// Handle YYYY-MM-DD format (from HTML date input)
if (/^\d{4}-\d{2}-\d{2}$/.test(val)) {
const date = new Date(val + 'T00:00:00.000Z')
return isNaN(date.getTime()) ? new Date() : date
}
// Handle ISO datetime format or any other parseable date string
const date = new Date(val)
return isNaN(date.getTime()) ? new Date() : date
}
return new Date()
})
.pipe(z.date()),
mileage: z.number().int().min(0), mileage: z.number().int().min(0),
action: MaintenanceAction, action: MaintenanceAction,
notes: z.string().optional(), notes: z.string().optional().transform((val) => val?.trim() || undefined),
cost: z.number().positive().optional(), cost: z.number().positive().optional(),
}) })