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>
216 lines
9.7 KiB
TypeScript
216 lines
9.7 KiB
TypeScript
'use client'
|
||
|
||
import { useState } from 'react'
|
||
import API_URL from '@/lib/api'
|
||
|
||
interface DataPreviewProps {
|
||
data: any
|
||
loading: boolean
|
||
formData: any
|
||
}
|
||
|
||
export default function DataPreview({ data, loading, formData }: DataPreviewProps) {
|
||
const [filling, setFilling] = useState(false)
|
||
const [fillResult, setFillResult] = useState<any>(null)
|
||
|
||
const handleFillToWebsite = async () => {
|
||
if (!formData) return
|
||
|
||
setFilling(true)
|
||
setFillResult(null)
|
||
|
||
try {
|
||
const response = await fetch(`${API_URL}/api/fill/journeybook`, {
|
||
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),
|
||
dry_run: false
|
||
}),
|
||
})
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.json()
|
||
throw new Error(errorData.detail || 'Chyba při vyplňování dat')
|
||
}
|
||
|
||
const result = await response.json()
|
||
setFillResult(result)
|
||
} catch (err: any) {
|
||
alert('Chyba: ' + err.message)
|
||
} finally {
|
||
setFilling(false)
|
||
}
|
||
}
|
||
if (loading) {
|
||
return (
|
||
<div className="bg-white/98 backdrop-blur-lg rounded-3xl shadow-2xl border border-white/30 overflow-hidden">
|
||
<div className="flex flex-col items-center justify-center h-96 p-8">
|
||
<div className="relative">
|
||
<div className="animate-spin rounded-full h-20 w-20 border-4 border-blue-200"></div>
|
||
<div className="animate-spin rounded-full h-20 w-20 border-t-4 border-blue-600 absolute top-0"></div>
|
||
</div>
|
||
<p className="mt-6 text-gray-700 font-semibold text-lg">Načítání dat...</p>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (!data) {
|
||
return (
|
||
<div className="bg-white/98 backdrop-blur-lg rounded-3xl shadow-2xl border border-white/30 overflow-hidden">
|
||
<div className="bg-gradient-to-r from-purple-50 to-purple-100 px-8 py-6 border-b border-purple-200">
|
||
<div className="flex items-center gap-4">
|
||
<div className="w-12 h-12 bg-gradient-to-br from-purple-500 to-purple-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="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||
</svg>
|
||
</div>
|
||
<h2 className="text-3xl font-bold text-gray-800">Náhled dat</h2>
|
||
</div>
|
||
</div>
|
||
<div className="p-8">
|
||
<div className="flex flex-col items-center justify-center py-16 text-center">
|
||
<div className="w-20 h-20 bg-gray-100 rounded-full flex items-center justify-center mb-4">
|
||
<svg className="w-10 h-10 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||
</svg>
|
||
</div>
|
||
<p className="text-gray-500 font-medium">
|
||
Vyplňte formulář a klikněte na "Vypočítat"
|
||
</p>
|
||
<p className="text-gray-400 text-sm mt-1">
|
||
Data se zobrazí zde
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="bg-white/98 backdrop-blur-lg rounded-3xl shadow-2xl border border-white/30 overflow-hidden">
|
||
<div className="bg-gradient-to-r from-purple-50 to-purple-100 px-8 py-6 border-b border-purple-200">
|
||
<div className="flex items-center gap-4">
|
||
<div className="w-12 h-12 bg-gradient-to-br from-purple-500 to-purple-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="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||
</svg>
|
||
</div>
|
||
<h2 className="text-3xl font-bold text-gray-800">Náhled dat</h2>
|
||
</div>
|
||
</div>
|
||
<div className="p-8">
|
||
|
||
<div className="mb-6 grid grid-cols-2 gap-3 bg-gradient-to-br from-blue-50 to-indigo-50 p-5 rounded-xl border border-blue-100">
|
||
<div>
|
||
<p className="text-sm text-gray-600">Měsíc</p>
|
||
<p className="font-semibold text-lg">{data.month}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-sm text-gray-600">Celkem záznamů</p>
|
||
<p className="font-semibold text-lg">{data.total_entries}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-sm text-gray-600">Počáteční km</p>
|
||
<p className="font-semibold text-lg">{data.start_km.toLocaleString()}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-sm text-gray-600">Koncový km</p>
|
||
<p className="font-semibold text-lg">{data.end_km.toLocaleString()}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-sm text-gray-600">Celkem ujeto</p>
|
||
<p className="font-semibold text-lg">{(data.end_km - data.start_km).toLocaleString()} km</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-sm text-gray-600">Filtrováno dnů</p>
|
||
<p className="font-semibold text-lg">{data.filtered_days}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="overflow-auto max-h-96 rounded-xl border border-gray-200">
|
||
<table className="w-full text-sm">
|
||
<thead className="bg-gradient-to-r from-gray-50 to-gray-100 sticky top-0">
|
||
<tr>
|
||
{data.entries.length > 0 && Object.keys(data.entries[0]).map((key: string) => (
|
||
<th key={key} className="px-4 py-3 text-left font-bold text-gray-700 border-b-2 border-gray-300 whitespace-nowrap">
|
||
{key}
|
||
</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-gray-100 bg-white">
|
||
{data.entries.map((entry: any, index: number) => (
|
||
<tr key={index} className="hover:bg-blue-50 transition-colors">
|
||
{Object.keys(entry).map((key: string) => (
|
||
<td key={key} className="px-4 py-3 text-right text-gray-600">
|
||
{entry[key] !== null && entry[key] !== undefined ? entry[key] : '-'}
|
||
</td>
|
||
))}
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{formData && formData.startDate === '2025-01-01' && (
|
||
<div className="mt-6">
|
||
<button
|
||
onClick={handleFillToWebsite}
|
||
disabled={filling}
|
||
className="w-full bg-gradient-to-r from-orange-600 to-orange-700 hover:from-orange-700 hover:to-orange-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 disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
<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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
||
</svg>
|
||
<span className="text-lg">{filling ? 'Vyplňování...' : 'Vyplnit na web'}</span>
|
||
</span>
|
||
</button>
|
||
|
||
{fillResult && (
|
||
<div className={`mt-4 ${fillResult.dry_run ? 'bg-blue-50 border-blue-200' : 'bg-green-50 border-green-200'} border rounded-xl p-4`}>
|
||
<h3 className={`font-bold ${fillResult.dry_run ? 'text-blue-900' : 'text-green-900'} mb-2`}>
|
||
{fillResult.dry_run ? 'Výsledek DRY RUN:' : 'Výsledek vyplňování:'}
|
||
</h3>
|
||
<div className={`text-sm ${fillResult.dry_run ? 'text-blue-800' : 'text-green-800'}`}>
|
||
<p>Měsíc: {fillResult.month}</p>
|
||
<p>Celkem řádků: {fillResult.total_rows}</p>
|
||
{fillResult.dry_run ? (
|
||
<p>Připraveno aktualizací: {fillResult.updates_prepared}</p>
|
||
) : (
|
||
<>
|
||
<p>Úspěšně vyplněno: {fillResult.successful}</p>
|
||
<p>Chyby: {fillResult.failed}</p>
|
||
</>
|
||
)}
|
||
{fillResult.dry_run && (
|
||
<p className="mt-2 font-semibold text-orange-700">
|
||
⚠️ DRY RUN MODE - Data nebyla odeslána na web
|
||
</p>
|
||
)}
|
||
{!fillResult.dry_run && fillResult.successful > 0 && (
|
||
<p className="mt-2 font-semibold text-green-700">
|
||
✅ Data byla úspěšně vyplněna. Nyní zkontrolujte na webu a klikněte "Uzavřít měsíc" ručně.
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|