initial: pulsy from pulse-clock-main
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getPulseSecret, verifyCookie, setAuthCookie, verifySecret } from '@/lib/auth-cookie';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
if (verifyCookie(request)) {
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { secret } = body as { secret?: string };
|
||||
|
||||
if (!secret) {
|
||||
return NextResponse.json({ error: 'Secret is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!getPulseSecret()) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Server not configured: PULSE_SECRET must be at least 32 characters' },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
if (!verifySecret(secret)) {
|
||||
return NextResponse.json({ error: 'Invalid secret' }, { status: 401 });
|
||||
}
|
||||
|
||||
const response = NextResponse.json({ ok: true });
|
||||
setAuthCookie(response);
|
||||
return response;
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid request' }, { status: 400 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db, dbReady } from '@/db';
|
||||
import { companies, employees, timeEntries, auditLog } from '@/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { CompanySchema, CompanyUpdateSchema } from '@/lib/schemas';
|
||||
import { ok, err } from '@/lib/api-response';
|
||||
|
||||
export async function GET() {
|
||||
await dbReady;
|
||||
const results = await db.select().from(companies).all();
|
||||
return NextResponse.json(ok(results));
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.json();
|
||||
const parsed = CompanySchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json(err(parsed.error.issues[0]!.message), { status: 400 });
|
||||
}
|
||||
await dbReady;
|
||||
const company = await db.insert(companies).values(parsed.data).returning().get();
|
||||
return NextResponse.json(ok({ company }));
|
||||
}
|
||||
|
||||
export async function PATCH(request: NextRequest) {
|
||||
const body = await request.json();
|
||||
const parsed = CompanyUpdateSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json(err(parsed.error.issues[0]!.message), { status: 400 });
|
||||
}
|
||||
await dbReady;
|
||||
const company = await db
|
||||
.update(companies)
|
||||
.set({ name: parsed.data.name })
|
||||
.where(eq(companies.id, parsed.data.id))
|
||||
.returning()
|
||||
.get();
|
||||
if (!company) {
|
||||
return NextResponse.json(err('Company not found'), { status: 404 });
|
||||
}
|
||||
return NextResponse.json(ok({ company }));
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const id = searchParams.get('id');
|
||||
const parsed = id ? parseInt(id) : NaN;
|
||||
if (isNaN(parsed)) {
|
||||
return NextResponse.json(err('Invalid id'), { status: 400 });
|
||||
}
|
||||
|
||||
await dbReady;
|
||||
const deletedCompany = await db.transaction(async (tx) => {
|
||||
await Promise.all([
|
||||
tx.delete(auditLog).where(eq(auditLog.companyId, parsed)).run(),
|
||||
tx.delete(timeEntries).where(eq(timeEntries.companyId, parsed)).run(),
|
||||
]);
|
||||
await tx.delete(employees).where(eq(employees.companyId, parsed)).run();
|
||||
return tx.delete(companies).where(eq(companies.id, parsed)).returning().get();
|
||||
});
|
||||
|
||||
if (!deletedCompany) {
|
||||
return NextResponse.json(err('Company not found'), { status: 404 });
|
||||
}
|
||||
return NextResponse.json(ok({ company: deletedCompany }));
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db, dbReady } from '@/db';
|
||||
import { employees, companies, timeEntries } from '@/db/schema';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import { EmployeeSchema, EmployeeUpdateSchema } from '@/lib/schemas';
|
||||
import { ok, err } from '@/lib/api-response';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const companyId = searchParams.get('companyId');
|
||||
if (!companyId) {
|
||||
return NextResponse.json(err('companyId is required'), { status: 400 });
|
||||
}
|
||||
await dbReady;
|
||||
const result = await db
|
||||
.select({
|
||||
id: employees.id,
|
||||
name: employees.name,
|
||||
isActive: employees.isActive,
|
||||
companyId: employees.companyId,
|
||||
})
|
||||
.from(employees)
|
||||
.where(and(eq(employees.companyId, parseInt(companyId)), eq(employees.isActive, true)))
|
||||
.all();
|
||||
return NextResponse.json(ok(result));
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.json();
|
||||
const parsed = EmployeeSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json(err(parsed.error.issues[0]!.message), { status: 400 });
|
||||
}
|
||||
await dbReady;
|
||||
const company = await db.select().from(companies).where(eq(companies.id, parsed.data.companyId)).get();
|
||||
if (!company) {
|
||||
return NextResponse.json(err('Company not found'), { status: 404 });
|
||||
}
|
||||
const employee = await db.insert(employees).values(parsed.data).returning().get();
|
||||
return NextResponse.json(ok({ employee }));
|
||||
}
|
||||
|
||||
export async function PATCH(request: NextRequest) {
|
||||
const body = await request.json();
|
||||
const parsed = EmployeeUpdateSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json(err(parsed.error.issues[0]!.message), { status: 400 });
|
||||
}
|
||||
const { id, ...rest } = parsed.data;
|
||||
const updateData: Partial<typeof employees.$inferInsert> = {};
|
||||
if (rest.name !== undefined) updateData.name = rest.name;
|
||||
if (rest.isActive !== undefined) updateData.isActive = rest.isActive;
|
||||
if (Object.keys(updateData).length === 0) {
|
||||
return NextResponse.json(err('No fields to update'), { status: 400 });
|
||||
}
|
||||
await dbReady;
|
||||
const employee = await db.update(employees).set(updateData).where(eq(employees.id, id)).returning().get();
|
||||
if (!employee) {
|
||||
return NextResponse.json(err('Employee not found'), { status: 404 });
|
||||
}
|
||||
return NextResponse.json(ok({ employee }));
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const id = searchParams.get('id');
|
||||
if (!id) {
|
||||
return NextResponse.json(err('id is required'), { status: 400 });
|
||||
}
|
||||
const parsedId = parseInt(id);
|
||||
if (isNaN(parsedId)) {
|
||||
return NextResponse.json(err('Invalid id'), { status: 400 });
|
||||
}
|
||||
await dbReady;
|
||||
const deletedEmployee = await db.transaction(async (tx) => {
|
||||
await tx.delete(timeEntries).where(eq(timeEntries.employeeId, parsedId)).run();
|
||||
return tx.delete(employees).where(eq(employees.id, parsedId)).returning().get();
|
||||
});
|
||||
if (!deletedEmployee) {
|
||||
return NextResponse.json(err('Employee not found'), { status: 404 });
|
||||
}
|
||||
return NextResponse.json(ok({ employee: deletedEmployee }));
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db, dbReady } from '@/db';
|
||||
import { employees, timeEntries } from '@/db/schema';
|
||||
import { eq, and, inArray } from 'drizzle-orm';
|
||||
import { statusAfter } from '@/lib/validation';
|
||||
import { ok, err } from '@/lib/api-response';
|
||||
import { calculateLiveShiftElapsed } from '@/lib/calculations';
|
||||
import { parseId } from '@/lib/params';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const companyId = parseId(searchParams.get('companyId'));
|
||||
if (companyId == null) {
|
||||
return NextResponse.json(err('valid companyId is required'), { status: 400 });
|
||||
}
|
||||
|
||||
await dbReady;
|
||||
const activeEmps = await db
|
||||
.select({ id: employees.id, name: employees.name })
|
||||
.from(employees)
|
||||
.where(and(eq(employees.companyId, companyId), eq(employees.isActive, true)))
|
||||
.all();
|
||||
|
||||
if (activeEmps.length === 0) {
|
||||
return NextResponse.json(ok({ employees: [], fetchedAt: Math.floor(Date.now() / 1000) }));
|
||||
}
|
||||
|
||||
const allEntries = await 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),
|
||||
inArray(
|
||||
timeEntries.employeeId,
|
||||
activeEmps.map((emp) => emp.id),
|
||||
),
|
||||
),
|
||||
)
|
||||
.orderBy(timeEntries.employeeId, timeEntries.timestamp, timeEntries.id)
|
||||
.all();
|
||||
|
||||
const employeeList = activeEmps.map((emp) => {
|
||||
const entries = allEntries.filter((entry) => entry.employeeId === emp.id);
|
||||
const latestEntry = entries.at(-1);
|
||||
return {
|
||||
id: emp.id,
|
||||
name: emp.name,
|
||||
status: latestEntry ? statusAfter(latestEntry.type) : 'OUT',
|
||||
lastPunchTimestamp: latestEntry?.timestamp ?? null,
|
||||
...calculateLiveShiftElapsed(entries),
|
||||
};
|
||||
});
|
||||
|
||||
return NextResponse.json(ok({ employees: employeeList, fetchedAt: Math.floor(Date.now() / 1000) }));
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export function GET() {
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { PunchSchema } from '@/lib/schemas';
|
||||
import { ok, err } from '@/lib/api-response';
|
||||
import { createValidatedTimeEntry } from '@/lib/time-entry-service';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const parsed = PunchSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json(err(parsed.error.issues[0]!.message), { status: 400 });
|
||||
}
|
||||
const { employeeId, companyId, type, photoBase64 } = parsed.data;
|
||||
|
||||
const result = await createValidatedTimeEntry({
|
||||
employeeId,
|
||||
companyId,
|
||||
type,
|
||||
photoBase64,
|
||||
source: 'kiosk',
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
return NextResponse.json(err(result.error), { status: result.status });
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
ok({
|
||||
entry: {
|
||||
id: result.entry.id,
|
||||
employeeId: result.entry.employeeId,
|
||||
type: result.entry.type,
|
||||
timestamp: result.entry.timestamp,
|
||||
},
|
||||
status: result.status,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Punch error:', error);
|
||||
return NextResponse.json(err('Internal server error'), { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db, dbReady } from '@/db';
|
||||
import { timeEntries, employees } from '@/db/schema';
|
||||
import { eq, and, gte, lte } from 'drizzle-orm';
|
||||
import { validateTimestampChronology } from '@/lib/validation';
|
||||
import { ManualEntrySchema, TimeEntryUpdateSchema } from '@/lib/schemas';
|
||||
import { ok, err } from '@/lib/api-response';
|
||||
import { createValidatedTimeEntry } from '@/lib/time-entry-service';
|
||||
|
||||
function parseRangeParam(value: string, boundary: 'start' | 'end'): number {
|
||||
if (/^\d+$/.test(value)) return Number(value);
|
||||
|
||||
const suffix = boundary === 'start' ? 'T00:00:00' : 'T23:59:59';
|
||||
return Math.floor(new Date(`${value}${suffix}`).getTime() / 1000);
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const companyId = searchParams.get('companyId');
|
||||
const employeeId = searchParams.get('employeeId');
|
||||
const startDate = searchParams.get('startDate');
|
||||
const endDate = searchParams.get('endDate');
|
||||
|
||||
if (!companyId) {
|
||||
return NextResponse.json(err('companyId is required'), { status: 400 });
|
||||
}
|
||||
|
||||
const whereConditions = [eq(timeEntries.companyId, parseInt(companyId))];
|
||||
if (employeeId) whereConditions.push(eq(timeEntries.employeeId, parseInt(employeeId)));
|
||||
if (startDate) whereConditions.push(gte(timeEntries.timestamp, parseRangeParam(startDate, 'start')));
|
||||
if (endDate) whereConditions.push(lte(timeEntries.timestamp, parseRangeParam(endDate, 'end')));
|
||||
|
||||
await dbReady;
|
||||
const results = await db
|
||||
.select({
|
||||
id: timeEntries.id,
|
||||
employeeId: timeEntries.employeeId,
|
||||
companyId: timeEntries.companyId,
|
||||
type: timeEntries.type,
|
||||
timestamp: timeEntries.timestamp,
|
||||
photoBase64: timeEntries.photoBase64,
|
||||
isDeleted: timeEntries.isDeleted,
|
||||
employeeName: employees.name,
|
||||
})
|
||||
.from(timeEntries)
|
||||
.innerJoin(employees, eq(timeEntries.employeeId, employees.id))
|
||||
.where(and(...whereConditions))
|
||||
.orderBy(timeEntries.timestamp)
|
||||
.all();
|
||||
|
||||
return NextResponse.json(ok(results));
|
||||
}
|
||||
|
||||
export async function PATCH(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const parsed = TimeEntryUpdateSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json(err(parsed.error.issues[0]!.message), { status: 400 });
|
||||
}
|
||||
const { id, isDeleted, timestamp, forceOverride } = parsed.data;
|
||||
|
||||
await dbReady;
|
||||
const result = await db.transaction(async (tx) => {
|
||||
const existing = await tx.select().from(timeEntries).where(eq(timeEntries.id, id)).get();
|
||||
if (!existing) {
|
||||
return { ok: false as const, status: 404, error: 'Entry not found' };
|
||||
}
|
||||
|
||||
if (timestamp !== undefined && timestamp !== existing.timestamp) {
|
||||
const entries = await tx
|
||||
.select()
|
||||
.from(timeEntries)
|
||||
.where(
|
||||
and(
|
||||
eq(timeEntries.employeeId, existing.employeeId),
|
||||
eq(timeEntries.companyId, existing.companyId),
|
||||
eq(timeEntries.isDeleted, false),
|
||||
),
|
||||
)
|
||||
.all();
|
||||
const validation = validateTimestampChronology(entries, id, timestamp, !!forceOverride);
|
||||
if (!validation.valid) {
|
||||
return { ok: false as const, status: 400, error: validation.error ?? 'Validation failed' };
|
||||
}
|
||||
}
|
||||
|
||||
const updateData: Partial<typeof timeEntries.$inferInsert> = {};
|
||||
if (isDeleted !== undefined) updateData.isDeleted = isDeleted;
|
||||
if (timestamp !== undefined) updateData.timestamp = timestamp;
|
||||
|
||||
const updated = await tx.update(timeEntries).set(updateData).where(eq(timeEntries.id, id)).returning().get();
|
||||
|
||||
return { ok: true as const, entry: updated };
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
return NextResponse.json(err(result.error), { status: result.status });
|
||||
}
|
||||
|
||||
return NextResponse.json(ok({ entry: result.entry }));
|
||||
} catch (error) {
|
||||
console.error('Update time entry error:', error);
|
||||
return NextResponse.json(err('Internal server error'), { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const parsed = ManualEntrySchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json(err(parsed.error.issues[0]!.message), { status: 400 });
|
||||
}
|
||||
const { employeeId, companyId, type, timestamp, photoBase64, forceOverride } = parsed.data;
|
||||
|
||||
const result = await createValidatedTimeEntry({
|
||||
employeeId,
|
||||
companyId,
|
||||
type,
|
||||
timestamp,
|
||||
photoBase64: photoBase64 || null,
|
||||
forceOverride: !!forceOverride,
|
||||
source: 'manual',
|
||||
});
|
||||
if (!result.ok) {
|
||||
return NextResponse.json(err(result.error), { status: result.status });
|
||||
}
|
||||
|
||||
return NextResponse.json(ok({ entry: result.entry, overridden: result.overridden }));
|
||||
} catch (error) {
|
||||
console.error('Add manual entry error:', error);
|
||||
return NextResponse.json(err('Internal server error'), { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user