Skip to content
Merged
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
1,287 changes: 461 additions & 826 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"express-validator": "^7.2.1",
"jsonwebtoken": "^9.0.2",
"mathjs": "^15.1.0",
"minio": "^8.0.6",
"minio": "^8.0.7",
"multer": "^2.0.2",
"nodemailer": "^7.0.11",
"pg": "^8.16.3",
Expand All @@ -43,6 +43,6 @@
"glob": "10.5.0",
"validator": "^13.15.22",
"js-yaml": "3.14.2",
"fast-xml-parser": "5.3.4"
"fast-xml-parser": "^5.4.1"
}
}
3 changes: 3 additions & 0 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import thirdPartiesRoutes from './src/routes/thirdparties.js';
import cashRequestRoutes from './src/routes/cashRequestRoutes.js';
import ahpRoutes from './src/routes/ahpRoutes.js';
import authRoutes from './src/routes/auth.js';
import outsidePartyRequestRoutes from './src/routes/outsidePartyRequests.js';

const app = express();
const server = http.createServer(app);
Expand All @@ -28,12 +29,14 @@ app.use(cors({ origin: '*' }));
app.use('/api/health', healthRoutes);
app.use('/api/v1/auth', authRoutes);
app.use('/api/v1/cash-requests', cashRequestRoutes);
app.use('/api/v1/petty-cash-requests', cashRequestRoutes);
app.use('/api/v1/issues', issueRoutes);
app.use('/api/v1/users', userRoutes);
app.use('/api/v1/messages', messageRoutes);
app.use('/api/v1/branches', branchRoutes);
app.use('/api/v1/thirdparties', thirdPartiesRoutes);
app.use('/api/v1/ahp', ahpRoutes);
app.use('/api/v1/outside-party-requests', outsidePartyRequestRoutes);

// Basic route
app.get('/api/', (req, res) => {
Expand Down
23 changes: 16 additions & 7 deletions src/controllers/cashRequestController.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export const createCashRequest = async (req, res) => {
});
}

const { technician_id, issue_id, amount, description } = req.body;
const { technician_id, issue_id, amount, description } = parseCreateBody(req.body);

const newCashRequest = await CashRequestService.createCashRequest({
technician_id,
Expand All @@ -111,12 +111,12 @@ export const createCashRequest = async (req, res) => {
description
});

try {
// Notify via realtime update that a new cash request has been created for the issue
issueRealtimeUpdate(issue_id, newCashRequest);
} catch (err) {
console.error('issueRealtimeUpdate error after creating cash request:', err);
}
// try {
// // Notify via realtime update that a new cash request has been created for the issue
// issueRealtimeUpdate(issue_id, newCashRequest);
// } catch (err) {
// console.error('issueRealtimeUpdate error after creating cash request:', err);
// }

res.status(201).json({
success: true,
Expand All @@ -133,6 +133,15 @@ export const createCashRequest = async (req, res) => {
}
};

// Ensure createCashRequest receives parsed numbers (express may pass strings from JSON)
function parseCreateBody(body) {
const technician_id = body.technician_id != null ? parseInt(body.technician_id, 10) : undefined;
const issue_id = body.issue_id != null ? parseInt(body.issue_id, 10) : undefined;
const amount = body.amount != null ? parseFloat(body.amount) : undefined;
const description = typeof body.description === 'string' ? body.description.trim() : body.description;
return { technician_id, issue_id, amount, description };
}

/**
* Update an existing cash request
* @route PUT /api/v1/cash-requests/:id
Expand Down
52 changes: 29 additions & 23 deletions src/controllers/issueController.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,38 @@ class IssueController {
// POST /api/issues - Create new issue
async createIssue(req, res) {
try {
const {
branch_id,
title,
manager_id,
description,
maintenance_executive_id,
technician_id,
const {
branch_id,
title,
manager_id,
description,
maintenance_executive_id,
technician_id,
status,
third_party_id
third_party_id
} = req.body;

// Validation
if (!branch_id || !title || !manager_id) {
if (!branch_id || !title) {
return res.status(400).json({
success: false,
message: 'Branch ID, title, and manager ID are required'
message: 'Branch ID and title are required'
});
}

const issueData = {
branch_id: parseInt(branch_id),
title,
manager_id: parseInt(manager_id),
description
};

// Include manager_id only if it is a valid non-zero integer
// When it's 0 or absent, issueService will auto-assign a branch manager
const parsedManagerId = manager_id ? parseInt(manager_id) : 0;
if (parsedManagerId > 0) {
issueData.manager_id = parsedManagerId;
}

// Add optional fields if provided
if (maintenance_executive_id) {
issueData.maintenance_executive_id = parseInt(maintenance_executive_id);
Expand All @@ -49,7 +55,7 @@ class IssueController {

if (result.success) {
// Notify all Maintenance Executives about the new issue(real-time)
try { notifyNewIssue(result); } catch (e) { console.error(e);}
try { notifyNewIssue(result); } catch (e) { console.error(e); }

try {
// Create dynamic namespaces for real-time communication
Expand All @@ -74,17 +80,17 @@ class IssueController {
// GET /api/issues - Get all issues
async getAllIssues(req, res) {
try {
const {
branch_id,
manager_id,
technician_id,
const {
branch_id,
manager_id,
technician_id,
maintenance_executive_id,
third_party_id,
status,
search,
limit,
search,
limit,
offset,
include_relations
include_relations
} = req.query;

const filters = {};
Expand Down Expand Up @@ -299,7 +305,7 @@ class IssueController {
}

// Notify all Maintenance Executives about the new issue(real-time) update with assigned ME
try { notifyNewIssue(result); } catch (e) { console.error(e);}
try { notifyNewIssue(result); } catch (e) { console.error(e); }

return res.status(200).json(result);
} else {
Expand Down Expand Up @@ -377,7 +383,7 @@ class IssueController {
});
}

const validStatuses = ['Open', 'In Progress', 'Done', 'Closed'];
const validStatuses = ['Open', 'In Progress', 'Pending Resolution', 'Pending Close', 'Done', 'Closed'];
if (!validStatuses.includes(status)) {
return res.status(400).json({
success: false,
Expand All @@ -396,10 +402,10 @@ class IssueController {
}

// Notify Maintenance Executives about the issue status update(real-time in home dashboard)
try { notifyNewIssue(result); } catch (e) { console.error(e);}
try { notifyNewIssue(result); } catch (e) { console.error(e); }

// Notify assigned technician about the issue status update(real-time in home dashboard)
try { notifyAssign(result.data.technician_id, result); } catch (e) { console.error(e);}
try { notifyAssign(result.data.technician_id, result); } catch (e) { console.error(e); }

// Remove all connections and delete the namespace for the issue
if (result.data.status === 'Closed' || result.data.status === 'Done') {
Expand Down
90 changes: 90 additions & 0 deletions src/controllers/outsidePartyRequestController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import * as OutsidePartyRequestService from '../services/outsidePartyRequestService.js';
import { issueRealtimeUpdate } from '../socket/socket.js';

/** GET /api/v1/outside-party-requests */
export const getAll = async (req, res) => {
try {
const filters = {};
if (req.query.issue_id) filters.issue_id = req.query.issue_id;
if (req.query.status) filters.status = req.query.status;
if (req.query.suggested_by) filters.suggested_by = req.query.suggested_by;
const data = await OutsidePartyRequestService.getAll(filters);
res.json({ success: true, count: data.length, data });
} catch (e) {
res.status(500).json({ success: false, message: e.message });
}
};

/** GET /api/v1/outside-party-requests/:id */
export const getOne = async (req, res) => {
try {
const data = await OutsidePartyRequestService.getById(req.params.id);
if (!data) return res.status(404).json({ success: false, message: 'Not found' });
res.json({ success: true, data });
} catch (e) {
res.status(500).json({ success: false, message: e.message });
}
};

/** POST /api/v1/outside-party-requests */
export const create = async (req, res) => {
try {
const data = await OutsidePartyRequestService.create(req.body);
try { issueRealtimeUpdate(data.issue_id, data); } catch (_) { }
res.status(201).json({ success: true, data });
} catch (e) {
res.status(400).json({ success: false, message: e.message });
}
};

/** PATCH /api/v1/outside-party-requests/:id/approve */
export const approve = async (req, res) => {
try {
const approverId = req.body.approved_by || req.body.user_id;
const comment = req.body.comment || null;

// Role check — only branch_manager / maintenance_executive
const userRole = req.body.user_role;
if (userRole && !['branch_manager', 'maintenance_executive'].includes(userRole)) {
return res.status(403).json({ success: false, message: 'Permission denied' });
}

const data = await OutsidePartyRequestService.approve(req.params.id, approverId, comment);
if (!data) return res.status(404).json({ success: false, message: 'Not found' });
try { issueRealtimeUpdate(data.issue_id, { ...data, event_type: 'outside_party_approved' }); } catch (_) { }
res.json({ success: true, data });
} catch (e) {
res.status(400).json({ success: false, message: e.message });
}
};

/** PATCH /api/v1/outside-party-requests/:id/reject */
export const reject = async (req, res) => {
try {
const approverId = req.body.approved_by || req.body.user_id;
const comment = req.body.comment || null;

const userRole = req.body.user_role;
if (userRole && !['branch_manager', 'maintenance_executive'].includes(userRole)) {
return res.status(403).json({ success: false, message: 'Permission denied' });
}

const data = await OutsidePartyRequestService.reject(req.params.id, approverId, comment);
if (!data) return res.status(404).json({ success: false, message: 'Not found' });
try { issueRealtimeUpdate(data.issue_id, { ...data, event_type: 'outside_party_rejected' }); } catch (_) { }
res.json({ success: true, data });
} catch (e) {
res.status(400).json({ success: false, message: e.message });
}
};

/** DELETE /api/v1/outside-party-requests/:id */
export const remove = async (req, res) => {
try {
const deleted = await OutsidePartyRequestService.remove(req.params.id);
if (!deleted) return res.status(404).json({ success: false, message: 'Not found' });
res.json({ success: true, message: 'Deleted' });
} catch (e) {
res.status(500).json({ success: false, message: e.message });
}
};
96 changes: 96 additions & 0 deletions src/controllers/statusController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import statusService from '../services/statusService.js';
import { statusUpdateLogCreated } from '../socket/socket.js';

class StatusController {
/**
* POST /api/v1/issues/:id/statuses
* Create a status update log for an issue.
*/
async createStatusUpdate(req, res) {
try {
const { id } = req.params;
const { user_id, description, image_url, image_urls, status_type } = req.body;

if (!id || isNaN(id)) {
return res.status(400).json({ success: false, message: 'Valid issue ID is required' });
}
if (!user_id) {
return res.status(400).json({ success: false, message: 'User ID is required' });
}
if (!description || description.trim() === '') {
return res.status(400).json({ success: false, message: 'Description is required' });
}

// Validate image_urls if provided
if (image_urls && !Array.isArray(image_urls)) {
return res.status(400).json({ success: false, message: 'image_urls must be an array' });
}

const validTypes = ['Open', 'Assigned', 'In Progress', 'Resolved', 'Closed'];
if (status_type && !validTypes.includes(status_type)) {
return res.status(400).json({
success: false,
message: `status_type must be one of: ${validTypes.join(', ')}`
});
}

const result = await statusService.createStatusUpdate({
issue_id: parseInt(id),
user_id: parseInt(user_id),
description: description.trim(),
image_url: image_url || null,
image_urls: image_urls || null,
status_type: status_type || 'Open'
});

if (result.success) {
// Emit socket event for real-time status log update
try {
statusUpdateLogCreated(parseInt(id), result.data);
} catch (socketErr) {
console.error('Socket emit failed for status update:', socketErr);
}

return res.status(201).json(result);
} else {
return res.status(400).json(result);
}
} catch (error) {
return res.status(500).json({
success: false,
message: 'Internal server error',
error: error.message
});
}
}

/**
* GET /api/v1/issues/:id/statuses
* Get all status updates for an issue.
*/
async getStatusUpdates(req, res) {
try {
const { id } = req.params;

if (!id || isNaN(id)) {
return res.status(400).json({ success: false, message: 'Valid issue ID is required' });
}

const result = await statusService.getStatusUpdatesByIssue(parseInt(id));

if (result.success) {
return res.status(200).json(result);
} else {
return res.status(400).json(result);
}
} catch (error) {
return res.status(500).json({
success: false,
message: 'Internal server error',
error: error.message
});
}
}
}

export default new StatusController();
Loading