"use server"; import { createAdminClient } from "@/lib/supabase/server"; import { auth } from "@clerk/nextjs/server"; import type { Permission } from "@/lib/permissions"; import { hasBypassRole, hasBuiltInPermission, PAGE_PERMISSIONS, DEFAULT_CUSTOM_ROLES, resolvePermissions, enforceDependencies } from "@/lib/permissions"; async function getOrgAndUserId(): Promise<{ orgId: string; userId: string }> { const { userId } = await auth(); if (!userId) throw new Error("Unauthenticated"); const supabase = await createAdminClient(); const { data: member } = await supabase .from("organisation_members") .select("organisation_id, role") .eq("user_id", userId) .single(); if (!member) throw new Error("No organisation found"); return { orgId: member.organisation_id, userId }; } /** * Get all permissions for the current user. * Combines built-in org role + custom employee roles. */ export async function getUserPermissions(): Promise<{ orgRole: string; permissions: Set; hierarchyLevel: number; }> { const { orgId, userId } = await getOrgAndUserId(); const supabase = await createAdminClient(); // Get org role const { data: member } = await supabase .from("organisation_members") .select("role") .eq("user_id", userId) .eq("organisation_id", orgId) .single(); const orgRole = member?.role || "guest"; // Built-in bypass (owner, admin, it_team) if (hasBypassRole(orgRole)) { const { BUILTIN_ROLE_LEVELS } = await import("@/lib/permissions"); return { orgRole, permissions: new Set(), hierarchyLevel: BUILTIN_ROLE_LEVELS[orgRole] || 0 }; } const permissions = new Set(); // Built-in role permissions const { BUILTIN_ROLE_PERMISSIONS, BUILTIN_ROLE_LEVELS } = await import("@/lib/permissions"); for (const p of (BUILTIN_ROLE_PERMISSIONS[orgRole] || [])) { permissions.add(p); } let hierarchyLevel = BUILTIN_ROLE_LEVELS[orgRole] || 0; // Custom employee roles (via employee record) const { data: employee } = await supabase .from("employees") .select("id") .eq("organisation_id", orgId) .eq("user_id", userId) .single(); if (employee) { const { data: empRoles } = await supabase .from("employee_roles") .select("role:roles(id, name, hierarchy_level, role_permissions(permission))") .eq("employee_id", employee.id); for (const er of empRoles || []) { const role = er.role as any; if (role.hierarchy_level > hierarchyLevel) { hierarchyLevel = role.hierarchy_level; } for (const rp of role.role_permissions || []) { permissions.add(rp.permission); } } } // Apply can_manage_admin resolution + dependency enforcement const resolved = enforceDependencies(resolvePermissions(permissions)); return { orgRole, permissions: resolved, hierarchyLevel }; } /** * Check if the current user can access a page. */ export async function canAccessPage(pageKey: string): Promise { const { orgRole, permissions } = await getUserPermissions(); if (hasBypassRole(orgRole)) return true; const required = (PAGE_PERMISSIONS as Record)[pageKey]; if (!required || required.length === 0) return true; return required.some((p) => permissions.has(p)); } /** * Check if the current user has a specific permission. */ export async function hasPermission(permission: Permission): Promise { const { orgRole, permissions } = await getUserPermissions(); if (hasBypassRole(orgRole)) return true; return permissions.has(permission); } /** * Get the current user's employee record with roles. */ export async function getCurrentEmployee(): Promise { const { orgId, userId } = await getOrgAndUserId(); const supabase = await createAdminClient(); const { data } = await supabase .from("employees") .select("*, department:departments(name), employee_roles(role:roles(id, name, hierarchy_level))") .eq("organisation_id", orgId) .eq("user_id", userId) .single(); return data; } // ── Role Management ────────────────────────────────────────── export async function fetchCustomRoles(): Promise { const { orgId } = await getOrgAndUserId(); const supabase = await createAdminClient(); const { data } = await supabase .from("roles") .select("*, role_permissions(permission)") .eq("organisation_id", orgId) .order("hierarchy_level", { ascending: false }); return data || []; } export async function createCustomRole( name: string, hierarchyLevel: number, permissions: Permission[], color: string = '#1D9E75', canManageLower: boolean = false ): Promise<{ success: boolean; roleId?: string; error?: string }> { const { orgId } = await getOrgAndUserId(); const supabase = await createAdminClient(); const { data, error } = await supabase .from("roles") .insert({ organisation_id: orgId, name, hierarchy_level: hierarchyLevel, color, can_manage_lower: canManageLower }) .select("id") .single(); if (error) return { success: false, error: error.message }; if (permissions.length > 0) { await supabase .from("role_permissions") .insert(permissions.map((p) => ({ role_id: data.id, permission: p }))); } return { success: true, roleId: data.id }; } export async function updateCustomRole( roleId: string, name: string, hierarchyLevel: number, permissions: Permission[], color: string = '#1D9E75', canManageLower: boolean = false ): Promise<{ success: boolean; error?: string }> { const { orgId } = await getOrgAndUserId(); const supabase = await createAdminClient(); await supabase .from("roles") .update({ name, hierarchy_level: hierarchyLevel, color, can_manage_lower: canManageLower }) .eq("id", roleId) .eq("organisation_id", orgId); // Replace permissions await supabase .from("role_permissions") .delete() .eq("role_id", roleId); if (permissions.length > 0) { await supabase .from("role_permissions") .insert(permissions.map((p) => ({ role_id: roleId, permission: p }))); } return { success: true }; } export async function deleteCustomRole(roleId: string): Promise<{ success: boolean; error?: string }> { const { orgId } = await getOrgAndUserId(); const supabase = await createAdminClient(); const { error } = await supabase .from("roles") .delete() .eq("id", roleId) .eq("organisation_id", orgId); if (error) return { success: false, error: error.message }; return { success: true }; } // ── Employee Role Assignment ───────────────────────────────── export async function assignRoleToEmployee( employeeId: string, roleId: string ): Promise<{ success: boolean; error?: string }> { const { orgId } = await getOrgAndUserId(); const supabase = await createAdminClient(); // Verify employee belongs to org const { data: emp } = await supabase .from("employees") .select("id") .eq("id", employeeId) .eq("organisation_id", orgId) .single(); if (!emp) return { success: false, error: "Employee not found" }; // Verify role belongs to org const { data: role } = await supabase .from("roles") .select("id") .eq("id", roleId) .eq("organisation_id", orgId) .single(); if (!role) return { success: false, error: "Role not found" }; const { error } = await supabase .from("employee_roles") .insert({ employee_id: employeeId, role_id: roleId }) .select() .single(); // Ignore unique violation (already assigned) if (error && error.code !== "23505") return { success: false, error: error.message }; return { success: true }; } export async function removeRoleFromEmployee( employeeId: string, roleId: string ): Promise<{ success: boolean; error?: string }> { const { orgId } = await getOrgAndUserId(); const supabase = await createAdminClient(); const { error } = await supabase .from("employee_roles") .delete() .eq("employee_id", employeeId) .eq("role_id", roleId); if (error) return { success: false, error: error.message }; return { success: true }; } // ── Employee Management ────────────────────────────────────── export async function removeEmployee(employeeId: string): Promise<{ success: boolean; error?: string }> { const { orgId, userId } = await getOrgAndUserId(); const supabase = await createAdminClient(); // Prevent removing yourself const { data: emp } = await supabase .from("employees") .select("user_id") .eq("id", employeeId) .eq("organisation_id", orgId) .single(); if (!emp) return { success: false, error: "Employee not found" }; if (emp.user_id === userId) return { success: false, error: "Cannot remove yourself" }; // Delete employee_roles first (FK constraint) await supabase.from("employee_roles").delete().eq("employee_id", employeeId); const { error } = await supabase .from("employees") .delete() .eq("id", employeeId) .eq("organisation_id", orgId); if (error) return { success: false, error: error.message }; return { success: true }; } // ── Resignation ────────────────────────────────────────────── export async function submitResignation( reason: string, lastDay: string ): Promise<{ success: boolean; error?: string }> { const { userId } = await getOrgAndUserId(); const supabase = await createAdminClient(); const { orgId } = await getOrgAndUserId(); const { data: emp } = await supabase .from("employees") .select("id") .eq("organisation_id", orgId) .eq("user_id", userId) .single(); if (!emp) return { success: false, error: "No employee record found" }; const { error } = await supabase .from("employees") .update({ resignation_status: "submitted", resignation_date: lastDay, resignation_reason: reason, }) .eq("id", emp.id); if (error) return { success: false, error: error.message }; return { success: true }; } export async function reviewResignation( employeeId: string, action: "accept" | "reject" ): Promise<{ success: boolean; error?: string }> { const { userId } = await getOrgAndUserId(); const supabase = await createAdminClient(); const { error } = await supabase .from("employees") .update({ resignation_status: action === "accept" ? "accepted" : "rejected", resignation_reviewed_by: userId, }) .eq("id", employeeId); if (error) return { success: false, error: error.message }; return { success: true }; } /** * Get employees whose resignation is visible to the current user. * Visible if: user is owner/admin, or user is higher in chain of command (manager), * or user has can_review_resignation permission. */ export async function fetchResignations(): Promise { const { orgId, userId } = await getOrgAndUserId(); const supabase = await createAdminClient(); const { data: member } = await supabase .from("organisation_members") .select("role") .eq("user_id", userId) .eq("organisation_id", orgId) .single(); const orgRole = member?.role || "employee"; if (hasBypassRole(orgRole)) { const { data } = await supabase .from("employees") .select("id, first_name, last_name, job_title, resignation_status, resignation_date, resignation_reason, resignation_reviewed_by, department:departments(name), employee_roles(role:roles(name, hierarchy_level))") .eq("organisation_id", orgId) .in("resignation_status", ["submitted", "accepted"]) .order("resignation_date", { ascending: true }); return data || []; } // Get user's employee record and hierarchy level const { data: userEmp } = await supabase .from("employees") .select("id, manager_id") .eq("organisation_id", orgId) .eq("user_id", userId) .single(); if (!userEmp) return []; // Show resignations of employees who report to this user (direct reports) // or employees with lower hierarchy level const { data } = await supabase .from("employees") .select("id, first_name, last_name, job_title, resignation_status, resignation_date, resignation_reason, manager_id, department:departments(name)") .eq("organisation_id", orgId) .in("resignation_status", ["submitted", "accepted"]) .or(`manager_id.eq.${userEmp.id}`) .order("resignation_date", { ascending: true }); return data || []; } /** * Get org stats for the portal. */ export async function getOrgStats(): Promise<{ totalEmployees: number; activeEmployees: number; departments: number; roles: number; ownerCount: number; adminCount: number; }> { const { orgId } = await getOrgAndUserId(); const supabase = await createAdminClient(); const { count: totalEmployees } = await supabase .from("employees") .select("*", { count: "exact", head: true }) .eq("organisation_id", orgId); const { count: activeEmployees } = await supabase .from("employees") .select("*", { count: "exact", head: true }) .eq("organisation_id", orgId) .eq("status", "active"); const { count: departments } = await supabase .from("departments") .select("*", { count: "exact", head: true }) .eq("organisation_id", orgId); const { count: roles } = await supabase .from("roles") .select("*", { count: "exact", head: true }) .eq("organisation_id", orgId); const { data: members } = await supabase .from("organisation_members") .select("role") .eq("organisation_id", orgId); const ownerCount = (members || []).filter((m) => m.role === "owner").length; const adminCount = (members || []).filter((m) => m.role === "admin").length; return { totalEmployees: totalEmployees || 0, activeEmployees: activeEmployees || 0, departments: departments || 0, roles: roles || 0, ownerCount, adminCount, }; } /** * Get employees with roles and org member info for directory. */ export async function fetchEmployeesWithRoles(): Promise { const { orgId } = await getOrgAndUserId(); const supabase = await createAdminClient(); const { data: employees } = await supabase .from("employees") .select("*, department:departments(name), employee_roles(role:roles(id, name, hierarchy_level))") .eq("organisation_id", orgId) .order("first_name", { ascending: true }); // Get org roles for each employee const { data: members } = await supabase .from("organisation_members") .select("user_id, role"); const memberMap = new Map(); (members || []).forEach((m) => memberMap.set(m.user_id, m.role)); return (employees || []).map((emp) => ({ ...emp, department_name: emp.department?.name || null, org_role: emp.user_id ? memberMap.get(emp.user_id) : null, })); } /** * Get the current user's profile info (name, email) from Supabase profiles table. */ export async function getCurrentUserInfo(): Promise<{ fullName: string; email: string; userId: string; } | null> { const { userId } = await auth(); if (!userId) return null; const supabase = await createAdminClient(); const { data } = await supabase .from("profiles") .select("email, full_name") .eq("id", userId) .single(); if (!data) return null; return { fullName: data.full_name || data.email?.split("@")[0] || "User", email: data.email || "", userId, }; } /** * Seed default custom roles for a newly created organisation. * Creates Finance, HR, and Operations roles with appropriate permissions. */ export async function seedDefaultRoles(organisationId: string): Promise { const supabase = await createAdminClient(); // Check if any custom roles already exist const { data: existing } = await supabase .from("roles") .select("id") .eq("organisation_id", organisationId) .limit(1); if (existing && existing.length > 0) return; // Already seeded for (const roleDef of DEFAULT_CUSTOM_ROLES) { const { data: role, error: roleError } = await supabase .from("roles") .insert({ organisation_id: organisationId, name: roleDef.name, hierarchy_level: roleDef.hierarchyLevel, color: roleDef.color, can_manage_lower: roleDef.canManageLower, }) .select("id") .single(); if (roleError || !role) { console.error(`Failed to seed role "${roleDef.name}":`, roleError); continue; } if (roleDef.permissions.length > 0) { const { error: permError } = await supabase .from("role_permissions") .insert(roleDef.permissions.map((p) => ({ role_id: role.id, permission: p }))); if (permError) { console.error(`Failed to seed permissions for "${roleDef.name}":`, permError); } } } }