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
20 changes: 20 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
@@ -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');
});
42 changes: 42 additions & 0 deletions elm_bundle.md
Original file line number Diff line number Diff line change
@@ -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`.```
45 changes: 45 additions & 0 deletions elm_leave_balances.js
Original file line number Diff line number Diff line change
@@ -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;
21 changes: 21 additions & 0 deletions elm_leave_categories.js
Original file line number Diff line number Diff line change
@@ -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;
59 changes: 59 additions & 0 deletions elm_leave_controller.js
Original file line number Diff line number Diff line change
@@ -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;
56 changes: 56 additions & 0 deletions elm_leave_detail.js
Original file line number Diff line number Diff line change
@@ -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 (
<Form onFinish={handleSubmit}>
<Form.Item label="Leave Category" name="categoryId">
<Select options={categoryIdOptions} />
</Form.Item>
<Form.Item label="Start Date" name="startDate">
<RangePicker onChange={(dates) => setStartDate(dates)} />
</Form.Item>
<Form.Item label="End Date" name="endDate">
<RangePicker onChange={(dates) => setEndDate(dates)} />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Update Leave Request
</Button>
</Form.Item>
</Form>
);
};

export default ElmLeaveDetail;
56 changes: 56 additions & 0 deletions elm_leave_form.js
Original file line number Diff line number Diff line change
@@ -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 (
<Form onFinish={handleSubmit}>
<Form.Item label="Leave Category" name="categoryId">
<Select options={categoryIdOptions} />
</Form.Item>
<Form.Item label="Start Date" name="startDate">
<RangePicker onChange={(dates) => setStartDate(dates)} />
</Form.Item>
<Form.Item label="End Date" name="endDate">
<RangePicker onChange={(dates) => setEndDate(dates)} />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Apply for Leave
</Button>
</Form.Item>
</Form>
);
};

export default ElmLeaveForm;
71 changes: 71 additions & 0 deletions elm_leave_list.js
Original file line number Diff line number Diff line change
@@ -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) => (
<Button type="link" onClick={() => navigate(`/leave/${record.id}`)}>
View
</Button>
),
},
];

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 (
<Table columns={columns} dataSource={leaveRequests} />
);
};

export default ElmLeaveList;
Loading