Files
kniha_jizd_web/backend/api/main.py
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

246 lines
9.3 KiB
Python

from fastapi import FastAPI, HTTPException, Depends
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import Optional
import pandas as pd
from io import BytesIO
from fastapi.responses import StreamingResponse
import logging
from datetime import datetime
from dateutil.relativedelta import relativedelta
from scrapers import AttendanceScraper, JourneybookScraper
from calculators.kilometer_calculator import KilometerCalculator
from fillers import JourneybookFiller
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI(title="Kniha Jízd API", version="1.0.0")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
class ScrapeRequest(BaseModel):
username: str
password: str
month: str
vehicle_registration: str = "4SH1148"
class CalculateRequest(BaseModel):
username: str
password: str
start_date: str # Format: YYYY-MM-DD
end_date: str # Format: YYYY-MM-DD
start_km: int
end_km: int
vehicle_registration: str = "4SH1148"
variance: float = 0.1
class FillRequest(CalculateRequest):
dry_run: bool = True
@app.get("/health")
async def health_check():
return {"status": "healthy"}
@app.post("/api/scrape/attendance")
async def scrape_attendance(request: ScrapeRequest):
"""Scrape attendance data for a month"""
try:
scraper = AttendanceScraper(request.username, request.password)
attendance_dates = scraper.scrape_month(request.month)
return {
"month": request.month,
"sick_vacation_days": attendance_dates,
"count": len(attendance_dates)
}
except Exception as e:
logger.error(f"Attendance scraping failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/scrape/journeybook")
async def scrape_journeybook(request: ScrapeRequest):
"""Scrape journeybook data for a month"""
try:
scraper = JourneybookScraper(request.username, request.password, request.vehicle_registration)
df = scraper.scrape_month(request.month)
return {
"month": request.month,
"entries": df.to_dict(orient='records'),
"count": len(df)
}
except Exception as e:
logger.error(f"Journeybook scraping failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/calculate")
async def calculate_kilometers(request: CalculateRequest):
"""Scrape data, filter sick days, and recalculate kilometers"""
try:
logger.info(f"Calculate request: start_date={request.start_date}, end_date={request.end_date}")
attendance_scraper = AttendanceScraper(request.username, request.password)
journeybook_scraper = JourneybookScraper(request.username, request.password, request.vehicle_registration)
# Get all months in the date range
start = datetime.strptime(request.start_date, "%Y-%m-%d")
end = datetime.strptime(request.end_date, "%Y-%m-%d")
logger.info(f"Parsed dates: start={start}, end={end}")
# Collect data from all months
all_attendance_dates = []
all_dfs = []
current = start
while current <= end:
month_str = current.strftime("%Y-%m")
logger.info(f"Scraping attendance for {month_str}")
attendance_dates = attendance_scraper.scrape_month(month_str)
all_attendance_dates.extend(attendance_dates)
logger.info(f"Scraping journeybook for {month_str}")
df_month = journeybook_scraper.scrape_month(month_str)
all_dfs.append(df_month)
current = current + relativedelta(months=1)
# Combine all months
df = pd.concat(all_dfs, ignore_index=True) if all_dfs else pd.DataFrame()
# Filter by actual date range, but preserve refueling rows (empty Datum)
if not df.empty:
date_parsed = pd.to_datetime(df["Datum"], format="%d.%m.%Y", errors='coerce')
is_refuel = df["Datum"].isna() | (df["Datum"] == "")
is_in_range = (date_parsed >= start) & (date_parsed <= end)
df = df[is_in_range | is_refuel]
logger.info(f"Filtering out {len(all_attendance_dates)} sick/vacation days")
# Only filter journey rows, not refueling rows
is_refuel = df["Datum"].isna() | (df["Datum"] == "")
df = df[~df["Datum"].isin(all_attendance_dates) | is_refuel]
logger.info("Recalculating kilometers")
df = KilometerCalculator.recalculate(df, request.start_km, request.end_km, request.variance)
return {
"month": f"{request.start_date} - {request.end_date}",
"start_km": request.start_km,
"end_km": request.end_km,
"filtered_days": len(all_attendance_dates),
"entries": df.to_dict(orient='records'),
"total_entries": len(df)
}
except Exception as e:
import traceback
logger.error(f"Calculate failed: {e}")
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/export/excel")
async def export_to_excel(request: CalculateRequest):
"""Generate and download Excel file"""
try:
attendance_scraper = AttendanceScraper(request.username, request.password)
journeybook_scraper = JourneybookScraper(request.username, request.password, request.vehicle_registration)
# Get all months in the date range
start = datetime.strptime(request.start_date, "%Y-%m-%d")
end = datetime.strptime(request.end_date, "%Y-%m-%d")
# Collect data from all months
all_attendance_dates = []
all_dfs = []
current = start
while current <= end:
month_str = current.strftime("%Y-%m")
attendance_dates = attendance_scraper.scrape_month(month_str)
all_attendance_dates.extend(attendance_dates)
df_month = journeybook_scraper.scrape_month(month_str)
all_dfs.append(df_month)
current = current + relativedelta(months=1)
# Combine all months
df = pd.concat(all_dfs, ignore_index=True) if all_dfs else pd.DataFrame()
# Filter by actual date range, but preserve refueling rows (empty Datum)
if not df.empty:
date_parsed = pd.to_datetime(df["Datum"], format="%d.%m.%Y", errors='coerce')
is_refuel = df["Datum"].isna() | (df["Datum"] == "")
is_in_range = (date_parsed >= start) & (date_parsed <= end)
df = df[is_in_range | is_refuel]
# Only filter journey rows, not refueling rows
is_refuel = df["Datum"].isna() | (df["Datum"] == "")
df = df[~df["Datum"].isin(all_attendance_dates) | is_refuel]
df = KilometerCalculator.recalculate(df, request.start_km, request.end_km, request.variance)
output = BytesIO()
df.to_excel(output, index=False, engine='openpyxl')
output.seek(0)
filename = f"journeybook_{request.start_date}_{request.end_date}.xlsx"
return StreamingResponse(
output,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f"attachment; filename={filename}"}
)
except Exception as e:
logger.error(f"Export failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/fill/journeybook")
async def fill_journeybook(request: FillRequest):
"""Fill calculated data back to kj.colsys.cz (restricted to January 2025)"""
try:
logger.info(f"Fill request received with dry_run={request.dry_run}")
# Only allow January 2025 for testing
if request.start_date != "2025-01-01" or request.end_date < "2025-01-31":
raise HTTPException(status_code=400, detail="Only January 2025 is allowed for testing")
attendance_scraper = AttendanceScraper(request.username, request.password)
journeybook_scraper = JourneybookScraper(request.username, request.password, request.vehicle_registration)
# Get January data
month_str = "2025-01"
attendance_dates = attendance_scraper.scrape_month(month_str)
df = journeybook_scraper.scrape_month(month_str)
# Filter sick/vacation days
df = df[~df["Datum"].isin(attendance_dates)]
# Recalculate kilometers
df = KilometerCalculator.recalculate(df, request.start_km, request.end_km, request.variance)
# Fill data back (supports dry_run mode)
filler = JourneybookFiller(request.username, request.password, request.vehicle_registration)
result = filler.fill_month(df, month_str, dry_run=request.dry_run)
return result
except Exception as e:
import traceback
logger.error(f"Fill failed: {e}")
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=str(e))
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)