Features: - FastAPI backend for scraping attendance and journey book data - Deterministic kilometer distribution with random variance - Refueling form filling with km values - Next.js frontend with date range selector - Docker deployment setup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
270 lines
11 KiB
TypeScript
270 lines
11 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import API_URL from '@/lib/api'
|
|
|
|
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().toISOString().slice(0, 10),
|
|
endDate: new Date().toISOString().slice(0, 10),
|
|
startKm: '',
|
|
endKm: '',
|
|
vehicleRegistration: '4SH1148',
|
|
variance: '0.1'
|
|
})
|
|
|
|
const [error, setError] = useState('')
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
setError('')
|
|
setLoading(true)
|
|
|
|
try {
|
|
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: formData.startDate,
|
|
end_date: 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 {
|
|
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: formData.startDate,
|
|
end_date: 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}_${formData.endDate}.xlsx`
|
|
a.click()
|
|
} catch (err: any) {
|
|
setError(err.message)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="bg-white/98 backdrop-blur-lg rounded-3xl shadow-2xl overflow-hidden border border-white/30">
|
|
<div className="bg-gradient-to-r from-blue-50 to-blue-100 px-8 py-6 border-b border-blue-200">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center shadow-lg">
|
|
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
|
</svg>
|
|
</div>
|
|
<h2 className="text-3xl font-bold text-gray-800">Vstupní údaje</h2>
|
|
</div>
|
|
</div>
|
|
<div className="p-8">
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-5">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Uživatelské jméno
|
|
</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={formData.username}
|
|
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
|
|
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
|
placeholder="Zadejte uživatelské jméno"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Heslo
|
|
</label>
|
|
<input
|
|
type="password"
|
|
required
|
|
value={formData.password}
|
|
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
|
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
|
placeholder="Zadejte heslo"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Datum od
|
|
</label>
|
|
<input
|
|
type="date"
|
|
required
|
|
value={formData.startDate}
|
|
onChange={(e) => setFormData({ ...formData, startDate: e.target.value })}
|
|
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Datum do
|
|
</label>
|
|
<input
|
|
type="date"
|
|
required
|
|
value={formData.endDate}
|
|
onChange={(e) => setFormData({ ...formData, endDate: e.target.value })}
|
|
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Počáteční stav [km]
|
|
</label>
|
|
<input
|
|
type="number"
|
|
required
|
|
value={formData.startKm}
|
|
onChange={(e) => setFormData({ ...formData, startKm: e.target.value })}
|
|
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Koncový stav [km]
|
|
</label>
|
|
<input
|
|
type="number"
|
|
required
|
|
value={formData.endKm}
|
|
onChange={(e) => setFormData({ ...formData, endKm: e.target.value })}
|
|
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
SPZ vozidla
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.vehicleRegistration}
|
|
onChange={(e) => setFormData({ ...formData, vehicleRegistration: e.target.value })}
|
|
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Variance (0-1)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
min="0"
|
|
max="1"
|
|
value={formData.variance}
|
|
onChange={(e) => setFormData({ ...formData, variance: e.target.value })}
|
|
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
|
/>
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
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-4 pt-6">
|
|
<button
|
|
type="submit"
|
|
className="flex-1 bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white font-bold py-4 px-6 rounded-xl shadow-lg hover:shadow-2xl transform hover:-translate-y-1 transition-all duration-200"
|
|
>
|
|
<span className="flex items-center justify-center gap-3">
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
|
</svg>
|
|
<span className="text-lg">Vypočítat</span>
|
|
</span>
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={handleExport}
|
|
className="flex-1 bg-gradient-to-r from-green-600 to-green-700 hover:from-green-700 hover:to-green-800 text-white font-bold py-4 px-6 rounded-xl shadow-lg hover:shadow-2xl transform hover:-translate-y-1 transition-all duration-200"
|
|
>
|
|
<span className="flex items-center justify-center gap-3">
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
<span className="text-lg">Export Excel</span>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|