Files
pulsy/src/app/api/payroll/route.ts
T

132 lines
4.7 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { db, dbReady } from '@/db';
import { employees, timeEntries, companies } from '@/db/schema';
import { eq, and, gte, lte } from 'drizzle-orm';
import { calculateDayMinutes, formatHours } from '@/lib/calculations';
import { format, addDays } from 'date-fns';
import { ok, err } from '@/lib/api-response';
import { parseId } from '@/lib/params';
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const companyId = parseId(searchParams.get('companyId'));
const startDateStr = searchParams.get('startDate');
const endDateStr = searchParams.get('endDate');
if (companyId == null) {
return NextResponse.json(err('valid companyId is required'), { status: 400 });
}
if (!startDateStr || !endDateStr) {
return NextResponse.json(err('startDate and endDate are required'), { status: 400 });
}
const startDate = new Date(startDateStr + 'T00:00:00');
const endDate = new Date(endDateStr + 'T23:59:59');
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
return NextResponse.json(err('Invalid date format'), { status: 400 });
}
const startUnix = Math.floor(startDate.getTime() / 1000);
const endUnix = Math.floor(endDate.getTime() / 1000);
const contextStartUnix = startUnix - 36 * 3600;
const contextEndUnix = endUnix + 36 * 3600;
await dbReady;
const [company, companyEmployees, allEntries] = await Promise.all([
db.select().from(companies).where(eq(companies.id, companyId)).get(),
db
.select()
.from(employees)
.where(and(eq(employees.companyId, companyId), eq(employees.isActive, true)))
.all(),
db
.select({
id: timeEntries.id,
employeeId: timeEntries.employeeId,
type: timeEntries.type,
timestamp: timeEntries.timestamp,
isDeleted: timeEntries.isDeleted,
})
.from(timeEntries)
.where(
and(
eq(timeEntries.companyId, companyId),
eq(timeEntries.isDeleted, false),
gte(timeEntries.timestamp, contextStartUnix),
lte(timeEntries.timestamp, contextEndUnix),
),
)
.orderBy(timeEntries.timestamp)
.all(),
]);
if (!company) {
return NextResponse.json(err('Company not found'), { status: 404 });
}
const days: string[] = [];
let dayDate = startDate;
while (dayDate <= endDate) {
days.push(format(dayDate, 'yyyy-MM-dd'));
dayDate = addDays(dayDate, 1);
}
const resultEmployees = companyEmployees.map((emp) => {
const empEntries = allEntries.filter((e) => e.employeeId === emp.id);
const empDays = days.map((dayStr) => {
const dayStart = new Date(dayStr + 'T00:00:00').getTime() / 1000;
const dayEnd = new Date(dayStr + 'T23:59:59').getTime() / 1000;
const dayEntries = empEntries.filter((e) => e.timestamp >= dayStart && e.timestamp <= dayEnd);
// Single call , errors are returned directly
const { clockMinutes, breakMinutes, workedMinutes, hasErrors, errors } = calculateDayMinutes(empEntries, dayStr);
const inEntry = dayEntries.find((e) => e.type === 'IN' && !e.isDeleted);
const outEntry = dayEntries.find((e) => e.type === 'OUT' && !e.isDeleted);
return {
date: dayStr,
inTime: inEntry ? format(new Date(inEntry.timestamp * 1000), 'h:mm a') : '',
outTime: outEntry ? format(new Date(outEntry.timestamp * 1000), 'h:mm a') : '',
breakMinutes: Math.round(breakMinutes),
clockMinutes: Math.round(clockMinutes),
workedMinutes: Math.round(workedMinutes),
hasErrors,
errors,
};
});
const totals = empDays.reduce(
(acc, day) => ({
clockMinutes: acc.clockMinutes + day.clockMinutes,
breakMinutes: acc.breakMinutes + day.breakMinutes,
workedMinutes: acc.workedMinutes + day.workedMinutes,
}),
{ clockMinutes: 0, breakMinutes: 0, workedMinutes: 0 },
);
return {
id: emp.id,
name: emp.name,
days: empDays,
totals,
};
});
let summary = 'Employee\tDate\tIn\tOut\tBreak\tNet\tNotes\n';
for (const emp of resultEmployees) {
for (const day of emp.days) {
const hasErrors = day.errors.length > 0;
summary += `${emp.name}\t${day.date}\t${day.inTime}\t${day.outTime}\t${formatHours(day.breakMinutes)}\t${formatHours(day.workedMinutes)}\t${hasErrors ? '[WARN] ' + day.errors.join('; ') : ''}\n`;
}
summary += `${emp.name}\tTOTAL\t\t\t${formatHours(emp.totals.breakMinutes)}\t${formatHours(emp.totals.workedMinutes)}\t\n`;
}
return NextResponse.json(
ok({
company: company.name,
period: { startDate: startDateStr, endDate: endDateStr },
employees: resultEmployees,
summary,
}),
);
}