Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
34df884
feat(elm): step 1 - module foundation (apps.py and registry.py)
uttkarshbagad Oct 11, 2026
0a8abd7
feat(elm): step 2 - add initial Django ORM models generated by Qwen2.…
uttkarshbagad Oct 11, 2026
9334a30
feat(elm): step 3 - add domain policy and leave entitlement rules
uttkarshbagad Oct 11, 2026
22ba2dd
feat(elm): step 4 - add leave application and substitute nomination s…
uttkarshbagad Oct 11, 2026
4d125e0
feat(elm): step 5 - add REST API serializers, views, and URL routes
uttkarshbagad Oct 11, 2026
89d3f9f
feat(elm): step 6 - register ELM module in settings and root URLs
uttkarshbagad Oct 11, 2026
2a50ef1
feat(elm): step 7 - add initial database migration for ELM models
uttkarshbagad Oct 11, 2026
e55ab89
feat(elm): step 8 - client wiring (API client, routes, and module reg…
uttkarshbagad Oct 11, 2026
f4a695e
feat(elm): step 9 - add LeaveBalance and LeaveApplication React pages
uttkarshbagad Oct 11, 2026
1b74fe9
fix(elm): step 10 - mechanical integration fixes for Fusion boundarie…
uttkarshbagad Oct 11, 2026
f1f996f
docs(pilot): step 11 - add Trial 1 prompts, interaction log, and summary
uttkarshbagad Oct 11, 2026
e4afea2
feat(elm): update models with workflow statuses and reasons
uttkarshbagad Oct 12, 2026
ba5d211
docs(phase2): add Phase 2 engineering log for Step 1
uttkarshbagad Oct 12, 2026
a5968b9
docs(phase2): rename log to changes_made_by_24bcs260
uttkarshbagad Oct 12, 2026
9e9507e
feat(elm): add pure-Python domain business rules and transition valid…
uttkarshbagad Oct 15, 2026
ae43983
feat(elm): integrate domain rules and workflow actions into applicati…
uttkarshbagad Oct 16, 2026
187c83b
docs(phase2): clean up Step 3 prompt documentation
uttkarshbagad Oct 16, 2026
916bed9
feat(elm): add schema migration for workflow statuses and reasons
uttkarshbagad Oct 16, 2026
937b9dc
test(elm): add automated pytest suite for domain policy business rules
uttkarshbagad Oct 16, 2026
fa0e6a3
feat(elm): wire complete REST API views, serializers, and routes
uttkarshbagad Oct 16, 2026
4d1a882
feat(elm): wire LeaveBalance and LeaveApplication frontend to REST API
uttkarshbagad Oct 16, 2026
7809065
docs(phase2): organize engineering log by commit hashes
uttkarshbagad Oct 16, 2026
41b5c20
Add ELM leave application history
Sanjanarai44 Oct 16, 2026
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
6 changes: 6 additions & 0 deletions .importlinter
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ source_modules =
modules.placement
modules.directory
modules.accesscontrol
modules.elm
forbidden_modules =
modules.placement.models
modules.placement.services
Expand All @@ -25,6 +26,10 @@ forbidden_modules =
modules.directory.services
modules.directory.api
modules.accesscontrol.models
modules.elm.models
modules.elm.services
modules.elm.api
modules.elm.domain
# DIRECT imports only. contracts.py legitimately imports its OWN module's
# models and services, so following the chain transitively would make this
# contract impossible to satisfy. What we forbid is a module reaching straight
Expand All @@ -38,6 +43,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
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"),
},
};
128 changes: 128 additions & 0 deletions client/src/modules/elm/LeaveApplication.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { type FormEvent, useState } from "react";
import { submitLeaveApplication } from "./api";

const LeaveApplication = () => {
const [leaveType, setLeaveType] = useState("1");
const [startDate, setStartDate] = useState("");
const [endDate, setEndDate] = useState("");
const [reason, setReason] = useState("");
const [loading, setLoading] = useState<boolean>(false);
const [feedback, setFeedback] = useState<{ type: "success" | "error"; message: string } | null>(null);

const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setLoading(true);
setFeedback(null);
try {
await submitLeaveApplication({
leave_type: parseInt(leaveType, 10),
start_date: startDate,
end_date: endDate,
reason,
});
setFeedback({ type: "success", message: "Leave application submitted successfully!" });
setStartDate("");
setEndDate("");
setReason("");
} catch (err: any) {
setFeedback({
type: "error",
message: err.response?.data?.detail || "Failed to submit leave application.",
});
} finally {
setLoading(false);
}
};

return (
<div style={{ padding: "1.5rem", maxWidth: "600px" }}>
<h1>Apply for Leave</h1>

{feedback && (
<div
style={{
padding: "0.75rem",
marginBottom: "1rem",
borderRadius: "4px",
backgroundColor: feedback.type === "success" ? "#e6ffed" : "#ffeef0",
color: feedback.type === "success" ? "#1a7f37" : "#cf222e",
border: `1px solid ${feedback.type === "success" ? "#a8f0b7" : "#ffc1c0"}`,
}}
>
{feedback.message}
</div>
)}

<form onSubmit={handleSubmit} style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
<div>
<label htmlFor="leaveType">Leave Type:</label>
<select
id="leaveType"
value={leaveType}
onChange={(e) => setLeaveType(e.target.value)}
style={{ width: "100%", padding: "0.5rem", marginTop: "0.25rem" }}
>
<option value="1">Casual Leave (CL)</option>
<option value="2">Recreational Holiday (RH)</option>
<option value="3">Special Casual Leave (SCL)</option>
<option value="4">Earned Leave (EL)</option>
<option value="5">Vacation Leave (VL)</option>
<option value="6">Compensatory Off Leave (COL)</option>
</select>
</div>

<div>
<label htmlFor="startDate">Start Date:</label>
<input
type="date"
id="startDate"
required
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
style={{ width: "100%", padding: "0.5rem", marginTop: "0.25rem" }}
/>
</div>

<div>
<label htmlFor="endDate">End Date:</label>
<input
type="date"
id="endDate"
required
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
style={{ width: "100%", padding: "0.5rem", marginTop: "0.25rem" }}
/>
</div>

<div>
<label htmlFor="reason">Reason:</label>
<textarea
id="reason"
rows={3}
value={reason}
onChange={(e) => setReason(e.target.value)}
style={{ width: "100%", padding: "0.5rem", marginTop: "0.25rem" }}
/>
</div>

<button
type="submit"
disabled={loading}
style={{
padding: "0.75rem",
backgroundColor: "#0969da",
color: "#fff",
border: "none",
borderRadius: "4px",
cursor: loading ? "not-allowed" : "pointer",
}}
>
{loading ? "Submitting..." : "Submit Application"}
</button>
</form>
</div>
);
};

export default LeaveApplication;
50 changes: 50 additions & 0 deletions client/src/modules/elm/LeaveBalance.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useEffect, useState } from "react";
import { fetchLeaveBalances, type LeaveBalanceRow } from "./api";

const LeaveBalance = () => {
const [leaveBalances, setLeaveBalances] = useState<LeaveBalanceRow[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
const fetchBalances = async () => {
try {
const balances = await fetchLeaveBalances();
setLeaveBalances(balances);
} catch (err: any) {
setError(err.response?.data?.detail || "Failed to fetch leave balances.");
} finally {
setLoading(false);
}
};

fetchBalances();
}, []);

if (loading) return <p>Loading leave balances...</p>;
if (error) return <p style={{ color: "red" }}>Error: {error}</p>;

return (
<div style={{ padding: "1.5rem" }}>
<h1>Leave Balances</h1>
<table style={{ width: "100%", borderCollapse: "collapse", marginTop: "1rem" }}>
<thead>
<tr style={{ textAlign: "left", borderBottom: "2px solid #ccc" }}>
<th style={{ padding: "0.5rem" }}>Leave Type ID</th>
<th style={{ padding: "0.5rem" }}>Balance (Days)</th>
</tr>
</thead>
<tbody>
{leaveBalances.map((balance) => (
<tr key={balance.id} style={{ borderBottom: "1px solid #eee" }}>
<td style={{ padding: "0.5rem" }}>{balance.leave_type}</td>
<td style={{ padding: "0.5rem" }}>{balance.balance}</td>
</tr>
))}
</tbody>
</table>
</div>
);
};

export default LeaveBalance;
150 changes: 150 additions & 0 deletions client/src/modules/elm/LeaveHistory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@

import { useEffect, useState } from "react";
import {
fetchLeaveApplications,
withdrawLeaveApplication,
type LeaveApplicationRow,
} from "./api";

function canWithdraw(status: string): boolean {
const normalized = status.toUpperCase().replace(/[\s-]+/g, "_");
return (
normalized === "AWAITING_SUBSTITUTE" ||
normalized === "AWAITING_APPROVAL"
);
}

export default function LeaveHistory() {
const [applications, setApplications] = useState<LeaveApplicationRow[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [busyId, setBusyId] = useState<number | null>(null);
const [message, setMessage] = useState("");

async function loadApplications() {
setLoading(true);
setError("");

try {
const rows = await fetchLeaveApplications();
setApplications(rows);
} catch {
setError("Unable to load leave applications. Please try again.");
} finally {
setLoading(false);
}
}

useEffect(() => {
void loadApplications();
}, []);

async function handleWithdraw(id: number) {
if (!window.confirm(`Withdraw leave application #${id}?`)) return;

setBusyId(id);
setError("");
setMessage("");

try {
await withdrawLeaveApplication(id);
setMessage(`Application #${id} withdrawn successfully.`);
await loadApplications();
} catch {
setError("Withdrawal failed. The application may no longer be eligible.");
} finally {
setBusyId(null);
}
}

return (
<main className="mx-auto max-w-5xl p-6">
<div className="mb-6 flex items-center justify-between gap-4">
<h1 className="text-2xl font-bold">Leave Application History</h1>
<button
type="button"
onClick={() => void loadApplications()}
disabled={loading}
className="rounded border px-4 py-2 disabled:opacity-50"
>
Refresh
</button>
</div>

{message && (
<p role="status" className="mb-4 text-green-700">
{message}
</p>
)}

{error && (
<div role="alert" className="mb-4 text-red-600">
<p>{error}</p>
<button
type="button"
onClick={() => void loadApplications()}
className="mt-2 underline"
>
Try again
</button>
</div>
)}

{loading ? (
<p role="status">Loading applications...</p>
) : applications.length === 0 ? (
<p>No leave applications found.</p>
) : (
<div className="overflow-x-auto rounded-lg border">
<table className="w-full text-left text-sm">
<thead className="bg-gray-100">
<tr>
<th className="p-3">ID</th>
<th className="p-3">Leave Type</th>
<th className="p-3">Start Date</th>
<th className="p-3">End Date</th>
<th className="p-3">Reason</th>
<th className="p-3">Status</th>
<th className="p-3">Action</th>
</tr>
</thead>
<tbody>
{applications.map((application) => (
<tr key={application.id} className="border-t">
<td className="p-3">{application.id}</td>
<td className="p-3">
{`Leave type #${application.leave_type}`}
</td>
<td className="p-3">{application.start_date}</td>
<td className="p-3">{application.end_date}</td>
<td className="p-3">
{application.reason || "—"}
</td>
<td className="p-3">
{application.status.replace(/_/g, " ")}
</td>
<td className="p-3">
{canWithdraw(application.status) ? (
<button
type="button"
onClick={() => void handleWithdraw(application.id)}
disabled={busyId !== null}
className="rounded border border-red-600 px-3 py-1 text-red-600 disabled:opacity-50"
>
{busyId === application.id
? "Withdrawing..."
: "Withdraw"}
</button>
) : (
"—"
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</main>
);
}
Loading