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
2 changes: 1 addition & 1 deletion backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
NODE_ENV=development
PORT=3000
PORT=5000
FRONTEND_URL=http://localhost:5173
JWT_SECRET=change_me
DATABASE_URL=DATABASE_URL=postgresql://postgres.[project-ref]:[password]@aws-1-ap-south-1.pooler.supabase.com:5432/postgres
Expand Down
5 changes: 4 additions & 1 deletion backend/src/middleware/errorHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

export const errorHandler = (err, _req, res, _next) => {
if (err instanceof ApiError) {
return res.status(err.statusCode).json({ message: err.message, details: err.details });
return res
.status(err.statusCode)
.json({ message: err.message, details: err.details });
}
console.error(err);
return res.status(500).json({ message: "Internal server error" });
};
39 changes: 39 additions & 0 deletions backend/src/modules/approvalRules/approvalRules.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { asyncHandler } from "../../utils/asyncHandler.js";
import { approvalRulesModel } from "./approvalRules.model.js";

const VALID_TYPES = ["sequential", "percentage", "hybrid"];

export const getCurrentRule = asyncHandler(async (req, res) => {
const companyId = req.user.companyId;
const result = await approvalRulesModel.getLatestByCompany(companyId);

if (!result.rows[0]) {
return res.status(404).json({ message: "No approval rule configured" });
}

res.json(result.rows[0]);
});

export const getRuleHistory = asyncHandler(async (req, res) => {
const companyId = req.user.companyId;
const result = await approvalRulesModel.getHistoryByCompany(companyId);
res.json(result.rows);
});

export const saveRule = asyncHandler(async (req, res) => {
const companyId = req.user.companyId;
const { type, config } = req.body;

if (!VALID_TYPES.includes(type)) {
return res.status(400).json({
message: `type must be one of: ${VALID_TYPES.join(", ")}`,
});
}

if (!config || typeof config !== "object" || Array.isArray(config)) {
return res.status(400).json({ message: "config must be an object" });
}

const result = await approvalRulesModel.create({ companyId, type, config });
res.status(201).json(result.rows[0]);
});
Comment on lines +23 to +39
33 changes: 33 additions & 0 deletions backend/src/modules/approvalRules/approvalRules.model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { query } from "../../config/db.js";

export const approvalRulesModel = {
async getLatestByCompany(companyId) {
return query(
`SELECT id, company_id, type, config, created_at
FROM approval_rules
WHERE company_id = $1
ORDER BY created_at DESC
LIMIT 1`,
[companyId],
);
},

async getHistoryByCompany(companyId) {
return query(
`SELECT id, company_id, type, config, created_at
FROM approval_rules
WHERE company_id = $1
ORDER BY created_at DESC`,
[companyId],
);
},

async create({ companyId, type, config }) {
return query(
`INSERT INTO approval_rules (company_id, type, config)
VALUES ($1, $2, $3)
RETURNING id, company_id, type, config, created_at`,
[companyId, type, config],
);
},
};
17 changes: 17 additions & 0 deletions backend/src/modules/approvalRules/approvalRules.routes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Router } from "express";
import { authenticate } from "../../middleware/authenticate.js";
import { authorize } from "../../middleware/authorize.js";
import {
getCurrentRule,
getRuleHistory,
saveRule,
} from "./approvalRules.controller.js";

const router = Router();
router.use(authenticate);

router.get("/", authorize("admin"), getCurrentRule);
router.get("/history", authorize("admin"), getRuleHistory);
router.put("/", authorize("admin"), saveRule);

export default router;
2 changes: 2 additions & 0 deletions backend/src/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import companyRoutes from "../modules/companies/companies.routes.js";
import expenseRoutes from "../modules/expenses/expenses.routes.js";
import analyticsRoutes from "../modules/analytics/analytics.routes.js";
import notificationRoutes from "../modules/notifications/notifications.routes.js";
import approvalRulesRoutes from "../modules/approvalRules/approvalRules.routes.js";

//these are routes

const router = Router();
router.use("/auth", authRoutes);
router.use("/users", userRoutes);
router.use("/approval-rules", approvalRulesRoutes);
router.use("/companies", companyRoutes);
router.use("/expenses", expenseRoutes);
router.use("/analytics", analyticsRoutes);
Expand Down
6 changes: 3 additions & 3 deletions frontend/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
VITE_API_URL=http://localhost:3000/api
VITE_API_BASE_URL=http://localhost:3000/api
VITE_SOCKET_URL=http://localhost:3000
VITE_API_URL=http://localhost:5000/api
VITE_API_BASE_URL=http://localhost:5000/api
VITE_SOCKET_URL=http://localhost:5000
15 changes: 10 additions & 5 deletions frontend/src/api/approvalService.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import api from "./axios";

export async function getPending() {
const { data } = await api.get("/expenses/pending");
return data?.data || [];
const { data } = await api.get("/expenses/pending");
return data?.data || [];
}

export async function saveWorkflowRules(payload) {
const { data } = await api.put("/workflows/rules", payload);
return data?.rule || data;
export async function getApprovalRules() {
const { data } = await api.get("/approval-rules");
return data;
}

export async function saveApprovalRules(payload) {
const { data } = await api.put("/approval-rules", payload);
return data;
}
Loading