Files
kniha_jizd_web/frontend/app/components/JourneyForm.tsx
Docker Config Backup b38452413d Final working solution: shadcn date picker with timezone fix
- Implemented shadcn/ui date picker with Czech localization
- Added month/year dropdown navigation for easy date selection
- Fixed critical timezone bug causing "No valid days found" error
  - Changed from toISOString() to local date formatting
  - Dates now correctly sent as 2025-01-01 instead of 2024-12-31
- Calendar auto-closes after date selection
- All features tested and working:
  - Journey calculation with correct date ranges
  - "Vyplnit na web" button visible and functional
  - Excel export working
  - Backend successfully processes January 2025 data

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-13 07:45:06 +02:00

265 lines
8.6 KiB
TypeScript

'use client'
import { useState } from 'react'
import API_URL from '@/lib/api'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { DatePicker } from '@/components/ui/date-picker'
import { Calculator, Download } from 'lucide-react'
interface JourneyFormProps {
onDataCalculated: (data: any) => void
setLoading: (loading: boolean) => void
onFormDataChange: (formData: any) => void
}
export default function JourneyForm({ onDataCalculated, setLoading, onFormDataChange }: JourneyFormProps) {
const [formData, setFormData] = useState({
username: '',
password: '',
startDate: new Date(),
endDate: new Date(),
startKm: '',
endKm: '',
vehicleRegistration: '4SH1148',
variance: '0.1'
})
const [error, setError] = useState('')
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError('')
// Validate date range
if (formData.startDate > formData.endDate) {
setError('Datum od musí být před nebo stejný jako Datum do')
return
}
setLoading(true)
try {
// Format dates as YYYY-MM-DD in local timezone
const formatLocalDate = (date: Date) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
const response = await fetch(`${API_URL}/api/calculate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: formData.username,
password: formData.password,
start_date: formatLocalDate(formData.startDate),
end_date: formatLocalDate(formData.endDate),
start_km: parseInt(formData.startKm),
end_km: parseInt(formData.endKm),
vehicle_registration: formData.vehicleRegistration,
variance: parseFloat(formData.variance)
}),
})
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.detail || 'Chyba při zpracování dat')
}
const data = await response.json()
onDataCalculated(data)
onFormDataChange(formData)
} catch (err: any) {
setError(err.message)
onDataCalculated(null)
onFormDataChange(null)
} finally {
setLoading(false)
}
}
const handleExport = async () => {
setLoading(true)
try {
// Format dates as YYYY-MM-DD in local timezone
const formatLocalDate = (date: Date) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
const response = await fetch(`${API_URL}/api/export/excel`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: formData.username,
password: formData.password,
start_date: formatLocalDate(formData.startDate),
end_date: formatLocalDate(formData.endDate),
start_km: parseInt(formData.startKm),
end_km: parseInt(formData.endKm),
vehicle_registration: formData.vehicleRegistration,
variance: parseFloat(formData.variance)
}),
})
if (!response.ok) throw new Error('Export failed')
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `journeybook_${formData.startDate.toISOString().slice(0, 10)}_${formData.endDate.toISOString().slice(0, 10)}.xlsx`
a.click()
} catch (err: any) {
setError(err.message)
} finally {
setLoading(false)
}
}
return (
<Card>
<CardHeader>
<CardTitle>Vstupní údaje</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="username">Uživatelské jméno</Label>
<Input
id="username"
type="text"
required
value={formData.username}
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
placeholder="Zadejte uživatelské jméno"
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">Heslo</Label>
<Input
id="password"
type="password"
required
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
placeholder="Zadejte heslo"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="startDate">Datum od</Label>
<DatePicker
value={formData.startDate}
onChange={(date) => date && setFormData({ ...formData, startDate: date })}
placeholder="Vyberte datum od"
/>
</div>
<div className="space-y-2">
<Label htmlFor="endDate">Datum do</Label>
<DatePicker
value={formData.endDate}
onChange={(date) => date && setFormData({ ...formData, endDate: date })}
placeholder="Vyberte datum do"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="startKm">Počáteční stav [km]</Label>
<Input
id="startKm"
type="number"
required
value={formData.startKm}
onChange={(e) => setFormData({ ...formData, startKm: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="endKm">Koncový stav [km]</Label>
<Input
id="endKm"
type="number"
required
value={formData.endKm}
onChange={(e) => setFormData({ ...formData, endKm: e.target.value })}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="vehicleRegistration">SPZ vozidla</Label>
<Input
id="vehicleRegistration"
type="text"
value={formData.vehicleRegistration}
onChange={(e) => setFormData({ ...formData, vehicleRegistration: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="variance">Variance (0-1)</Label>
<Input
id="variance"
type="number"
step="0.01"
min="0"
max="1"
value={formData.variance}
onChange={(e) => setFormData({ ...formData, variance: e.target.value })}
/>
<p className="text-xs text-muted-foreground">
Náhodná variace rozdělení kilometrů (doporučeno 0.1 = 10%)
</p>
</div>
{error && (
<div className="bg-red-50 border-l-4 border-red-500 text-red-700 px-4 py-3 rounded-lg shadow-md flex items-start gap-3">
<svg className="w-5 h-5 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
<span>{error}</span>
</div>
)}
<div className="flex flex-col sm:flex-row gap-3 pt-4">
<Button
type="submit"
className="flex-1"
>
<Calculator className="mr-2 h-4 w-4" />
Vypočítat
</Button>
<Button
type="button"
onClick={handleExport}
variant="outline"
className="flex-1"
>
<Download className="mr-2 h-4 w-4" />
Export Excel
</Button>
</div>
</form>
</CardContent>
</Card>
)
}