Initial commit - Route Commerce platform

This commit is contained in:
2026-06-01 19:40:55 +00:00
commit 53a9671461
617 changed files with 106132 additions and 0 deletions
+264
View File
@@ -0,0 +1,264 @@
import { NextRequest, NextResponse } from "next/server";
import { getAdminUser } from "@/lib/admin-permissions";
import { getTimeTrackingSettings, getTimeTrackingWorkers, getWorkerTimeLogs } from "@/actions/time-tracking";
const TUXEDO_BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de";
const IRD_BRAND_ID = "b1cb7a96-d82b-40b1-80b1-d6dd26c56e28";
type LogEntry = {
id: string;
worker_id: string;
worker_name: string;
task_id: string | null;
task_name: string;
clock_in: string;
clock_out: string | null;
lunch_break_minutes: number;
notes: string | null;
submitted_via: string;
total_minutes: number;
created_at: string;
brandId: string;
};
function formatDate(iso: string): string {
return new Date(iso).toISOString().slice(0, 10);
}
function formatTime(iso: string): string {
return new Date(iso).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: true });
}
function csvRow(values: (string | number | boolean | null)[]): string {
return values.map(v => `"${String(v ?? "").replace(/"/g, '""')}"`).join(",");
}
// Determine current pay period from settings
function getPayPeriodDates(
startDay: number,
lengthDays: number
): { start: string; end: string } {
const now = new Date();
const currentDayOfWeek = now.getDay(); // 0=Sun
const daysSinceStartDay = (currentDayOfWeek - startDay + 7) % 7;
const periodStart = new Date(now);
periodStart.setDate(now.getDate() - daysSinceStartDay - (lengthDays - 1));
const periodEnd = new Date(periodStart);
periodEnd.setDate(periodStart.getDate() + lengthDays - 1);
return {
start: formatDate(periodStart.toISOString()),
end: formatDate(periodEnd.toISOString()),
};
}
type ExportType = "quickbooks" | "payroll" | "detailed" | "summary";
export async function GET(req: NextRequest) {
const adminUser = await getAdminUser();
if (!adminUser) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { searchParams } = new URL(req.url);
const exportType = (searchParams.get("type") ?? "detailed") as ExportType;
const brandFilter = searchParams.get("brand") ?? null; // "tuxedo" | "ird" | null
const startDate = searchParams.get("start") ?? "";
const endDate = searchParams.get("end") ?? "";
const includeOvertime = searchParams.get("overtime") !== "false";
const includeNotes = searchParams.get("notes") !== "false";
// Determine brand IDs to export
let brandIds: string[] = [];
if (brandFilter === "tuxedo") brandIds = [TUXEDO_BRAND_ID];
else if (brandFilter === "ird") brandIds = [IRD_BRAND_ID];
else if (adminUser.brand_id) brandIds = [adminUser.brand_id];
else brandIds = [TUXEDO_BRAND_ID, IRD_BRAND_ID];
// Collect all logs across brands
let allLogs: LogEntry[] = [];
let allSettings: Awaited<ReturnType<typeof getTimeTrackingSettings>>[] = [];
let allWorkers: Awaited<ReturnType<typeof getTimeTrackingWorkers>>[] = [];
for (const brandId of brandIds) {
const [workers, settings, logs] = await Promise.all([
getTimeTrackingWorkers(brandId),
getTimeTrackingSettings(brandId),
getWorkerTimeLogs(brandId, {
start: startDate || undefined,
end: endDate || undefined,
limit: 5000,
}),
]);
allWorkers.push(workers);
allSettings.push(settings);
allLogs = allLogs.concat((logs as LogEntry[]).map(l => ({ ...l, brandId })));
}
// Sort by clock_in
allLogs.sort((a, b) => new Date(a.clock_in).getTime() - new Date(b.clock_in).getTime());
// Default to current pay period if no dates given
const defaultPeriod = getPayPeriodDates(0, 7);
const start = startDate || defaultPeriod.start;
const end = endDate || defaultPeriod.end;
// Filter by date range
allLogs = allLogs.filter(l => {
const d = formatDate(l.clock_in);
return d >= start && d <= end;
});
// Helper to compute overtime for a log entry
function computeOvertime(log: (typeof allLogs)[0], settings: NonNullable<typeof allSettings[0]>) {
if (!includeOvertime) return { regularHours: 0, overtimeHours: 0 };
const totalHours = log.total_minutes / 60;
const dailyThreshold = Number(settings.daily_overtime_threshold);
const weeklyThreshold = Number(settings.weekly_overtime_threshold);
// Compute which threshold was hit
const overtimeHours = Math.max(0, totalHours - dailyThreshold);
const regularHours = totalHours - overtimeHours;
return { regularHours, overtimeHours };
}
let csv = "";
let filename = "";
if (exportType === "quickbooks") {
// QuickBooks Time import format
// Columns: Employee Name, Date, Clock In, Clock Out, Hours, Task/Service Item, Overtime Hours, Notes
const headers = ["Employee Name", "Date", "Clock In", "Clock Out", "Hours", "Task / Service Item", "Overtime Hours", "Notes"];
const rows = allLogs.map((log, i) => {
const worker = allWorkers.flat().find(w => w.id === log.worker_id);
const settings = allSettings.find(s => s && s.brand_id === log.brandId);
const { regularHours, overtimeHours } = settings ? computeOvertime(log, settings) : { regularHours: 0, overtimeHours: 0 };
const totalHours = log.total_minutes / 60;
return [
log.worker_name,
formatDate(log.clock_in),
formatTime(log.clock_in),
log.clock_out ? formatTime(log.clock_out) : "",
totalHours.toFixed(2),
log.task_name,
overtimeHours > 0 ? overtimeHours.toFixed(2) : "",
log.notes ?? "",
];
});
csv = [headers, ...rows].map(csvRow).join("\n");
filename = `time-tracking-quickbooks-${start}-to-${end}.csv`;
}
else if (exportType === "payroll") {
// Standard payroll export — grouped by worker with totals
// Columns: Worker Name, Role, Total Hours, Regular Hours, Overtime Hours, Days Worked, Tasks
const workerMap = new Map<string, {
name: string; role: string; totalMin: number; days: Set<string>; tasks: Set<string>;
dailyOTMin: number; weeklyOTMin: number;
}>();
for (const log of allLogs) {
if (!workerMap.has(log.worker_id)) {
const w = allWorkers.flat().find(w => w.id === log.worker_id);
workerMap.set(log.worker_id, {
name: log.worker_name,
role: w?.role ?? "worker",
totalMin: 0, days: new Set(), tasks: new Set(),
dailyOTMin: 0, weeklyOTMin: 0,
});
}
const entry = workerMap.get(log.worker_id)!;
entry.totalMin += log.total_minutes;
entry.days.add(formatDate(log.clock_in));
entry.tasks.add(log.task_name);
}
const headers = ["Worker Name", "Role", "Days Worked", "Total Hours", "Regular Hours", "Overtime Hours", "Tasks"];
const rows = [...workerMap.values()].map(e => {
const totalH = e.totalMin / 60;
const otH = Math.max(0, totalH - 40); // weekly OT at 40h
const regH = totalH - otH;
return [
e.name,
e.role,
e.days.size,
totalH.toFixed(2),
regH.toFixed(2),
otH > 0 ? otH.toFixed(2) : "",
[...e.tasks].join("; "),
];
});
csv = [headers, ...rows].map(csvRow).join("\n");
filename = `time-tracking-payroll-${start}-to-${end}.csv`;
}
else if (exportType === "summary") {
// Pay period summary — one row per worker per pay period
const brandName = allSettings[0]?.brand_name ?? "Farm";
const headers = ["Brand", "Worker Name", "Pay Period Start", "Pay Period End", "Total Hours", "Daily OT Hours", "Weekly OT Hours", "Status"];
const workerMap = new Map<string, {
brandName: string; name: string; totalMin: number; dailyOT: number; weeklyOT: number;
}>();
for (const log of allLogs) {
if (!workerMap.has(log.worker_id)) {
const settings = allSettings.find(s => s && s.brand_id === log.brandId);
workerMap.set(log.worker_id, {
brandName: settings?.brand_name ?? brandName,
name: log.worker_name,
totalMin: 0, dailyOT: 0, weeklyOT: 0,
});
}
const entry = workerMap.get(log.worker_id)!;
const totalH = log.total_minutes / 60;
const settings = allSettings.find(s => s && s.brand_id === log.brandId);
if (settings) {
const dailyThr = Number(settings.daily_overtime_threshold);
const weeklyThr = Number(settings.weekly_overtime_threshold);
if (totalH > dailyThr) entry.dailyOT += (totalH - dailyThr);
if (totalH > weeklyThr) entry.weeklyOT += (totalH - weeklyThr);
}
entry.totalMin += log.total_minutes;
}
const rows = [...workerMap.values()].map(e => {
const totalH = e.totalMin / 60;
return [
e.brandName,
e.name,
start, end,
totalH.toFixed(2),
e.dailyOT > 0 ? e.dailyOT.toFixed(2) : "",
e.weeklyOT > 0 ? e.weeklyOT.toFixed(2) : "",
e.weeklyOT > 0 ? "OVERTIME" : totalH > 40 ? "NEAR OT" : "OK",
];
});
csv = [headers, ...rows].map(csvRow).join("\n");
filename = `time-tracking-summary-${start}-to-${end}.csv`;
}
else {
// Detailed audit log
const headers = [
"Worker Name", "Task", "Date", "Clock In", "Clock Out",
"Lunch (min)", "Total Minutes", "Hours", "Overtime Hours",
"Submitted Via", "Notes", "Brand",
];
const rows = allLogs.map(log => {
const settings = allSettings.find(s => s && s.brand_id === log.brandId);
const { regularHours, overtimeHours } = settings ? computeOvertime(log, settings) : { regularHours: 0, overtimeHours: 0 };
return [
log.worker_name,
log.task_name,
formatDate(log.clock_in),
formatTime(log.clock_in),
log.clock_out ? formatTime(log.clock_out) : "",
log.lunch_break_minutes,
log.total_minutes,
(log.total_minutes / 60).toFixed(2),
overtimeHours > 0 ? overtimeHours.toFixed(2) : "",
log.submitted_via,
log.notes ?? "",
allSettings.find(s => s && s.brand_id === log.brandId)?.brand_name ?? "",
];
});
csv = [headers, ...rows].map(csvRow).join("\n");
filename = `time-tracking-detailed-${start}-to-${end}.csv`;
}
return new NextResponse(csv, {
headers: {
"Content-Type": "text/csv",
"Content-Disposition": `attachment; filename="${filename}"`,
},
});
}