Ensured consistent day calculation using stored days field and removed duplicate daysBetween logic. Implemented working-day-only calculation, excluding weekends (Saturday & Sunday), and blocked invalid date selections. Corrected handling of Unpaid leave so it does not affect leave balance. Added routing and integrated Sonner notifications for login, manager actions, and employee leave submission.
188 lines
6.0 KiB
TypeScript
188 lines
6.0 KiB
TypeScript
export interface LeaveRequest {
|
||
id: number;
|
||
userId: number;
|
||
userName?: string;
|
||
type: "Sick" | "Casual" | "Annual" | "Unpaid";
|
||
fromDate: string;
|
||
toDate: string;
|
||
/** Immutable day count stored at apply-time; used for all balance calculations. */
|
||
days: number;
|
||
reason: string;
|
||
status: "Pending" | "Approved" | "Rejected";
|
||
appliedAt: string;
|
||
}
|
||
|
||
export interface UserRecord {
|
||
id: number;
|
||
username: string;
|
||
name: string;
|
||
role: "employee" | "manager";
|
||
leaveBalance?: number;
|
||
}
|
||
|
||
const LEAVES_KEY = "lms_leaves";
|
||
const USERS_KEY = "lms_users";
|
||
|
||
// Seed from JSON on first load
|
||
async function seedIfNeeded() {
|
||
if (typeof window === "undefined") return;
|
||
if (!localStorage.getItem(LEAVES_KEY)) {
|
||
const mod = await import("@/data/leaves.json");
|
||
const raw = mod.default ?? mod;
|
||
const leaves = Array.isArray(raw) ? raw : ((raw as any).leaves ?? []);
|
||
localStorage.setItem(LEAVES_KEY, JSON.stringify(leaves));
|
||
}
|
||
if (!localStorage.getItem(USERS_KEY)) {
|
||
const mod = await import("@/data/users.json");
|
||
const raw = mod.default ?? mod;
|
||
const users = Array.isArray(raw) ? raw : ((raw as any).users ?? []);
|
||
localStorage.setItem(USERS_KEY, JSON.stringify(users));
|
||
}
|
||
}
|
||
|
||
export async function getLeaves(): Promise<LeaveRequest[]> {
|
||
await seedIfNeeded();
|
||
const raw = localStorage.getItem(LEAVES_KEY) ?? "[]";
|
||
const leaves: LeaveRequest[] = JSON.parse(raw);
|
||
// Attach userName from users
|
||
const users = getUsers();
|
||
return leaves.map((l) => {
|
||
const u = users.find((u) => u.id === l.userId);
|
||
return { ...l, userName: u?.name ?? "Unknown" };
|
||
});
|
||
}
|
||
|
||
export function getUsers(): UserRecord[] {
|
||
const raw = localStorage.getItem(USERS_KEY) ?? "[]";
|
||
return JSON.parse(raw);
|
||
}
|
||
|
||
export async function getLeavesForUser(userId: number): Promise<LeaveRequest[]> {
|
||
const all = await getLeaves();
|
||
return all.filter((l) => l.userId === userId);
|
||
}
|
||
|
||
export async function applyLeave(
|
||
userId: number,
|
||
data: { type: LeaveRequest["type"]; fromDate: string; toDate: string; reason: string }
|
||
): Promise<{ ok: boolean; error?: string }> {
|
||
await seedIfNeeded();
|
||
|
||
// Block if fromDate or toDate is a weekend
|
||
if (isWeekend(data.fromDate) || isWeekend(data.toDate)) {
|
||
return { ok: false, error: "Leave cannot start or end on a weekend (Saturday/Sunday)." };
|
||
}
|
||
|
||
// Calculate days using the shared util (no inline duplication)
|
||
const days = daysBetween(data.fromDate, data.toDate);
|
||
|
||
if (days === 0) {
|
||
return { ok: false, error: "Selected range has no working days." };
|
||
}
|
||
|
||
// Check balance — account for already-pending requests
|
||
const users = getUsers();
|
||
const user = users.find((u) => u.id === userId);
|
||
if (!user) return { ok: false, error: "User not found" };
|
||
|
||
const leaves: LeaveRequest[] = JSON.parse(localStorage.getItem(LEAVES_KEY) ?? "[]");
|
||
|
||
// Only check balance for non-Unpaid leave
|
||
if (data.type !== "Unpaid") {
|
||
const pendingDays = leaves
|
||
.filter((l) => l.userId === userId && l.status === "Pending" && l.type !== "Unpaid")
|
||
.reduce((sum, l) => sum + (l.days ?? daysBetween(l.fromDate, l.toDate)), 0);
|
||
|
||
const availableBalance = (user.leaveBalance ?? 0) - pendingDays;
|
||
if (availableBalance < days)
|
||
return {
|
||
ok: false,
|
||
error: `Insufficient balance. You have ${user.leaveBalance} total days, but ${pendingDays} are reserved by pending requests (${availableBalance} available).`,
|
||
};
|
||
}
|
||
|
||
const newLeave: LeaveRequest = {
|
||
id: Date.now(),
|
||
userId,
|
||
type: data.type,
|
||
fromDate: data.fromDate,
|
||
toDate: data.toDate,
|
||
days,
|
||
reason: data.reason,
|
||
status: "Pending",
|
||
appliedAt: new Date().toISOString().split("T")[0],
|
||
};
|
||
leaves.push(newLeave);
|
||
localStorage.setItem(LEAVES_KEY, JSON.stringify(leaves));
|
||
return { ok: true };
|
||
}
|
||
|
||
export async function updateLeaveStatus(
|
||
leaveId: number,
|
||
status: "Approved" | "Rejected"
|
||
): Promise<void> {
|
||
await seedIfNeeded();
|
||
const leaves: LeaveRequest[] = JSON.parse(localStorage.getItem(LEAVES_KEY) ?? "[]");
|
||
const idx = leaves.findIndex((l) => l.id === leaveId);
|
||
if (idx === -1) return;
|
||
|
||
const leave = leaves[idx];
|
||
const wasApproved = leave.status === "Approved";
|
||
leaves[idx] = { ...leave, status };
|
||
localStorage.setItem(LEAVES_KEY, JSON.stringify(leaves));
|
||
|
||
// Adjust balance using the immutable `days` stored at apply-time.
|
||
// Fallback to daysBetween for legacy records that pre-date the `days` field.
|
||
const users = getUsers();
|
||
const user = users.find((u) => u.id === leave.userId);
|
||
if (!user || user.role !== "employee") return;
|
||
|
||
const days = leave.days ?? daysBetween(leave.fromDate, leave.toDate);
|
||
|
||
// Only adjust balance for non-Unpaid leave
|
||
if (leave.type !== "Unpaid") {
|
||
if (status === "Approved" && !wasApproved) {
|
||
user.leaveBalance = (user.leaveBalance ?? 0) - days;
|
||
} else if (status === "Rejected" && wasApproved) {
|
||
user.leaveBalance = (user.leaveBalance ?? 0) + days;
|
||
}
|
||
|
||
localStorage.setItem(USERS_KEY, JSON.stringify(users));
|
||
|
||
// Also update session user if it's the same person
|
||
const sessionUser = localStorage.getItem("lms_user");
|
||
if (sessionUser) {
|
||
const su = JSON.parse(sessionUser);
|
||
if (su.id === user.id) {
|
||
su.leaveBalance = user.leaveBalance;
|
||
localStorage.setItem("lms_user", JSON.stringify(su));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
export function getUserBalance(userId: number): number {
|
||
const users = getUsers();
|
||
const u = users.find((u) => u.id === userId);
|
||
return u?.leaveBalance ?? 0;
|
||
}
|
||
|
||
// Returns true if date is Saturday or Sunday
|
||
export function isWeekend(dateStr: string): boolean {
|
||
const day = new Date(dateStr).getDay();
|
||
return day === 0 || day === 6;
|
||
}
|
||
|
||
// Count only working days (Mon–Fri) between two dates
|
||
export function daysBetween(from: string, to: string): number {
|
||
const start = new Date(from);
|
||
const end = new Date(to);
|
||
let count = 0;
|
||
const current = new Date(start);
|
||
while (current <= end) {
|
||
const day = current.getDay();
|
||
if (day !== 0 && day !== 6) count++;
|
||
current.setDate(current.getDate() + 1);
|
||
}
|
||
return count;
|
||
} |