v2.1.0: Copy Previous Week, Bulk Approve, Overtime Tracking
Features: - Copy Previous Week: one-tap to duplicate last week's entries - Bulk Approve/Reject: multi-select + batch actions for admin reviews - Overtime Tracking: admin report tab + employee real-time OT warnings - Homeowner filter on reports - Homeowner edit/search/address fields - Employee role management + deactivate/reactivate - Reopen locked timesheets Fixes: - Timezone bug (UTC vs local date parsing) - CORS, approve 400, PDF download, auto-save errors - History page crash, rate limiting
This commit is contained in:
@@ -268,4 +268,92 @@ router.delete('/:id', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ─────────────── POST /api/entries/copy-week ───────────────
|
||||
router.post('/copy-week', async (req, res) => {
|
||||
try {
|
||||
const { fromWeek, toWeek } = req.body;
|
||||
if (!fromWeek || !toWeek) {
|
||||
return res.status(400).json({ error: 'fromWeek and toWeek are required (YYYY-MM-DD Monday)' });
|
||||
}
|
||||
|
||||
const fromMonday = getWeekMonday(fromWeek);
|
||||
const toMonday = getWeekMonday(toWeek);
|
||||
const fromSunday = getWeekSunday(fromMonday);
|
||||
|
||||
// Check target week isn't locked
|
||||
const targetTs = await req.prisma.timesheet.findUnique({
|
||||
where: { userId_weekStart: { userId: req.user.id, weekStart: new Date(toMonday + 'T00:00:00Z') } },
|
||||
});
|
||||
if (targetTs && ['submitted', 'approved'].includes(targetTs.status)) {
|
||||
return res.status(403).json({ error: 'Target week is locked (submitted or approved)' });
|
||||
}
|
||||
|
||||
// Get source week entries
|
||||
const sourceEntries = await req.prisma.timeEntry.findMany({
|
||||
where: {
|
||||
userId: req.user.id,
|
||||
date: {
|
||||
gte: new Date(fromMonday + 'T00:00:00Z'),
|
||||
lte: new Date(fromSunday + 'T00:00:00Z'),
|
||||
},
|
||||
},
|
||||
include: { homeowner: { select: { id: true, isActive: true } } },
|
||||
});
|
||||
|
||||
if (sourceEntries.length === 0) {
|
||||
return res.status(404).json({ error: 'No entries found in source week' });
|
||||
}
|
||||
|
||||
// Calculate day offset (Mon=0 ... Sun=6)
|
||||
const fromStart = new Date(fromMonday + 'T00:00:00Z');
|
||||
const toStart = new Date(toMonday + 'T00:00:00Z');
|
||||
const dayOffset = Math.round((toStart - fromStart) / (1000 * 60 * 60 * 24));
|
||||
|
||||
// Delete existing entries in target week (that are in draft)
|
||||
const toSunday = getWeekSunday(toMonday);
|
||||
await req.prisma.timeEntry.deleteMany({
|
||||
where: {
|
||||
userId: req.user.id,
|
||||
date: {
|
||||
gte: new Date(toMonday + 'T00:00:00Z'),
|
||||
lte: new Date(toSunday + 'T00:00:00Z'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Copy entries with shifted dates, only for active homeowners
|
||||
const created = [];
|
||||
for (const entry of sourceEntries) {
|
||||
if (!entry.homeowner.isActive) continue;
|
||||
const oldDate = new Date(entry.date);
|
||||
const newDate = new Date(oldDate);
|
||||
newDate.setUTCDate(newDate.getUTCDate() + dayOffset);
|
||||
|
||||
const newEntry = await req.prisma.timeEntry.create({
|
||||
data: {
|
||||
userId: req.user.id,
|
||||
date: newDate,
|
||||
homeownerId: entry.homeownerId,
|
||||
hoursWorked: entry.hoursWorked,
|
||||
workDescription: entry.workDescription,
|
||||
},
|
||||
include: { homeowner: { select: { name: true } } },
|
||||
});
|
||||
created.push({
|
||||
id: newEntry.id,
|
||||
date: newEntry.date.toISOString().split('T')[0],
|
||||
homeownerId: newEntry.homeownerId,
|
||||
homeownerName: newEntry.homeowner.name,
|
||||
hoursWorked: newEntry.hoursWorked,
|
||||
workDescription: newEntry.workDescription,
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({ copied: created.length, entries: created });
|
||||
} catch (err) {
|
||||
console.error('Copy week error:', err);
|
||||
res.status(500).json({ error: 'Failed to copy week' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
Reference in New Issue
Block a user