139 lines
4.7 KiB
Python
139 lines
4.7 KiB
Python
"""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",
|
|
})
|