← back to Norma

app/api/grants/calendar/route.ts

186 lines

import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { getOrgId } from '@/lib/orgId';

interface CalendarEvent {
  id: string;
  grant_id: string;
  title: string;
  funder: string;
  date: string;
  status: string;
  priority: string;
  type: 'deadline' | 'notification' | 'period_start' | 'period_end';
  color: string;
}

/**
 * Assign a color based on priority, status, and event type.
 */
function getEventColor(priority: string, status: string, type: string): string {
  // Status-based overrides
  if (status === 'awarded') return '#22c55e';    // green
  if (status === 'declined') return '#9ca3af';    // gray
  if (status === 'expired') return '#9ca3af';     // gray
  if (status === 'submitted') return '#8b5cf6';   // purple

  // Type-based for non-deadline events
  if (type === 'period_start') return '#06b6d4';  // cyan
  if (type === 'period_end') return '#f97316';     // orange
  if (type === 'notification') return '#eab308';   // yellow

  // Priority-based for deadlines
  switch (priority) {
    case 'high': return '#ef4444';     // red
    case 'medium': return '#3b82f6';   // blue
    case 'low': return '#6b7280';      // gray
    default: return '#3b82f6';         // blue
  }
}

/**
 * GET /api/grants/calendar
 * Return grants as calendar events.
 * Filters: ?month= (YYYY-MM), ?status=
 * Returns { events: [...] } where each event has id, title, funder, date, status,
 * priority, type, color. Multiple events per grant if it has notification_date,
 * grant_period_start, grant_period_end.
 */
export async function GET(request: NextRequest) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;
  const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;

  try {
    const { searchParams } = new URL(request.url);
    const month = searchParams.get('month');   // YYYY-MM
    const status = searchParams.get('status');

    const conditions: string[] = [];
    const values: unknown[] = [];
    let paramIndex = 1;

    if (orgId) {
      conditions.push(`org_id = $${paramIndex}`);
      values.push(orgId);
      paramIndex++;
    }

    if (status) {
      conditions.push(`status = $${paramIndex}`);
      values.push(status);
      paramIndex++;
    }

    if (month) {
      // Parse YYYY-MM to start and end of month
      if (!/^\d{4}-\d{2}$/.test(month)) {
        return NextResponse.json({ error: 'Invalid month format. Use YYYY-MM.' }, { status: 400 });
      }
      const [year, mon] = month.split('-').map(Number);
      if (!year || !mon || mon < 1 || mon > 12) {
        return NextResponse.json({ error: 'Invalid month format. Use YYYY-MM.' }, { status: 400 });
      }
      const monthStart = `${year}-${String(mon).padStart(2, '0')}-01`;
      const nextMon = mon === 12 ? 1 : mon + 1;
      const nextYear = mon === 12 ? year + 1 : year;
      const monthEnd = `${nextYear}-${String(nextMon).padStart(2, '0')}-01`;

      // Include grants where any of their dates fall within the month
      conditions.push(
        `(
          (deadline >= $${paramIndex} AND deadline < $${paramIndex + 1})
          OR (notification_date >= $${paramIndex} AND notification_date < $${paramIndex + 1})
          OR (grant_period_start >= $${paramIndex}::date AND grant_period_start < $${paramIndex + 1}::date)
          OR (grant_period_end >= $${paramIndex}::date AND grant_period_end < $${paramIndex + 1}::date)
        )`
      );
      values.push(monthStart, monthEnd);
      paramIndex += 2;
    } else {
      // Only return grants that have at least one date set
      conditions.push(
        `(deadline IS NOT NULL OR notification_date IS NOT NULL OR grant_period_start IS NOT NULL OR grant_period_end IS NOT NULL)`
      );
    }

    const whereClause =
      conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';

    const result = await query(
      `SELECT * FROM grants ${whereClause} ORDER BY deadline ASC NULLS LAST`,
      values
    );

    // Build calendar events -- one per date type per grant
    const events: CalendarEvent[] = [];

    for (const grant of result.rows) {
      if (grant.deadline) {
        events.push({
          id: `${grant.id}-deadline`,
          grant_id: grant.id,
          title: `${grant.title} — Deadline`,
          funder: grant.funder,
          date: grant.deadline,
          status: grant.status,
          priority: grant.priority,
          type: 'deadline',
          color: getEventColor(grant.priority, grant.status, 'deadline'),
        });
      }

      if (grant.notification_date) {
        events.push({
          id: `${grant.id}-notification`,
          grant_id: grant.id,
          title: `${grant.title} — Notification`,
          funder: grant.funder,
          date: grant.notification_date,
          status: grant.status,
          priority: grant.priority,
          type: 'notification',
          color: getEventColor(grant.priority, grant.status, 'notification'),
        });
      }

      if (grant.grant_period_start) {
        events.push({
          id: `${grant.id}-period_start`,
          grant_id: grant.id,
          title: `${grant.title} — Period Start`,
          funder: grant.funder,
          date: grant.grant_period_start,
          status: grant.status,
          priority: grant.priority,
          type: 'period_start',
          color: getEventColor(grant.priority, grant.status, 'period_start'),
        });
      }

      if (grant.grant_period_end) {
        events.push({
          id: `${grant.id}-period_end`,
          grant_id: grant.id,
          title: `${grant.title} — Period End`,
          funder: grant.funder,
          date: grant.grant_period_end,
          status: grant.status,
          priority: grant.priority,
          type: 'period_end',
          color: getEventColor(grant.priority, grant.status, 'period_end'),
        });
      }
    }

    // Sort events by date
    events.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());

    return NextResponse.json({ events });
  } catch (err) {
    console.error('[api/grants/calendar] GET error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to fetch grant calendar events' }, { status: 500 });
  }
}