Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 121 additions & 7 deletions app-backend/src/controllers/shiftattendance.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,26 +119,140 @@ export const checkOut = async (req, res) => {
return res.status(403).json({ message: "Not assigned to this shift" });
}

const attendance = await ShiftAttendance.findOne({ guard: guardId, shift: shiftId });
const attendance = await ShiftAttendance.findOne({
guardId: guardId,
shiftId: shiftId
});

if (!attendance) {
return res.status(404).json({ message: "No check-in record found" });
}

if (attendance.clockOut) {
if (attendance.checkOutTime) {
return res.status(400).json({ message: "Already checked out" });
}

attendance.clockOut = new Date();
attendance.recordedBy = guardId;
attendance.checkOutTime = new Date();

await attendance.save();

return res.status(200).json({ message: "Check-out recorded", attendance });
// AUTO TIMESHEET GENERATION
// When a guard checks out, we automatically:
// 1. Calculate hours worked
// 2. Create a new Timesheet record
// 3. Save it to the database
const checkInTime = new Date(attendance.checkInTime);
const checkOutTime = new Date(attendance.checkOutTime);
const hoursWorked = (checkOutTime - checkInTime) / (1000 * 60 * 60); // in hours

const Timesheet = (await import("../models/Timesheet.js")).default;

const timesheet = new Timesheet({
guardId,
shiftId,
date: checkInTime, // Store full date
checkInTime: checkInTime,
checkOutTime: checkOutTime,
hoursWorked: parseFloat(hoursWorked.toFixed(2)),
});

await timesheet.save();


return res.status(200).json({
message: "Check-out recorded successfully",
attendance,
timesheet: {
id: timesheet._id,
hoursWorked: timesheet.hoursWorked
}
});
} catch (error) {
console.error("Checkout Error:", error);
return res.status(500).json({ message: error.message });
}
};
// GET /api/v1/attendance/:userId

// GET /api/v1/timesheets
export const getTimesheets = async (req, res) => {
try {
const { guardId, startDate, endDate } = req.query;
const Timesheet = (await import("../models/Timesheet.js")).default;

let filter = {};

if (guardId) {
filter.guardId = guardId;
}
if (startDate) {
filter.date = { $gte: new Date(startDate) };
}
if (endDate) {
filter.date = filter.date || {};
filter.date.$lte = new Date(endDate);
}

const timesheets = await Timesheet.find(filter)
.populate('guardId', 'name email') // Optional: show guard info
.populate('shiftId', 'date startTime endTime')
.sort({ date: -1 });

res.status(200).json({
message: "Timesheets retrieved successfully",
count: timesheets.length,
timesheets
});
} catch (error) {
console.error("Get Timesheets Error:", error);
res.status(500).json({ message: error.message });
}
};
// export const checkOut = async (req, res) => {
// try {
// const { latitude, longitude } = req.body;
// const { shiftId } = req.params;
// const guardId = req.user?._id || req.user?.id;

// if (latitude === undefined || longitude === undefined) {
// return res.status(400).json({ message: "Invalid location coordinates" });
// }

// const latNum = Number(latitude);
// const lngNum = Number(longitude);

// if (!Number.isFinite(latNum) || !Number.isFinite(lngNum)) {
// return res.status(400).json({ message: "Invalid location coordinates" });
// }

// const shift = await Shift.findById(shiftId);
// if (!shift) {
// return res.status(404).json({ message: "Shift not found" });
// }

// if (String(shift.assignedGuard) !== String(guardId)) {
// return res.status(403).json({ message: "Not assigned to this shift" });
// }

// const attendance = await ShiftAttendance.findOne({ guard: guardId, shift: shiftId });
// if (!attendance) {
// return res.status(404).json({ message: "No check-in record found" });
// }

// if (attendance.clockOut) {
// return res.status(400).json({ message: "Already checked out" });
// }

// attendance.clockOut = new Date();
// attendance.recordedBy = guardId;

// await attendance.save();

// return res.status(200).json({ message: "Check-out recorded", attendance });
// } catch (error) {
// return res.status(500).json({ message: error.message });
// }
// };
// // GET /api/v1/attendance/:userId
export const getAttendanceByUserId = async (req, res) => {
try {
const { userId } = req.params;
Expand All @@ -160,4 +274,4 @@ export const getAttendanceByUserId = async (req, res) => {
} catch (error) {
res.status(500).json({ message: error.message });
}
};
};
45 changes: 45 additions & 0 deletions app-backend/src/models/Timesheet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// This model stores timesheet records that are automatically
import mongoose from "mongoose"; //Created for when a guard checks in/out of a shift

const timesheetSchema = new mongoose.Schema(
{
guardId: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
shiftId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Shift",
required: true,
},
date: {
type: Date,
required: true,
},
checkInTime: {
type: Date,
required: true,
},
checkOutTime: {
type: Date,
required: true,
},
hoursWorked: {
type: Number,
required: true,
},
status: {
type: String,
enum: ["completed", "pending"],
default: "completed",
},
},
{ timestamps: true }
);

// Prevent duplicate timesheet for same guard + shift
timesheetSchema.index({ guardId: 1, shiftId: 1 }, { unique: true });

const Timesheet = mongoose.model("Timesheet", timesheetSchema);
export default Timesheet;
7 changes: 5 additions & 2 deletions app-backend/src/routes/shiftattendance.routes.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import express from "express";
import { checkIn, checkOut, getAttendanceByUserId } from "../controllers/shiftattendance.controller.js";
import { checkIn, checkOut, getAttendanceByUserId, getTimesheets } from "../controllers/shiftattendance.controller.js";
import auth from "../middleware/auth.js";

const router = express.Router();
Expand Down Expand Up @@ -136,6 +136,9 @@ router.post("/checkout/:shiftId", auth, checkOut);
* 500:
* description: Server error
*/
// Get all timesheets (with optional filters)
router.get("/timesheets", auth, getTimesheets); // This endpoint is used for the employer panel timesheet page
router.get("/:userId", auth, getAttendanceByUserId);

export default router;

export default router;
73 changes: 43 additions & 30 deletions app-frontend/employer-panel/src/pages/Timesheet.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,36 +103,48 @@ export default function Timesheet({ language }) {
const [sortBy, setSortBy] = useState(Sort.DateDesc);
const [page, setPage] = useState(1);

useEffect(() => {
let cancelled = false;
(async () => {
try {
const { data } = await http.get("/shifts/myshifts");
const list = Array.isArray(data) ? data : [];
const assignedShifts = list.filter((s) => s.acceptedBy);
const attendanceLists = await Promise.all(
assignedShifts.map((s) =>
http
.get(`/payroll/attendance/${s._id}`)
.then((r) => r.data)
.catch(() => ({ records: [] }))
)
);
if (cancelled) return;
setRows(buildRows(assignedShifts, attendanceLists));
} catch (err) {
if (cancelled) return;
setError(
err?.response?.data?.message ||
err.message ||
"Failed to load timesheets"
);
} finally {
if (!cancelled) setLoading(false);
}
})();
return () => { cancelled = true; };
}, []);
useEffect(() => { //Changes made
let cancelled = false;

(async () => {
try {
setLoading(true);
setError(null);
// This was changed from the old endpoint to /attendance/timesheets
// as part of the Auto Timesheet Generation feature.
// This makes the Timesheet tab display real auto-generated data.
const { data } = await http.get("/attendance/timesheets"); // NEW ENDPOINT

if (cancelled) return;

// Transform data to match the table structure
const formattedRows = Array.isArray(data.timesheets)
? data.timesheets.map(ts => ({
id: ts._id,
guard: ts.guardId?.name || "Unknown Guard",
shiftDate: formatDate(ts.date),
location: "N/A", // You can improve later
clockIn: formatTime(ts.checkInTime),
clockOut: formatTime(ts.checkOutTime),
totalHours: formatHoursWorked(ts.hoursWorked),
payRate: "--",
status: "Completed",
totalPayment: ts.hoursWorked ? `$${(ts.hoursWorked * 25).toFixed(2)}` : "--", // assuming $25/hr
}))
: [];

setRows(formattedRows);
} catch (err) {
if (cancelled) return;
setError(err?.response?.data?.message || "Failed to load timesheets");
console.error(err);
} finally {
if (!cancelled) setLoading(false);
}
})();

return () => { cancelled = true; };
}, []);

const summary = useMemo(() => {
const counts = { Active: 0, Late: 0, Absent: 0, Completed: 0 };
Expand Down Expand Up @@ -340,3 +352,4 @@ const sortSelectStyle = {
cursor: "pointer",
};

//test commit fix
Loading