Initial commit
This commit is contained in:
138
routers/calendar.py
Normal file
138
routers/calendar.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""Calendar: unified read-only month view of appointments, meetings, and tasks."""
|
||||
|
||||
from fastapi import APIRouter, Request, Depends
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import text
|
||||
from datetime import date, timedelta
|
||||
import calendar
|
||||
|
||||
from core.database import get_db
|
||||
from core.sidebar import get_sidebar_data
|
||||
|
||||
router = APIRouter(prefix="/calendar", tags=["calendar"])
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def calendar_view(
|
||||
request: Request,
|
||||
year: int = None,
|
||||
month: int = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
today = date.today()
|
||||
year = year or today.year
|
||||
month = month or today.month
|
||||
|
||||
# Clamp to valid range
|
||||
if month < 1 or month > 12:
|
||||
month = today.month
|
||||
if year < 2000 or year > 2100:
|
||||
year = today.year
|
||||
|
||||
first_day = date(year, month, 1)
|
||||
last_day = date(year, month, calendar.monthrange(year, month)[1])
|
||||
|
||||
# Prev/next month
|
||||
prev_month = first_day - timedelta(days=1)
|
||||
next_month = last_day + timedelta(days=1)
|
||||
|
||||
sidebar = await get_sidebar_data(db)
|
||||
|
||||
# Appointments in this month (by start_at)
|
||||
appt_result = await db.execute(text("""
|
||||
SELECT id, title, start_at, end_at, all_day, location
|
||||
FROM appointments
|
||||
WHERE is_deleted = false
|
||||
AND start_at::date >= :first AND start_at::date <= :last
|
||||
ORDER BY start_at
|
||||
"""), {"first": first_day, "last": last_day})
|
||||
appointments = [dict(r._mapping) for r in appt_result]
|
||||
|
||||
# Meetings in this month (by meeting_date)
|
||||
meet_result = await db.execute(text("""
|
||||
SELECT id, title, meeting_date, start_at, location, status
|
||||
FROM meetings
|
||||
WHERE is_deleted = false
|
||||
AND meeting_date >= :first AND meeting_date <= :last
|
||||
ORDER BY meeting_date, start_at
|
||||
"""), {"first": first_day, "last": last_day})
|
||||
meetings = [dict(r._mapping) for r in meet_result]
|
||||
|
||||
# Tasks with due dates in this month (open/in_progress only)
|
||||
task_result = await db.execute(text("""
|
||||
SELECT t.id, t.title, t.due_date, t.priority, t.status,
|
||||
p.name as project_name
|
||||
FROM tasks t
|
||||
LEFT JOIN projects p ON t.project_id = p.id
|
||||
WHERE t.is_deleted = false
|
||||
AND t.status IN ('open', 'in_progress')
|
||||
AND t.due_date >= :first AND t.due_date <= :last
|
||||
ORDER BY t.due_date, t.priority
|
||||
"""), {"first": first_day, "last": last_day})
|
||||
tasks = [dict(r._mapping) for r in task_result]
|
||||
|
||||
# Build day-indexed event map
|
||||
days_map = {}
|
||||
for a in appointments:
|
||||
day = a["start_at"].date().day if a["start_at"] else None
|
||||
if day:
|
||||
days_map.setdefault(day, []).append({
|
||||
"type": "appointment",
|
||||
"id": a["id"],
|
||||
"title": a["title"],
|
||||
"time": None if a["all_day"] else a["start_at"].strftime("%-I:%M %p"),
|
||||
"url": f"/appointments/{a['id']}",
|
||||
"all_day": a["all_day"],
|
||||
})
|
||||
|
||||
for m in meetings:
|
||||
day = m["meeting_date"].day if m["meeting_date"] else None
|
||||
if day:
|
||||
time_str = m["start_at"].strftime("%-I:%M %p") if m["start_at"] else None
|
||||
days_map.setdefault(day, []).append({
|
||||
"type": "meeting",
|
||||
"id": m["id"],
|
||||
"title": m["title"],
|
||||
"time": time_str,
|
||||
"url": f"/meetings/{m['id']}",
|
||||
"all_day": False,
|
||||
})
|
||||
|
||||
for t in tasks:
|
||||
day = t["due_date"].day if t["due_date"] else None
|
||||
if day:
|
||||
days_map.setdefault(day, []).append({
|
||||
"type": "task",
|
||||
"id": t["id"],
|
||||
"title": t["title"],
|
||||
"time": None,
|
||||
"url": f"/tasks/{t['id']}",
|
||||
"priority": t["priority"],
|
||||
"project_name": t.get("project_name"),
|
||||
"all_day": False,
|
||||
})
|
||||
|
||||
# Build calendar grid (weeks of days)
|
||||
# Monday=0, Sunday=6
|
||||
cal = calendar.Calendar(firstweekday=6) # Sunday start
|
||||
weeks = cal.monthdayscalendar(year, month)
|
||||
|
||||
return templates.TemplateResponse("calendar.html", {
|
||||
"request": request,
|
||||
"sidebar": sidebar,
|
||||
"year": year,
|
||||
"month": month,
|
||||
"month_name": calendar.month_name[month],
|
||||
"weeks": weeks,
|
||||
"days_map": days_map,
|
||||
"today": today,
|
||||
"first_day": first_day,
|
||||
"prev_year": prev_month.year,
|
||||
"prev_month": prev_month.month,
|
||||
"next_year": next_month.year,
|
||||
"next_month": next_month.month,
|
||||
"page_title": f"Calendar - {calendar.month_name[month]} {year}",
|
||||
"active_nav": "calendar",
|
||||
})
|
||||
Reference in New Issue
Block a user