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
17 changes: 17 additions & 0 deletions .importlinter
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ name = Modules may reach each other ONLY through contracts.py
type = forbidden
source_modules =
modules.placement
modules.elm
modules.directory
modules.accesscontrol
forbidden_modules =
Expand All @@ -21,6 +22,11 @@ forbidden_modules =
modules.placement.selectors
modules.placement.api
modules.placement.domain
modules.elm.models
modules.elm.services
modules.elm.selectors
modules.elm.api
modules.elm.domain
modules.directory.models
modules.directory.services
modules.directory.api
Expand All @@ -38,6 +44,7 @@ type = forbidden
source_modules =
# Every module's domain, not just the first one written.
modules.placement.domain
modules.elm.domain
forbidden_modules =
django
rest_framework
Expand Down Expand Up @@ -66,3 +73,13 @@ layers =
modules.placement.selectors
modules.placement.models
modules.placement.domain

[importlinter:contract:elm-layers]
name = Layering inside elm
type = layers
layers =
modules.elm.api
modules.elm.services
modules.elm.selectors
modules.elm.models
modules.elm.domain
5 changes: 5 additions & 0 deletions client/src/app/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,9 @@ export const MODULE_REGISTRY: Record<string, ModuleManifest> = {
basePath: "/placement",
load: () => import("../modules/placement/routes"),
},
elm: {
code: "elm",
basePath: "/elm",
load: () => import("../modules/elm/routes"),
},
};
72 changes: 72 additions & 0 deletions client/src/modules/elm/pages/AdminPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from "react";
import {
Card,
Title,
Table,
Badge,
Button,
Stack,
Text,
Group,
TextInput,
Select,
} from "@mantine/core";

export default function AdminPage() {
const holidays = [
{ date: "2026-01-26", name: "Republic Day", type: "closed" },
{ date: "2026-03-25", name: "Holi", type: "closed" },
{ date: "2026-04-14", name: "Ambedkar Jayanti", type: "restricted" },
{ date: "2026-08-15", name: "Independence Day", type: "closed" },
{ date: "2026-10-02", name: "Gandhi Jayanti", type: "closed" },
];

return (
<Stack gap="lg">
<Title order={2}>Leave Administration & Calendars</Title>
<Text c="dimmed" size="sm">
Maintain annual holiday calendars, publish Restricted Holidays (RH), and execute year-end processing.
</Text>

<Card withBorder radius="md" p="md">
<Group justify="space-between" mb="md">
<Title order={4}>Published Holiday Calendar (2026)</Title>
<Button size="xs" color="blue">Add Holiday / RH</Button>
</Group>

<Table striped>
<Table.Thead>
<Table.Tr>
<Table.Th>Date</Table.Th>
<Table.Th>Holiday Name</Table.Th>
<Table.Th>Type</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{holidays.map((h) => (
<Table.Tr key={h.date}>
<Table.Td>{h.date}</Table.Td>
<Table.Td>{h.name}</Table.Td>
<Table.Td>
<Badge color={h.type === "closed" ? "red" : "violet"}>
{h.type === "closed" ? "Closed Holiday" : "Restricted Holiday (RH)"}
</Badge>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Card>

<Card withBorder radius="md" p="md">
<Title order={4} mb="xs">Year-End Leave Ledger Processing (EVT-01)</Title>
<Text size="sm" c="dimmed" mb="md">
Lapses unused CL/RH/SCL, converts Faculty VL to EL at 2:1 ratio, and carries forward EL/COL to next calendar year.
</Text>
<Button color="orange" variant="light">
Run Year-End Processing
</Button>
</Card>
</Stack>
);
}
151 changes: 151 additions & 0 deletions client/src/modules/elm/pages/ApplyLeavePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import React, { useState } from "react";
import {
Card,
Title,
TextInput,
Select,
Button,
Stack,
Text,
Checkbox,
Group,
Textarea,
Notification,
} from "@mantine/core";

export default function ApplyLeavePage() {
const [category, setCategory] = useState<string | null>("CL");
const [startDate, setStartDate] = useState("");
const [endDate, setEndDate] = useState("");
const [isHalfDay, setIsHalfDay] = useState(false);
const [halfDayPeriod, setHalfDayPeriod] = useState<string | null>("first_half");
const [reason, setReason] = useState("");
const [leavingStation, setLeavingStation] = useState(false);
const [stationAddress, setStationAddress] = useState("");
const [emergencyPhone, setEmergencyPhone] = useState("");
const [substituteId, setSubstituteId] = useState("");
const [submitted, setSubmitted] = useState(false);

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setSubmitted(true);
};

return (
<Stack gap="lg" maw={700}>
<Title order={2}>Apply for Leave</Title>
<Text c="dimmed" size="sm">
Submit a new leave application with role-based routing and substitute nomination.
</Text>

{submitted && (
<Notification color="teal" title="Application Submitted" onClose={() => setSubmitted(false)}>
Your leave application has been submitted for review.
</Notification>
)}

<Card withBorder padding="lg" radius="md">
<form onSubmit={handleSubmit}>
<Stack gap="md">
<Select
label="Leave Category"
placeholder="Select category"
data={[
{ value: "CL", label: "Casual Leave (CL)" },
{ value: "RH", label: "Restricted Holiday (RH)" },
{ value: "SCL", label: "Special Casual Leave (SCL)" },
{ value: "EL", label: "Earned Leave (EL)" },
{ value: "VL", label: "Vacation Leave (VL)" },
{ value: "COL", label: "Commuted Leave (COL - Medical)" },
]}
value={category}
onChange={setCategory}
required
/>

<Group grow>
<TextInput
label="Start Date"
type="date"
value={startDate}
onChange={(e) => setStartDate(e.currentTarget.value)}
required
/>
<TextInput
label="End Date"
type="date"
value={endDate}
onChange={(e) => setEndDate(e.currentTarget.value)}
required
/>
</Group>

{category === "CL" && (
<Group>
<Checkbox
label="Half Day Leave (0.5 CL)"
checked={isHalfDay}
onChange={(e) => setIsHalfDay(e.currentTarget.checked)}
/>
{isHalfDay && (
<Select
placeholder="Select Half"
data={[
{ value: "first_half", label: "First Half" },
{ value: "second_half", label: "Second Half" },
]}
value={halfDayPeriod}
onChange={setHalfDayPeriod}
/>
)}
</Group>
)}

<Textarea
label="Purpose / Reason"
placeholder="State the reason for leave"
value={reason}
onChange={(e) => setReason(e.currentTarget.value)}
minRows={3}
required
/>

<TextInput
label="Substitute Employee ID (Optional)"
placeholder="Enter User ID of nominated colleague"
value={substituteId}
onChange={(e) => setSubstituteId(e.currentTarget.value)}
/>

<Checkbox
label="Station Leave Required (Leaving Jabalpur)"
checked={leavingStation}
onChange={(e) => setLeavingStation(e.currentTarget.checked)}
/>

{leavingStation && (
<Stack gap="xs">
<TextInput
label="Contact Address while on leave"
placeholder="Address outside station"
value={stationAddress}
onChange={(e) => setStationAddress(e.currentTarget.value)}
/>
<TextInput
label="Emergency Phone"
placeholder="Emergency contact number"
value={emergencyPhone}
onChange={(e) => setEmergencyPhone(e.currentTarget.value)}
/>
</Stack>
)}

<Button type="submit" color="blue" mt="md">
Submit Leave Application
</Button>
</Stack>
</form>
</Card>
</Stack>
);
}
91 changes: 91 additions & 0 deletions client/src/modules/elm/pages/ApprovalsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React from "react";
import {
Card,
Title,
Table,
Badge,
Button,
Stack,
Text,
Group,
} from "@mantine/core";

interface ApprovalsPageProps {
role: "unit_head" | "establishment" | "sanction";
}

export default function ApprovalsPage({ role }: ApprovalsPageProps) {
const titles = {
unit_head: "Unit Head Leave Reviews",
establishment: "Establishment Verification Queue",
sanction: "Competent Authority Sanction Decisions",
};

const pendingList = [
{
id: 201,
employeeName: "Dr. A. Verma",
category: "EL",
days: 4,
period: "2026-11-10 to 2026-11-13",
substitute: "Accepted (Dr. K. Patel)",
stationLeave: "Yes (Delhi conference)",
},
];

return (
<Stack gap="lg">
<Title order={2}>{titles[role]}</Title>
<Text c="dimmed" size="sm">
Process leave applications awaiting your review or sanction decision.
</Text>

<Card withBorder radius="md" p="md">
<Table striped>
<Table.Thead>
<Table.Tr>
<Table.Th>ID</Table.Th>
<Table.Th>Employee</Table.Th>
<Table.Th>Category</Table.Th>
<Table.Th>Days</Table.Th>
<Table.Th>Period</Table.Th>
<Table.Th>Substitute Status</Table.Th>
<Table.Th>Station Leave</Table.Th>
<Table.Th>Action</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{pendingList.map((p) => (
<Table.Tr key={p.id}>
<Table.Td>#{p.id}</Table.Td>
<Table.Td>{p.employeeName}</Table.Td>
<Table.Td><Badge color="blue">{p.category}</Badge></Table.Td>
<Table.Td>{p.days}</Table.Td>
<Table.Td>{p.period}</Table.Td>
<Table.Td><Badge color="teal" variant="light">{p.substitute}</Badge></Table.Td>
<Table.Td>{p.stationLeave}</Table.Td>
<Table.Td>
{role === "unit_head" && (
<Group gap="xs">
<Button size="xs" color="teal">Recommend</Button>
<Button size="xs" color="orange" variant="light">Not Recommend</Button>
</Group>
)}
{role === "establishment" && (
<Button size="xs" color="blue">Verify & Route</Button>
)}
{role === "sanction" && (
<Group gap="xs">
<Button size="xs" color="teal">Sanction</Button>
<Button size="xs" color="red" variant="subtle">Reject</Button>
</Group>
)}
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Card>
</Stack>
);
}
Loading