"use server"; import { createAdminClient } from "@/lib/supabase/server"; import { auth } from "@clerk/nextjs/server"; // ── Helpers ────────────────────────────────────────────────── async function getOrgAndUserId() { const { userId } = await auth(); if (!userId) throw new Error("Unauthenticated"); const supabase = await createAdminClient(); const { data, error } = await supabase .from("organisation_members") .select("organisation_id, role") .eq("user_id", userId) .single(); if (error || !data) throw new Error("No organisation found"); return { orgId: data.organisation_id, userId, role: data.role }; } // ── Types ──────────────────────────────────────────────────── export interface AccrualRule { id: string; organisation_id: string; leave_type_id: string; accrual_rate: number; accrual_frequency: "monthly" | "fortnightly" | "weekly"; max_balance: number | null; created_at: string; } export interface AccrualRuleWithLeaveType extends AccrualRule { leave_type: { id: string; name: string } | null; } export interface EmployeeLeaveBalance { leave_type_id: string; leave_type_name: string; total_accrued: number; total_taken: number; approved_pending: number; remaining: number; } // ── Fetch functions ────────────────────────────────────────── export async function fetchAccrualRules(): Promise { const { orgId } = await getOrgAndUserId(); const supabase = await createAdminClient(); const { data, error } = await supabase .from("leave_accrual_rules") .select("*, leave_type:leave_types(id, name)") .eq("organisation_id", orgId); if (error) throw error; return (data || []) as AccrualRuleWithLeaveType[]; } export async function fetchLeaveTypesForAccrual(): Promise< { id: string; name: string }[] > { const { orgId } = await getOrgAndUserId(); const supabase = await createAdminClient(); const { data, error } = await supabase .from("leave_types") .select("id, name") .eq("organisation_id", orgId) .order("name", { ascending: true }); if (error) throw error; return (data || []) as { id: string; name: string }[]; } // ── Server actions (writes) ────────────────────────────────── export async function saveAccrualRule( leaveTypeId: string, accrualRate: number, frequency: "monthly" | "fortnightly" | "weekly", maxBalance: number | null ): Promise<{ success: boolean; error?: string }> { const { orgId } = await getOrgAndUserId(); const supabase = await createAdminClient(); const { error } = await supabase .from("leave_accrual_rules") .upsert( { organisation_id: orgId, leave_type_id: leaveTypeId, accrual_rate: accrualRate, accrual_frequency: frequency, max_balance: maxBalance, }, { onConflict: "organisation_id,leave_type_id" } ); if (error) return { success: false, error: error.message }; return { success: true }; } export async function runAccrual(): Promise<{ success: boolean; count?: number; error?: string; }> { const { orgId } = await getOrgAndUserId(); const supabase = await createAdminClient(); // Fetch accrual rules for this organisation const { data: rules, error: rulesError } = await supabase .from("leave_accrual_rules") .select("*") .eq("organisation_id", orgId); if (rulesError) return { success: false, error: rulesError.message }; if (!rules || rules.length === 0) return { success: true, count: 0 }; // Fetch active employees const { data: employees, error: empError } = await supabase .from("employees") .select("id, start_date, created_at") .eq("organisation_id", orgId) .eq("status", "active"); if (empError) return { success: false, error: empError.message }; if (!employees || employees.length === 0) return { success: true, count: 0 }; let count = 0; const now = new Date(); for (const emp of employees) { for (const rule of rules) { const startDate = new Date(emp.start_date || emp.created_at); const monthsSince = (now.getFullYear() - startDate.getFullYear()) * 12 + now.getMonth() - startDate.getMonth(); // Calculate earned leave based on frequency let earned = 0; if (rule.accrual_frequency === "monthly") { earned = Math.max(0, monthsSince * Number(rule.accrual_rate)); } else if (rule.accrual_frequency === "fortnightly") { earned = Math.max(0, monthsSince * 2 * Number(rule.accrual_rate)); } else { // weekly earned = Math.max(0, monthsSince * 4 * Number(rule.accrual_rate)); } // Cap at max balance if set if (rule.max_balance) { earned = Math.min(earned, Number(rule.max_balance)); } // Get current accrued balance from the latest accrued entry const { data: existing } = await supabase .from("leave_requests") .select("accrued_balance") .eq("organisation_id", orgId) .eq("employee_id", emp.id) .eq("leave_type_id", rule.leave_type_id) .eq("status", "accrued") .order("created_at", { ascending: false }) .single(); const currentBalance = existing?.accrued_balance || 0; const toAccrue = earned - currentBalance; if (toAccrue > 0.001) { const { error: insertError } = await supabase .from("leave_requests") .insert({ organisation_id: orgId, employee_id: emp.id, leave_type_id: rule.leave_type_id, status: "accrued", start_date: now.toISOString().split("T")[0], end_date: now.toISOString().split("T")[0], days_count: 0, accrued_balance: earned, }); if (!insertError) { count++; } } } } return { success: true, count }; } export async function fetchEmployeeLeaveBalanceAccrual( employeeId: string ): Promise { const { orgId } = await getOrgAndUserId(); const supabase = await createAdminClient(); // Get accrual rules with leave type info const { data: rules, error: rulesError } = await supabase .from("leave_accrual_rules") .select("*, leave_type:leave_types(id, name)") .eq("organisation_id", orgId); if (rulesError) throw rulesError; if (!rules || rules.length === 0) return []; const balances: EmployeeLeaveBalance[] = []; for (const rule of rules) { // Get all leave request entries for this employee + leave type const { data: entries, error: entriesError } = await supabase .from("leave_requests") .select("status, days_count, accrued_balance") .eq("organisation_id", orgId) .eq("employee_id", employeeId) .eq("leave_type_id", rule.leave_type_id); if (entriesError) throw entriesError; const totalAccrued = entries ?.filter((e) => e.status === "accrued") .reduce((sum, e) => sum + Number(e.accrued_balance || 0), 0) || 0; const totalTaken = entries ?.filter((e) => e.status === "approved") .reduce((sum, e) => sum + Number(e.days_count || 0), 0) || 0; const approvedPending = entries ?.filter((e) => e.status === "pending") .reduce((sum, e) => sum + Number(e.days_count || 0), 0) || 0; balances.push({ leave_type_id: rule.leave_type_id, leave_type_name: rule.leave_type?.name || "Unknown", total_accrued: totalAccrued, total_taken: totalTaken, approved_pending: approvedPending, remaining: Math.max(0, totalAccrued - totalTaken), }); } return balances; }