import { NextRequest, NextResponse } from 'next/server'; import { Pool } from 'pg'; import { TimeEntry } from '@/lib/types/database'; // Initialize PostgreSQL connection const pool = new Pool({ host: process.env.POSTGRES_HOST, port: parseInt(process.env.POSTGRES_PORT || '5432'), database: process.env.POSTGRES_DB || 'pulse_autotask', user: process.env.POSTGRES_USER, password: process.env.POSTGRES_PASSWORD, ssl: process.env.POSTGRES_SSL === 'true' ? { rejectUnauthorized: false } : false, }); export async function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url); // Parse query parameters const limit = parseInt(searchParams.get('limit') || '100'); const offset = parseInt(searchParams.get('offset') || '0'); const search = searchParams.get('search') || ''; const resourceId = searchParams.get('resource_id'); const ticketId = searchParams.get('ticket_id'); const taskId = searchParams.get('task_id'); const projectId = searchParams.get('project_id'); const companyId = searchParams.get('company_id'); const startDate = searchParams.get('start_date'); const endDate = searchParams.get('end_date'); const sortBy = searchParams.get('sort_by') || 'entry_date'; const sortOrder = searchParams.get('sort_order') || 'desc'; const minHours = searchParams.get('min_hours'); const maxHours = searchParams.get('max_hours'); const billable = searchParams.get('billable'); const approved = searchParams.get('approved'); const hasTicket = searchParams.get('has_ticket'); // Build WHERE conditions const conditions: string[] = ['te.is_deleted = false']; const params: any[] = []; let paramIndex = 1; // Add search condition (search in notes, title, internal_notes) if (search) { conditions.push(`( te.notes ILIKE $${paramIndex} OR te.title ILIKE $${paramIndex} OR te.internal_notes ILIKE $${paramIndex} )`); params.push(`%${search}%`); paramIndex++; } // Add filter conditions if (resourceId) { conditions.push(`te.resource_id = $${paramIndex}`); params.push(resourceId); paramIndex++; } if (ticketId) { conditions.push(`te.ticket_id = $${paramIndex}`); params.push(ticketId); paramIndex++; } if (taskId) { conditions.push(`te.task_id = $${paramIndex}`); params.push(taskId); paramIndex++; } if (projectId) { conditions.push(`te.project_id = $${paramIndex}`); params.push(projectId); paramIndex++; } if (companyId) { conditions.push(`te.company_id = $${paramIndex}`); params.push(companyId); paramIndex++; } if (startDate) { conditions.push(`te.entry_date >= $${paramIndex}`); params.push(startDate); paramIndex++; } if (endDate) { conditions.push(`te.entry_date <= $${paramIndex}`); params.push(endDate); paramIndex++; } if (minHours) { conditions.push(`te.hours_worked >= $${paramIndex}`); params.push(minHours); paramIndex++; } if (maxHours) { conditions.push(`te.hours_worked <= $${paramIndex}`); params.push(maxHours); paramIndex++; } if (billable !== null && billable !== undefined) { conditions.push(`te.billable = $${paramIndex}`); params.push(billable === 'true'); paramIndex++; } if (approved !== null && approved !== undefined) { conditions.push(`te.approved = $${paramIndex}`); params.push(approved === 'true'); paramIndex++; } if (hasTicket === 'true') { conditions.push(`te.ticket_id IS NOT NULL`); } // Validate sort column const validSortColumns = [ 'entry_date', 'hours_worked', 'created_at', 'updated_at', 'resource_id', 'ticket_id', 'task_id', 'project_id', 'company_id', 'title', 'billable', 'approved' ]; const validSortBy = validSortColumns.includes(sortBy) ? sortBy : 'entry_date'; const validSortOrder = sortOrder.toLowerCase() === 'asc' ? 'ASC' : 'DESC'; // Build the main query const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; const query = ` SELECT te.id, te.resource_id, r.first_name || ' ' || r.last_name as resource_name, te.ticket_id, t.ticket_number, t.title as ticket_title, te.task_id, task.title as task_title, te.project_id, p.project_name, te.company_id, c.company_name, te.entry_date, te.hours_worked, te.notes, te.internal_notes, te.title, te.type, te.start_date_time, te.end_date_time, te.billable, te.billing_rate, te.approved, te.approved_date_time, te.non_billable, te.created_at, te.updated_at, te.synced_at FROM time_entries te LEFT JOIN resources r ON te.resource_id = r.id LEFT JOIN tickets t ON te.ticket_id = t.id LEFT JOIN tasks task ON te.task_id = task.id LEFT JOIN projects p ON te.project_id = p.id LEFT JOIN companies c ON te.company_id = c.id ${whereClause} ORDER BY te.${validSortBy} ${validSortOrder} LIMIT $${paramIndex} OFFSET $${paramIndex + 1} `; params.push(limit, offset); paramIndex += 2; // Get total count const countQuery = ` SELECT COUNT(*) as total FROM time_entries te ${whereClause} `; const client = await pool.connect(); try { // Execute both queries in parallel const [result, countResult] = await Promise.all([ client.query(query, params), client.query(countQuery, params.slice(0, -2)) // Remove limit and offset for count ]); const timeEntries: TimeEntry[] = result.rows; const total = parseInt(countResult.rows[0].total); // Get summary statistics const summaryQuery = ` SELECT COUNT(*) as total_entries, SUM(hours_worked) as total_hours, AVG(hours_worked) as avg_hours, MIN(entry_date) as earliest_date, MAX(entry_date) as latest_date, COUNT(CASE WHEN billable = true THEN 1 END) as billable_entries, COUNT(CASE WHEN approved = true THEN 1 END) as approved_entries FROM time_entries te ${whereClause} `; const summaryResult = await client.query(summaryQuery, params.slice(0, -2)); const summary = summaryResult.rows[0]; return NextResponse.json({ timeEntries, pagination: { total, limit, offset, hasMore: offset + limit < total, }, summary: { totalEntries: parseInt(summary.total_entries), totalHours: parseFloat(summary.total_hours) || 0, averageHours: parseFloat(summary.avg_hours) || 0, earliestDate: summary.earliest_date, latestDate: summary.latest_date, billableEntries: parseInt(summary.billable_entries), approvedEntries: parseInt(summary.approved_entries), }, }); } finally { client.release(); } } catch (error) { console.error('Error fetching time entries:', error); return NextResponse.json( { error: 'Failed to fetch time entries' }, { status: 500 } ); } } export async function POST(request: NextRequest) { try { const body = await request.json(); // Validate required fields const requiredFields = ['resource_id', 'entry_date', 'hours_worked']; for (const field of requiredFields) { if (!body[field]) { return NextResponse.json( { error: `Missing required field: ${field}` }, { status: 400 } ); } } const client = await pool.connect(); try { const query = ` INSERT INTO time_entries ( resource_id, ticket_id, task_id, project_id, company_id, entry_date, hours_worked, notes, internal_notes, title, type, start_date_time, end_date_time, billable, billing_rate, approved, approved_date_time, non_billable, created_at, updated_at, synced_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, NOW(), NOW(), NOW() ) RETURNING * `; const values = [ body.resource_id, body.ticket_id || null, body.task_id || null, body.project_id || null, body.company_id || null, body.entry_date, body.hours_worked, body.notes || null, body.internal_notes || null, body.title || null, body.type || null, body.start_date_time || null, body.end_date_time || null, body.billable !== undefined ? body.billable : true, body.billing_rate || null, body.approved !== undefined ? body.approved : false, body.approved_date_time || null, body.non_billable !== undefined ? body.non_billable : false, ]; const result = await client.query(query, values); const timeEntry: TimeEntry = result.rows[0]; return NextResponse.json({ timeEntry }, { status: 201 }); } finally { client.release(); } } catch (error) { console.error('Error creating time entry:', error); return NextResponse.json( { error: 'Failed to create time entry' }, { status: 500 } ); } }