diff --git a/app.js b/app.js
new file mode 100644
index 00000000..387a2f26
--- /dev/null
+++ b/app.js
@@ -0,0 +1,20 @@
+// path/to/app.js
+import express from 'express';
+import bodyParser from 'body-parser';
+import cors from 'cors';
+import { sequelize } from './db';
+import elmLeaveRoutes from './modules/elm/routes/elm_leave_routes';
+
+const app = express();
+app.use(bodyParser.json());
+app.use(cors());
+
+sequelize.sync({ force: false }).then(() => {
+ console.log('Database synced');
+});
+
+app.use('/api', elmLeaveRoutes);
+
+app.listen(3000, () => {
+ console.log('Server is running on port 3000');
+});
diff --git a/elm_bundle.md b/elm_bundle.md
new file mode 100644
index 00000000..37307ebe
--- /dev/null
+++ b/elm_bundle.md
@@ -0,0 +1,42 @@
+# Employee Leave Management (ELM) Integration Specification
+
+## 1. Core Domain & Data Models
+* **Leave Categories & Entitlements:**
+ * **CL (Casual Leave):** 8 days/year, no carry-forward. Supports half-days (0.5 deduction). Continuous-day counting does *not* apply[cite: 3, 4].
+ * **RH (Restricted Holiday):** 2 days/year, no carry-forward. Must match effective published RH calendar[cite: 3, 4].
+ * **SCL (Special Casual Leave):** Up to 15 days/year, no carry-forward. Admissibility decided by authority[cite: 3, 4].
+ * **EL (Earned Leave):** 30 days/year for non-faculty. Unused EL carries forward. Continuous-day counting applies (weekends/holidays inside interval count as leave)[cite: 3, 4].
+ * **VL (Vacation Leave):** 60 days for faculty based on Academic Calendar. No carry-forward; unused VL converts to EL at year-end (2 VL : 1 EL)[cite: 3, 4].
+ * **COL (Commuted Leave):** 20 days/year. Requires medical evidence; carries forward. Continuous-day counting applies[cite: 3, 4].
+* **Station Leave:** Captured within the same leave application if the employee leaves Jabalpur; not a separate process[cite: 3, 4].
+* **Roles:** Employee, Substitute (Responsibility Holder), Unit Head, Establishment (Deputy Registrar), Dean, Registrar, Director, Leave Administrator[cite: 3].
+
+## 2. Core State Machine & Workflows
+All leaves follow variations of these strict state transitions[cite: 1]:
+1. **Draft / Not Submitted:** Initial state[cite: 1].
+2. **Awaiting Substitute Response:** If substitute is required, they must Accept to release the routing gate. If Declined, state moves to **Applicant Action Required** for replacement nomination[cite: 1, 4].
+3. **Awaiting Unit Head Review:**
+ * For CL/RH: Unit Head gives final `Approve` or `Reject`[cite: 1, 3].
+ * For SCL/EL/COL/VL: Unit Head records `Recommended` or `Not Recommended` (which is not a rejection) and routes forward[cite: 1, 3].
+4. **Awaiting Establishment Routing:** Optional configured step for higher-sanction verification[cite: 1, 3].
+5. **Awaiting Final Sanction Decision:** Final authority (Dean/Registrar/Director) records `Approve` or `Reject`[cite: 1, 3].
+* **Director Exception:** If the Director is the applicant, they use a self-sanction path bypassing Unit Head[cite: 1, 4].
+
+## 3. Lifecycle Actions & APIs (Use Cases)
+* **Withdraw (EL-UC-007):** A pending request can be withdrawn immediately at any time *before* final approval. Terminates all routing[cite: 2, 4].
+* **Cancel (EL-UC-008):** Approved leave can only be cancelled *before* the start date. Routes through original hierarchy. If approved, balance is restored; original leave is NOT edited in place[cite: 2, 4].
+* **Extend (EL-UC-009):** Only for ongoing EL/COL/VL. Approvals post *incremental* additional periods without modifying the original record[cite: 2, 4].
+* **Resume (EL-UC-010/011):** For EL/COL/VL. Employee submits actual return date. If early/late, Sanctioning Authority reviews, system calculates actual period, adjusts balance, and syncs attendance[cite: 2, 4].
+* **Offline Leave (EL-UC-016):** Admins can retrospectively sync authorized offline/paper leave without triggering a new approval workflow[cite: 2, 4].
+* **Year-End Processing (EL-UC-020):** Event-triggered. Lapses CL/RH/SCL, converts VL to EL, carries forward EL/COL[cite: 2, 4].
+
+## 4. Key Business Rules (Validation)
+* **Overlap Guard (BR-EL-012):** New requests cannot overlap with active pending/approved leave. Exception: First-half and second-half CL on the same date are allowed[cite: 4].
+* **Substitute Guard (BR-EL-014):** Nominee must be an employee, cannot be the applicant, and cannot have overlapping pending/approved leave[cite: 4].
+* **Immutable Pending State (BR-EL-034):** Submitted requests cannot be modified in place. The user must withdraw and submit a new application[cite: 4].
+* **Role-Based Auth (BR-EL-036):** All APIs must strictly validate role authorizations[cite: 4].
+
+## 5. Fusion ERP Integration Directives
+* **Auth & Roles:** Use existing Fusion authentication and role-management decorators/middleware.
+* **UI/UX:** Inherit common Fusion layouts, navigation, and reusable UI components. Do not build a standalone portal.
+* **Database:** Extend the existing Fusion database schema. Create normalized tables for `elm_leave_requests`, `elm_leave_balances`, `elm_substitute_logs`, and `elm_state_transitions`.```
\ No newline at end of file
diff --git a/elm_leave_balances.js b/elm_leave_balances.js
new file mode 100644
index 00000000..d7c9aa68
--- /dev/null
+++ b/elm_leave_balances.js
@@ -0,0 +1,45 @@
+// path/to/modules/elm/models/elm_leave_balances.js
+import { DataTypes, Model } from 'sequelize';
+
+class ElmLeaveBalance extends Model {}
+
+ElmLeaveBalance.init({
+ employeeId: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ references: {
+ model: 'users',
+ key: 'id',
+ },
+ onDelete: 'CASCADE',
+ },
+ categoryId: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ references: {
+ model: 'elm_leave_categories',
+ key: 'id',
+ },
+ onDelete: 'CASCADE',
+ },
+ balance: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ defaultValue: 0,
+ },
+ createdAt: {
+ type: DataTypes.DATE,
+ allowNull: false,
+ defaultValue: DataTypes.NOW,
+ },
+ updatedAt: {
+ type: DataTypes.DATE,
+ allowNull: false,
+ defaultValue: DataTypes.NOW,
+ },
+}, {
+ sequelize: null, // This will be set by the Sequelize instance
+ tableName: 'elm_leave_balances',
+});
+
+export default ElmLeaveBalance;
diff --git a/elm_leave_categories.js b/elm_leave_categories.js
new file mode 100644
index 00000000..f790a64b
--- /dev/null
+++ b/elm_leave_categories.js
@@ -0,0 +1,21 @@
+// path/to/modules/elm/models/elm_leave_categories.js
+import { DataTypes, Model } from 'sequelize';
+
+class ElmLeaveCategory extends Model {}
+
+ElmLeaveCategory.init({
+ name: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ unique: true,
+ },
+ description: {
+ type: DataTypes.TEXT,
+ allowNull: true,
+ },
+}, {
+ sequelize: null, // This will be set by the Sequelize instance
+ tableName: 'elm_leave_categories',
+});
+
+export default ElmLeaveCategory;
diff --git a/elm_leave_controller.js b/elm_leave_controller.js
new file mode 100644
index 00000000..521dbf56
--- /dev/null
+++ b/elm_leave_controller.js
@@ -0,0 +1,59 @@
+// path/to/modules/elm/controllers/elm_leave_controller.js
+import express from 'express';
+import { Op } from 'sequelize';
+import { ElmLeaveService } from '../services';
+
+const router = express.Router();
+const elmLeaveService = new ElmLeaveService();
+
+router.get('/leave', async (req, res) => {
+ const { employeeId } = req.query;
+ try {
+ const leaveRequests = await elmLeaveService.getLeaveRequests(employeeId);
+ res.status(200).json(leaveRequests);
+ } catch (error) {
+ res.status(400).json({ error: error.message });
+ }
+});
+
+router.get('/leave/:id', async (req, res) => {
+ const { id } = req.params;
+ try {
+ const leaveRequest = await elmLeaveService.getLeaveRequest(id);
+ res.status(200).json(leaveRequest);
+ } catch (error) {
+ res.status(400).json({ error: error.message });
+ }
+});
+
+router.post('/leave', async (req, res) => {
+ const { employeeId, categoryId, startDate, endDate } = req.body;
+ try {
+ const leaveRequest = await elmLeaveService.applyForLeave(employeeId, categoryId, startDate, endDate);
+ res.status(201).json(leaveRequest);
+ } catch (error) {
+ res.status(400).json({ error: error.message });
+ }
+});
+
+router.put('/leave/:id', async (req, res) => {
+ const { id, categoryId, startDate, endDate } = req.body;
+ try {
+ const leaveRequest = await elmLeaveService.updateLeaveRequest(id, categoryId, startDate, endDate);
+ res.status(200).json(leaveRequest);
+ } catch (error) {
+ res.status(400).json({ error: error.message });
+ }
+});
+
+router.delete('/leave/:id', async (req, res) => {
+ const { id } = req.params;
+ try {
+ await elmLeaveService.cancelLeave(id);
+ res.status(204).send();
+ } catch (error) {
+ res.status(400).json({ error: error.message });
+ }
+});
+
+export default router;
diff --git a/elm_leave_detail.js b/elm_leave_detail.js
new file mode 100644
index 00000000..dde71ed6
--- /dev/null
+++ b/elm_leave_detail.js
@@ -0,0 +1,56 @@
+// path/to/modules/elm/components/elm_leave_detail.js
+import React from 'react';
+import { Form, Input, DatePicker } from 'antd';
+import { useNavigate } from 'react-router-dom';
+import { useElmLeaveService } from '../services';
+
+const { RangePicker } = DatePicker;
+
+const ElmLeaveDetail = ({ leaveRequest }) => {
+ const [startDate, setStartDate] = useState(leaveRequest.startDate);
+ const [endDate, setEndDate] = useState(leaveRequest.endDate);
+ const [categoryId, setCategoryId] = useState(leaveRequest.categoryId);
+ const [categoryIdOptions, setCategoryIdOptions] = useState([]);
+ const navigate = useNavigate();
+ const elmLeaveService = useElmLeaveService();
+
+ const fetchLeaveCategories = async () => {
+ const categories = await elmLeaveService.getLeaveCategories();
+ setCategoryIdOptions(categories.map(category => ({ label: category.name, value: category.id })));
+ };
+
+ const handleSubmit = async () => {
+ try {
+ const updatedRequest = await elmLeaveService.updateLeaveRequest(
+ leaveRequest.id,
+ categoryId,
+ startDate,
+ endDate
+ );
+ navigate(`/leave/${updatedRequest.id}`);
+ } catch (error) {
+ alert(error.message);
+ }
+ };
+
+ return (
+
+
+
+
+ setStartDate(dates)} />
+
+
+ setEndDate(dates)} />
+
+
+
+
+
+ );
+};
+
+export default ElmLeaveDetail;
diff --git a/elm_leave_form.js b/elm_leave_form.js
new file mode 100644
index 00000000..005019b7
--- /dev/null
+++ b/elm_leave_form.js
@@ -0,0 +1,56 @@
+// path/to/modules/elm/components/elm_leave_form.js
+import React, { useState } from 'react';
+import { Button, Form, Input, DatePicker } from 'antd';
+import { useNavigate } from 'react-router-dom';
+import { useElmLeaveService } from '../services';
+
+const { RangePicker } = DatePicker;
+
+const ElmLeaveForm = () => {
+ const [startDate, setStartDate] = useState(null);
+ const [endDate, setEndDate] = useState(null);
+ const [categoryId, setCategoryId] = useState('');
+ const [categoryIdOptions, setCategoryIdOptions] = useState([]);
+ const navigate = useNavigate();
+ const elmLeaveService = useElmLeaveService();
+
+ const fetchLeaveCategories = async () => {
+ const categories = await elmLeaveService.getLeaveCategories();
+ setCategoryIdOptions(categories.map(category => ({ label: category.name, value: category.id })));
+ };
+
+ const handleSubmit = async () => {
+ try {
+ const leaveRequest = await elmLeaveService.applyForLeave(
+ localStorage.getItem('userId'),
+ categoryId,
+ startDate,
+ endDate
+ );
+ navigate(`/leave/${leaveRequest.id}`);
+ } catch (error) {
+ alert(error.message);
+ }
+ };
+
+ return (
+
+
+
+
+ setStartDate(dates)} />
+
+
+ setEndDate(dates)} />
+
+
+
+
+
+ );
+};
+
+export default ElmLeaveForm;
diff --git a/elm_leave_list.js b/elm_leave_list.js
new file mode 100644
index 00000000..1cb07405
--- /dev/null
+++ b/elm_leave_list.js
@@ -0,0 +1,71 @@
+// path/to/modules/elm/components/elm_leave_list.js
+import React from 'react';
+import { Table, Button } from 'antd';
+import { useNavigate } from 'react-router-dom';
+import { useElmLeaveService } from '../services';
+
+const columns = [
+ {
+ title: 'Employee',
+ dataIndex: 'employeeName',
+ key: 'employeeName',
+ },
+ {
+ title: 'Category',
+ dataIndex: 'categoryName',
+ key: 'categoryName',
+ },
+ {
+ title: 'Start Date',
+ dataIndex: 'startDate',
+ key: 'startDate',
+ },
+ {
+ title: 'End Date',
+ dataIndex: 'endDate',
+ key: 'endDate',
+ },
+ {
+ title: 'Status',
+ dataIndex: 'status',
+ key: 'status',
+ },
+ {
+ title: 'Action',
+ dataIndex: 'action',
+ key: 'action',
+ render: (text, record) => (
+
+ ),
+ },
+];
+
+const ElmLeaveList = () => {
+ const [leaveRequests, setLeaveRequests] = useState([]);
+ const navigate = useNavigate();
+ const elmLeaveService = useElmLeaveService();
+
+ const fetchLeaveRequests = async () => {
+ const requests = await elmLeaveService.getLeaveRequests(localStorage.getItem('userId'));
+ setLeaveRequests(requests.map(request => ({
+ id: request.id,
+ employeeName: request.employee.name,
+ categoryName: request.category.name,
+ startDate: request.startDate.toISOString().split('T')[0],
+ endDate: request.endDate.toISOString().split('T')[0],
+ status: request.status,
+ })));
+ };
+
+ React.useEffect(() => {
+ fetchLeaveRequests();
+ }, []);
+
+ return (
+
+ );
+};
+
+export default ElmLeaveList;
diff --git a/elm_leave_requests.js b/elm_leave_requests.js
new file mode 100644
index 00000000..28323d9e
--- /dev/null
+++ b/elm_leave_requests.js
@@ -0,0 +1,53 @@
+// path/to/modules/elm/models/elm_leave_requests.js
+import { DataTypes, Model } from 'sequelize';
+
+class ElmLeaveRequest extends Model {}
+
+ElmLeaveRequest.init({
+ employeeId: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ references: {
+ model: 'users',
+ key: 'id',
+ },
+ onDelete: 'CASCADE',
+ },
+ categoryId: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ references: {
+ model: 'elm_leave_categories',
+ key: 'id',
+ },
+ onDelete: 'CASCADE',
+ },
+ startDate: {
+ type: DataTypes.DATE,
+ allowNull: false,
+ },
+ endDate: {
+ type: DataTypes.DATE,
+ allowNull: false,
+ },
+ status: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ defaultValue: 'draft',
+ },
+ createdAt: {
+ type: DataTypes.DATE,
+ allowNull: false,
+ defaultValue: DataTypes.NOW,
+ },
+ updatedAt: {
+ type: DataTypes.DATE,
+ allowNull: false,
+ defaultValue: DataTypes.NOW,
+ },
+}, {
+ sequelize: null, // This will be set by the Sequelize instance
+ tableName: 'elm_leave_requests',
+});
+
+export default ElmLeaveRequest;
diff --git a/elm_leave_routes.js b/elm_leave_routes.js
new file mode 100644
index 00000000..22a0f21e
--- /dev/null
+++ b/elm_leave_routes.js
@@ -0,0 +1,59 @@
+// path/to/modules/elm/routes/elm_leave_routes.js
+import express from 'express';
+import elmLeaveController from './elm_leave_controller';
+import { useElmLeaveService } from '../services';
+
+const router = express.Router();
+const elmLeaveService = new ElmLeaveService();
+
+router.get('/leave', async (req, res) => {
+ const { employeeId } = req.query;
+ try {
+ const leaveRequests = await elmLeaveService.getLeaveRequests(employeeId);
+ res.status(200).json(leaveRequests);
+ } catch (error) {
+ res.status(400).json({ error: error.message });
+ }
+});
+
+router.get('/leave/:id', async (req, res) => {
+ const { id } = req.params;
+ try {
+ const leaveRequest = await elmLeaveService.getLeaveRequest(id);
+ res.status(200).json(leaveRequest);
+ } catch (error) {
+ res.status(400).json({ error: error.message });
+ }
+});
+
+router.post('/leave', async (req, res) => {
+ const { employeeId, categoryId, startDate, endDate } = req.body;
+ try {
+ const leaveRequest = await elmLeaveService.applyForLeave(employeeId, categoryId, startDate, endDate);
+ res.status(201).json(leaveRequest);
+ } catch (error) {
+ res.status(400).json({ error: error.message });
+ }
+});
+
+router.put('/leave/:id', async (req, res) => {
+ const { id, categoryId, startDate, endDate } = req.body;
+ try {
+ const leaveRequest = await elmLeaveService.updateLeaveRequest(id, categoryId, startDate, endDate);
+ res.status(200).json(leaveRequest);
+ } catch (error) {
+ res.status(400).json({ error: error.message });
+ }
+});
+
+router.delete('/leave/:id', async (req, res) => {
+ const { id } = req.params;
+ try {
+ await elmLeaveService.cancelLeave(id);
+ res.status(204).send();
+ } catch (error) {
+ res.status(400).json({ error: error.message });
+ }
+});
+
+export default router;
diff --git a/elm_leave_service.js b/elm_leave_service.js
new file mode 100644
index 00000000..8a38c35e
--- /dev/null
+++ b/elm_leave_service.js
@@ -0,0 +1,177 @@
+// path/to/modules/elm/services/elm_leave_service.js
+import { Op } from 'sequelize';
+import { ElmLeaveCategory, ElmLeaveBalance, ElmLeaveRequest } from '../models';
+
+class ElmLeaveService {
+ async applyForLeave(employeeId, categoryId, startDate, endDate) {
+ const leaveCategory = await ElmLeaveCategory.findByPk(categoryId);
+ if (!leaveCategory) {
+ throw new Error('Invalid leave category');
+ }
+
+ const balance = await ElmLeaveBalance.findOne({
+ where: {
+ employeeId,
+ categoryId,
+ },
+ });
+
+ if (!balance) {
+ throw new Error('Insufficient leave balance');
+ }
+
+ if (balance.balance < 1) {
+ throw new Error('Insufficient leave balance');
+ }
+
+ const leaveRequest = await ElmLeaveRequest.create({
+ employeeId,
+ categoryId,
+ startDate,
+ endDate,
+ status: 'draft',
+ });
+
+ balance.balance -= 1;
+ await balance.save();
+
+ return leaveRequest;
+ }
+
+ async respondToSubstitute(substituteId, leaveRequestId) {
+ const leaveRequest = await ElmLeaveRequest.findByPk(leaveRequestId);
+ if (!leaveRequest) {
+ throw new Error('Invalid leave request');
+ }
+
+ if (leaveRequest.status !== 'awaiting_substitute_response') {
+ throw new Error('Invalid leave request status');
+ }
+
+ const substitute = await User.findByPk(substituteId);
+ if (!substitute) {
+ throw new Error('Invalid substitute');
+ }
+
+ leaveRequest.substituteId = substituteId;
+ leaveRequest.status = 'awaiting_unit_head_review';
+ await leaveRequest.save();
+
+ return leaveRequest;
+ }
+
+ async approveLeave(leaveRequestId) {
+ const leaveRequest = await ElmLeaveRequest.findByPk(leaveRequestId);
+ if (!leaveRequest) {
+ throw new Error('Invalid leave request');
+ }
+
+ if (leaveRequest.status !== 'awaiting_unit_head_review') {
+ throw new Error('Invalid leave request status');
+ }
+
+ leaveRequest.status = 'awaiting_establishment_routing';
+ await leaveRequest.save();
+
+ return leaveRequest;
+ }
+
+ async cancelLeave(leaveRequestId) {
+ const leaveRequest = await ElmLeaveRequest.findByPk(leaveRequestId);
+ if (!leaveRequest) {
+ throw new Error('Invalid leave request');
+ }
+
+ if (leaveRequest.status !== 'submitted') {
+ throw new Error('Invalid leave request status');
+ }
+
+ leaveRequest.status = 'cancelled';
+ await leaveRequest.save();
+
+ return leaveRequest;
+ }
+
+ async extendLeave(leaveRequestId, additionalDays) {
+ const leaveRequest = await ElmLeaveRequest.findByPk(leaveRequestId);
+ if (!leaveRequest) {
+ throw new Error('Invalid leave request');
+ }
+
+ if (leaveRequest.status !== 'ongoing') {
+ throw new Error('Invalid leave request status');
+ }
+
+ leaveRequest.endDate = leaveRequest.endDate.addDays(additionalDays);
+ await leaveRequest.save();
+
+ return leaveRequest;
+ }
+
+ async resumeLeave(leaveRequestId, actualReturnDate) {
+ const leaveRequest = await ElmLeaveRequest.findByPk(leaveRequestId);
+ if (!leaveRequest) {
+ throw new Error('Invalid leave request');
+ }
+
+ if (leaveRequest.status !== 'ongoing') {
+ throw new Error('Invalid leave request status');
+ }
+
+ leaveRequest.endDate = actualReturnDate;
+ await leaveRequest.save();
+
+ return leaveRequest;
+ }
+
+ async withdrawLeave(leaveRequestId) {
+ const leaveRequest = await ElmLeaveRequest.findByPk(leaveRequestId);
+ if (!leaveRequest) {
+ throw new Error('Invalid leave request');
+ }
+
+ if (leaveRequest.status !== 'draft') {
+ throw new Error('Invalid leave request status');
+ }
+
+ leaveRequest.status = 'withdrawn';
+ await leaveRequest.save();
+
+ return leaveRequest;
+ }
+
+ async getLeaveRequests(employeeId) {
+ const leaveRequests = await ElmLeaveRequest.findAll({
+ where: {
+ employeeId,
+ },
+ });
+
+ return leaveRequests;
+ }
+
+ async getLeaveRequest(id) {
+ const leaveRequest = await ElmLeaveRequest.findByPk(id);
+ if (!leaveRequest) {
+ throw new Error('Invalid leave request');
+ }
+
+ return leaveRequest;
+ }
+
+ async updateLeaveRequest(id, categoryId, startDate, endDate) {
+ const leaveRequest = await ElmLeaveRequest.findByPk(id);
+ if (!leaveRequest) {
+ throw new Error('Invalid leave request');
+ }
+
+ leaveRequest.categoryId = categoryId;
+ leaveRequest.startDate = startDate;
+ leaveRequest.endDate = endDate;
+ await leaveRequest.save();
+
+ return leaveRequest;
+ }
+}
+
+export default ElmLeaveService;