wulf-pulse/app/api/data/time-entries/route.ts

314 lines
9.4 KiB
TypeScript
Raw Normal View History

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 }
);
}
}