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
309 changes: 272 additions & 37 deletions app-backend/package-lock.json

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions app-backend/src/controllers/admin.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -538,9 +538,7 @@ export const rejectGuardLicense = async (req, res) => {
* @access Admin
*/
export const getSmtpSettings = async (req, res) => {
try {
const envPath = path.join(__dirname, '../../.env');

try {
const settings = {
SMTP_HOST: process.env.SMTP_HOST || '',
SMTP_PORT: process.env.SMTP_PORT || '587',
Expand Down
1 change: 0 additions & 1 deletion app-backend/src/controllers/auth.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { sendEmployerCredentials } from '../utils/sendEmail.js';
import { sendOTP } from '../utils/sendEmail.js';
import { ACTIONS } from "../middleware/logger.js";
import EOI from '../models/eoi.js';
import { GridFSBucket } from 'mongodb';

import Guard from '../models/Guard.js'; // use the discriminator so license fields persist

Expand Down
1 change: 0 additions & 1 deletion app-backend/src/controllers/notification.controller.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Notification from '../models/Notification.js';
import { ROLES } from '../middleware/rbac.js';

/**
* GET /notifications
Expand Down
1 change: 0 additions & 1 deletion app-backend/src/controllers/shift.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import Shift from '../models/Shift.js';
import Branch from '../models/Branch.js';
import Guard from '../models/Guard.js';
import Availability from '../models/Availability.js';
import ShiftAttendance from '../models/ShiftAttendance.js';

import { ACTIONS } from "../middleware/logger.js";

Expand Down
132 changes: 76 additions & 56 deletions app-backend/src/controllers/shiftattendance.controller.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ShiftAttendance from "../models/ShiftAttendance.js";
import Shift from "../models/Shift.js";

// Utility: calculate distance using the Haversine formula
function calculateDistance(lat1, lon1, lat2, lon2) {
Expand All @@ -8,8 +9,8 @@ function calculateDistance(lat1, lon1, lat2, lon2) {
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos((lat1 * Math.PI) / 180) *
Math.cos((lat2 * Math.PI) / 180) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
Math.cos((lat2 * Math.PI) / 180) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c; // distance in km
}
Expand All @@ -19,69 +20,65 @@ export const checkIn = async (req, res) => {
try {
const { latitude, longitude } = req.body;
const { shiftId } = req.params;
const guardId = req.user.id;

// ✅ Validate inputs
if (
latitude === undefined ||
longitude === undefined ||
isNaN(latitude) ||
isNaN(longitude)
) {
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" });
}

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

// ✅ Check guard is assigned
const siteLatitude = shift.location?.latitude;
const siteLongitude = shift.location?.longitude;

if (!Number.isFinite(siteLatitude) || !Number.isFinite(siteLongitude)) {
return res.status(400).json({ message: "Shift location not configured" });
}

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

// ✅ Prevent duplicate check-in
const existing = await ShiftAttendance.findOne({ guardId, shiftId });
const existing = await ShiftAttendance.findOne({ guard: guardId, shift: shiftId });
if (existing) {
return res.status(400).json({ message: "Already checked in" });
}

// ✅ Get real site location
const siteLocation = shift.siteId?.location;
if (!siteLocation?.latitude || !siteLocation?.longitude) {
return res.status(400).json({ message: "Site location not configured" });
const distance = calculateDistance(latNum, lngNum, siteLatitude, siteLongitude);
if (distance > 0.1) {
return res.status(400).json({ message: "Not within shift radius (100m)" });
}

const distance = calculateDistance(
latitude,
longitude,
siteLocation.latitude,
siteLocation.longitude
);
const [startHour, startMinute] = String(shift.startTime).split(":").map(Number);
const [endHour, endMinute] = String(shift.endTime).split(":").map(Number);

// ✅ Radius check (100m)
if (distance > 0.1) {
return res.status(400).json({
message: "Not within shift radius (100m)",
});
const scheduledStart = new Date(shift.date);
scheduledStart.setHours(startHour, startMinute, 0, 0);

const scheduledEnd = new Date(shift.date);
scheduledEnd.setHours(endHour, endMinute, 0, 0);

if (scheduledEnd <= scheduledStart) {
scheduledEnd.setDate(scheduledEnd.getDate() + 1);
}

// ✅ Save attendance
const attendance = new ShiftAttendance({
guardId,
shiftId,
checkInTime: new Date(),
siteLocation: {
type: "Point",
coordinates: [siteLocation.longitude, siteLocation.latitude],
},
checkInLocation: {
type: "Point",
coordinates: [longitude, latitude],
},
locationVerified: true,
shift: shiftId,
guard: guardId,
clockIn: new Date(),
scheduledStart,
scheduledEnd,
recordedBy: guardId,
});

await attendance.save();
Expand All @@ -90,7 +87,6 @@ export const checkIn = async (req, res) => {
message: "Check-in recorded",
attendance,
});

} catch (error) {
return res.status(500).json({ message: error.message });
}
Expand All @@ -99,32 +95,56 @@ export const checkIn = async (req, res) => {
// POST /api/v1/attendance/checkout/:shiftId
export const checkOut = async (req, res) => {
try {
console.log("Incoming check-in request:", req.params, req.body);

const { latitude, longitude } = req.body;
const { shiftId } = req.params;
const guardId = req.user.id;
const guardId = req.user?._id || req.user?.id;

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

const attendance = await ShiftAttendance.findOne({ guardId, shiftId });
if (!attendance)
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;

attendance.checkOutTime = new Date();
attendance.checkOutLocation = { type: "Point", coordinates: [longitude, latitude] };
await attendance.save();

res.status(200).json({ message: "Check-out recorded", attendance });
return res.status(200).json({ message: "Check-out recorded", attendance });
} catch (error) {
res.status(500).json({ message: error.message });
return res.status(500).json({ message: error.message });
}
};
// GET /api/v1/attendance/:userId
export const getAttendanceByUserId = async (req, res) => {
try {
const { userId } = req.params;

const attendanceRecords = await ShiftAttendance.find({ guardId: userId })
.sort({ checkInTime: -1 });
const attendanceRecords = await ShiftAttendance.find({ guard: userId })
.sort({ clockIn: -1 });

if (!attendanceRecords.length) {
return res.status(404).json({
Expand Down
2 changes: 1 addition & 1 deletion app-backend/src/controllers/verification.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import GuardVerification from '../models/GuardVerification.js';
import ManualVerification from '../models/ManualVerification.js';
import Guard from '../models/Guard.js';
import { encryptLicence, decryptLicence } from '../utils/crypto.js';
import { encryptLicence } from '../utils/crypto.js';
import { verifyNSW } from '../adapters/verification/nswAdapter.js';
import { createManualVerification } from '../adapters/verification/manualAdapter.js';

Expand Down
2 changes: 1 addition & 1 deletion app-backend/src/middleware/errorHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Catches any errors passed via next(err)
* and returns a JSON response.
*/
export default function errorHandler(err, req, res, next) {
export default function errorHandler(err, req, res, _next) {
// Log full error for diagnostics
console.error(err);

Expand Down
4 changes: 1 addition & 3 deletions app-backend/src/routes/auth.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,12 +191,10 @@ const upload = multer({

// ---------------- MongoDB GridFS Setup ----------------
const mongoUri = process.env.MONGO_URI; // e.g., mongodb://localhost:27017/secureShift
let dbClient;
let gridFSBucket;

MongoClient.connect(mongoUri)
.then(client => {
dbClient = client;
const db = client.db(); // default DB from URI
gridFSBucket = new GridFSBucket(db, { bucketName: 'eoiDocuments' });
console.log('Connected to MongoDB GridFS for EOI uploads');
Expand Down Expand Up @@ -309,7 +307,7 @@ const fileInfos = await Promise.all(req.files.map(file => {
};

// Use your submitEOI controller to store eoiData in a collection
const { eoi, employerCreated } = await submitEOI(eoiData);
const { employerCreated } = await submitEOI(eoiData);

let message = 'EOI submitted successfully';
if (!employerCreated) {
Expand Down
1 change: 0 additions & 1 deletion app-backend/src/routes/verification.routes.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// src/routes/verification.routes.js
import { Router } from 'express';
import auth from '../middleware/auth.js';
import { adminOnly } from '../middleware/role.js';
import {
startVerification,
getStatus,
Expand Down
13 changes: 3 additions & 10 deletions app-backend/src/scripts/extractExpiryDate.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,17 @@ async function extract() {
//fullT = fullT.replace(/\s+/g,"").toLowerCase();

//extract all dates
const dateReg = /\b(\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}|\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2})\b/g;
const dateReg = /\b(\d{1,2}[/-]\d{1,2}[/-]\d{2,4}|\d{4}[/-]\d{1,2}[/-]\d{1,2})\b/g;
let dates = fullT.match(dateReg) || []; //creating an array of dates
dates = [...new Set(dates)]; // creating a set where duplicates are removed


const latest = dates.map(d => new Date(d.replace(/(\d{2})\/(\d{2})\/(\d{4})/,"$2/$1/$3")))
.sort((a, b) => b - a)[0]; //for finding the latest date to pinpoint the expiry date

//helper function to get our desired text from the full text
function getValueAndDate(text,value){
const regex = new RegExp(`${value}\\s*:?\\s*([\\w\\-/]+)`, "i");
const match = text.match(regex);
return match ? match[1].trim() : null;
}

//console.log(fullT);
const startDate = getValueAndDate(fullT,"cover start date");
const endtDate = getValueAndDate(fullT,"cover end date")
// const startDate = getValueAndDate(fullT,"cover start date");
// const endtDate = getValueAndDate(fullT,"cover end date")
//console.log("dates in document:", dates);
//console.log("cover start date:", startDate );
console.log("cover end date:" ,latest?.toLocaleDateString("en-AU"));
Expand Down
Loading