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>
This commit is contained in:
Docker Config Backup
2025-10-13 07:45:06 +02:00
parent ffdfac65de
commit b38452413d
22 changed files with 2612 additions and 653 deletions

View File

@@ -23,6 +23,14 @@ export default function DataPreview({ data, loading, formData }: DataPreviewProp
setFillResult(null)
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/fill/journeybook`, {
method: 'POST',
headers: {
@@ -31,8 +39,8 @@ export default function DataPreview({ data, loading, formData }: DataPreviewProp
body: JSON.stringify({
username: formData.username,
password: formData.password,
start_date: formData.startDate,
end_date: formData.endDate,
start_date: formatLocalDate(formData.startDate),
end_date: formatLocalDate(formData.endDate),
start_km: parseInt(formData.startKm),
end_km: parseInt(formData.endKm),
vehicle_registration: formData.vehicleRegistration,
@@ -142,7 +150,7 @@ export default function DataPreview({ data, loading, formData }: DataPreviewProp
</table>
</div>
{formData && formData.startDate === '2025-01-01' && (
{formData && (
<div className="mt-4">
<Button
onClick={handleFillToWebsite}

View File

@@ -6,6 +6,7 @@ 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 {
@@ -18,8 +19,8 @@ export default function JourneyForm({ onDataCalculated, setLoading, onFormDataCh
const [formData, setFormData] = useState({
username: '',
password: '',
startDate: new Date().toISOString().slice(0, 10),
endDate: new Date().toISOString().slice(0, 10),
startDate: new Date(),
endDate: new Date(),
startKm: '',
endKm: '',
vehicleRegistration: '4SH1148',
@@ -31,9 +32,24 @@ export default function JourneyForm({ onDataCalculated, setLoading, onFormDataCh
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: {
@@ -42,8 +58,8 @@ export default function JourneyForm({ onDataCalculated, setLoading, onFormDataCh
body: JSON.stringify({
username: formData.username,
password: formData.password,
start_date: formData.startDate,
end_date: formData.endDate,
start_date: formatLocalDate(formData.startDate),
end_date: formatLocalDate(formData.endDate),
start_km: parseInt(formData.startKm),
end_km: parseInt(formData.endKm),
vehicle_registration: formData.vehicleRegistration,
@@ -71,6 +87,14 @@ export default function JourneyForm({ onDataCalculated, setLoading, onFormDataCh
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: {
@@ -79,8 +103,8 @@ export default function JourneyForm({ onDataCalculated, setLoading, onFormDataCh
body: JSON.stringify({
username: formData.username,
password: formData.password,
start_date: formData.startDate,
end_date: formData.endDate,
start_date: formatLocalDate(formData.startDate),
end_date: formatLocalDate(formData.endDate),
start_km: parseInt(formData.startKm),
end_km: parseInt(formData.endKm),
vehicle_registration: formData.vehicleRegistration,
@@ -94,7 +118,7 @@ export default function JourneyForm({ onDataCalculated, setLoading, onFormDataCh
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `journeybook_${formData.startDate}_${formData.endDate}.xlsx`
a.download = `journeybook_${formData.startDate.toISOString().slice(0, 10)}_${formData.endDate.toISOString().slice(0, 10)}.xlsx`
a.click()
} catch (err: any) {
setError(err.message)
@@ -138,23 +162,19 @@ export default function JourneyForm({ onDataCalculated, setLoading, onFormDataCh
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="startDate">Datum od</Label>
<Input
id="startDate"
type="date"
required
<DatePicker
value={formData.startDate}
onChange={(e) => setFormData({ ...formData, startDate: e.target.value })}
onChange={(date) => date && setFormData({ ...formData, startDate: date })}
placeholder="Vyberte datum od"
/>
</div>
<div className="space-y-2">
<Label htmlFor="endDate">Datum do</Label>
<Input
id="endDate"
type="date"
required
<DatePicker
value={formData.endDate}
onChange={(e) => setFormData({ ...formData, endDate: e.target.value })}
onChange={(date) => date && setFormData({ ...formData, endDate: date })}
placeholder="Vyberte datum do"
/>
</div>
</div>

View File

@@ -1,38 +1,62 @@
@import "tailwindcss";
@tailwind base;
@tailwind components;
@tailwind utilities;
@theme {
--color-background: 0 0% 100%;
--color-foreground: 222.2 84% 4.9%;
--color-card: 0 0% 100%;
--color-card-foreground: 222.2 84% 4.9%;
--color-popover: 0 0% 100%;
--color-popover-foreground: 222.2 84% 4.9%;
--color-primary: 221.2 83.2% 53.3%;
--color-primary-foreground: 210 40% 98%;
--color-secondary: 210 40% 96.1%;
--color-secondary-foreground: 222.2 47.4% 11.2%;
--color-muted: 210 40% 96.1%;
--color-muted-foreground: 215.4 16.3% 46.9%;
--color-accent: 210 40% 96.1%;
--color-accent-foreground: 222.2 47.4% 11.2%;
--color-destructive: 0 84.2% 60.2%;
--color-destructive-foreground: 210 40% 98%;
--color-border: 214.3 31.8% 91.4%;
--color-input: 214.3 31.8% 91.4%;
--color-ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
@layer base {
* {
border-color: hsl(var(--border));
}
body {
background: hsl(var(--color-background));
min-height: 100vh;
color: hsl(var(--color-foreground));
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
body {
background-color: hsl(var(--background));
color: hsl(var(--foreground));
font-feature-settings: "rlig" 1, "calt" 1;
}
}