initial: pulsy from pulse-clock-main
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
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,
|
||||
}),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user