Files
kniha_jizd_web/frontend/app/components/DataPreview.tsx
Docker Config Backup 410d5092ff feat: Integrate shadcn/ui design system
- Install shadcn/ui dependencies (CVA, clsx, tailwind-merge, lucide-react)
- Install Radix UI primitives (slot, label)
- Create utility helper (lib/utils.ts with cn function)
- Update Tailwind config for shadcn/ui theme support
- Add CSS variables for theming in globals.css (Tailwind v4 syntax)
- Add shadcn/ui components: Button, Card, Input, Label
- Update JourneyForm with shadcn/ui components and icons
- Update DataPreview with shadcn/ui components and icons
- Improve UI consistency and accessibility

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-10 20:41:06 +02:00

249 lines
10 KiB
TypeScript

'use client'
import { useState } from 'react'
import API_URL from '@/lib/api'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Upload, BarChart3, Loader2, FileText, CheckCircle2, AlertCircle } from 'lucide-react'
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 (
<Card className="bg-white/98 backdrop-blur-lg border-white/30">
<CardContent className="flex flex-col items-center justify-center h-96 p-8">
<Loader2 className="h-20 w-20 text-blue-600 animate-spin" />
<p className="mt-6 text-gray-700 font-semibold text-lg">Načítání dat...</p>
</CardContent>
</Card>
)
}
if (!data) {
return (
<Card className="bg-white/98 backdrop-blur-lg border-white/30">
<CardHeader className="bg-gradient-to-r from-purple-50 to-purple-100 border-b border-purple-200">
<CardTitle className="text-3xl font-bold text-gray-800 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">
<BarChart3 className="w-7 h-7 text-white" />
</div>
<span>Náhled dat</span>
</CardTitle>
</CardHeader>
<CardContent 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">
<FileText className="w-10 h-10 text-gray-400" />
</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>
</CardContent>
</Card>
)
}
return (
<Card className="bg-white/98 backdrop-blur-lg border-white/30">
<CardHeader className="bg-gradient-to-r from-purple-50 to-purple-100 border-b border-purple-200">
<CardTitle className="text-3xl font-bold text-gray-800 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">
<BarChart3 className="w-7 h-7 text-white" />
</div>
<span>Náhled dat</span>
</CardTitle>
</CardHeader>
<CardContent 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}
size="lg"
className="w-full bg-gradient-to-r from-orange-600 to-orange-700 hover:from-orange-700 hover:to-orange-800 text-lg h-12"
>
{filling ? (
<>
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
Vyplňování...
</>
) : (
<>
<Upload className="mr-2 h-5 w-5" />
Vyplnit na web
</>
)}
</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 flex items-center gap-2`}>
{fillResult.dry_run ? (
<>
<AlertCircle className="h-5 w-5" />
Výsledek DRY RUN:
</>
) : (
<>
<CheckCircle2 className="h-5 w-5" />
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>
{fillResult.dry_run ? (
<>
<p>Připraveno aktualizací: {fillResult.updates_prepared}</p>
<p>Připraveno smazání: {fillResult.deletes_prepared}</p>
</>
) : (
<>
<p>Celkem aktualizací: {fillResult.updates_total}</p>
<p>Úspěšně vyplněno: {fillResult.updates_successful}</p>
<p>Chyby při vyplňování: {fillResult.updates_failed}</p>
{fillResult.deletes_total > 0 && (
<>
<p className="mt-2">Celkem smazání: {fillResult.deletes_total}</p>
<p>Úspěšně smazáno: {fillResult.deletes_successful}</p>
<p>Chyby při mazání: {fillResult.deletes_failed}</p>
</>
)}
</>
)}
{fillResult.dry_run && (
<p className="mt-2 font-semibold text-orange-700 flex items-center gap-2">
<AlertCircle className="h-4 w-4" />
DRY RUN MODE - Data nebyla odeslána na web
</p>
)}
{!fillResult.dry_run && fillResult.updates_successful > 0 && (
<p className="mt-2 font-semibold text-green-700 flex items-center gap-2">
<CheckCircle2 className="h-4 w-4" />
Data byla úspěšně vyplněna. Nyní zkontrolujte na webu a klikněte "Uzavřít měsíc" ručně.
</p>
)}
{!fillResult.dry_run && fillResult.errors && fillResult.errors.length > 0 && (
<div className="mt-2 p-2 bg-red-100 border border-red-300 rounded">
<p className="font-semibold text-red-900 flex items-center gap-2">
<AlertCircle className="h-4 w-4" />
Chyby:
</p>
{fillResult.errors.map((err: any, idx: number) => (
<p key={idx} className="text-xs text-red-800"> {err.type} řádek {err.row}: {err.error}</p>
))}
</div>
)}
</div>
</div>
)}
</div>
)}
</CardContent>
</Card>
)
}