"use server"; import { createAdminClient } from "@/lib/supabase/server"; import { auth } from "@clerk/nextjs/server"; import { randomBytes } from "crypto"; async function getOrgAndUserId(): Promise<{ orgId: string; userId: string; orgRole: 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, orgRole: member.role }; } // ── Invite Links ───────────────────────────────────────────── export async function createInviteLink( expiryHours: number | null, defaultRoleId: string | null ): Promise<{ success: boolean; token?: string; error?: string }> { const { orgId, userId } = await getOrgAndUserId(); const supabase = await createAdminClient(); const token = `inv_${randomBytes(16).toString("hex")}`; const expiresAt = expiryHours && expiryHours > 0 ? new Date(Date.now() + expiryHours * 60 * 60 * 1000).toISOString() : null; const { error } = await supabase.from("invite_links").insert({ organisation_id: orgId, token, default_role_id: defaultRoleId, expiry_hours: expiryHours, expires_at: expiresAt, created_by: userId, }); if (error) return { success: false, error: error.message }; return { success: true, token }; } export async function fetchInviteLinks(): Promise { const { orgId } = await getOrgAndUserId(); const supabase = await createAdminClient(); const { data } = await supabase .from("invite_links") .select("*, default_role:roles(name, color)") .eq("organisation_id", orgId) .order("created_at", { ascending: false }); return data || []; } export async function revokeInviteLink(linkId: string): Promise<{ success: boolean; error?: string }> { const { orgId } = await getOrgAndUserId(); const supabase = await createAdminClient(); const { error } = await supabase .from("invite_links") .update({ revoked: true }) .eq("id", linkId) .eq("organisation_id", orgId); if (error) return { success: false, error: error.message }; return { success: true }; } export async function useInviteLink(token: string): Promise<{ success: boolean; error?: string; roleId?: string | null }> { const { userId } = await auth(); if (!userId) return { success: false, error: "Not authenticated" }; const supabase = await createAdminClient(); const { data: link, error } = await supabase .from("invite_links") .select("*") .eq("token", token) .single(); if (error || !link) return { success: false, error: "Invalid invite link" }; if (link.revoked) return { success: false, error: "This invite link has been revoked" }; if (link.expires_at && new Date(link.expires_at) < new Date()) return { success: false, error: "This invite link has expired" }; if (link.max_uses && link.used_count >= link.max_uses) return { success: false, error: "This invite link has reached its usage limit" }; const { data: existing } = await supabase .from("organisation_members") .select("id") .eq("user_id", userId) .eq("organisation_id", link.organisation_id) .single(); if (existing) return { success: false, error: "You are already a member of this organisation" }; await supabase.from("organisation_members").insert({ organisation_id: link.organisation_id, user_id: userId, role: "guest", }); if (link.default_role_id) { const { data: emp } = await supabase .from("employees") .select("id") .eq("organisation_id", link.organisation_id) .eq("user_id", userId) .single(); if (emp) { await supabase.from("employee_roles").insert({ employee_id: emp.id, role_id: link.default_role_id, }); } } await supabase .from("invite_links") .update({ used_count: link.used_count + 1 }) .eq("id", link.id); return { success: true, roleId: link.default_role_id }; } // ── Notifications ──────────────────────────────────────────── export async function createNotification( userId: string | null, type: string, title: string, body?: string, metadata?: any ): Promise { const { orgId } = await getOrgAndUserId(); const supabase = await createAdminClient(); await supabase.from("notification_events").insert({ organisation_id: orgId, user_id: userId, type, title, body, metadata, }); } export async function createNotificationBroadcast( type: string, title: string, body?: string, metadata?: any ): Promise { const { orgId } = await getOrgAndUserId(); const supabase = await createAdminClient(); await supabase.from("notification_events").insert({ organisation_id: orgId, user_id: null, type, title, body, metadata, }); } export async function fetchNotifications(limit = 30): Promise { 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) return []; const { data } = await supabase .from("notification_events") .select("*") .eq("organisation_id", member.organisation_id) .or(`user_id.eq.${userId},user_id.is.null`) .order("created_at", { ascending: false }) .limit(limit); return data || []; } export async function markNotificationRead(notificationId: string): Promise { const supabase = await createAdminClient(); await supabase .from("notification_events") .update({ read_at: new Date().toISOString() }) .eq("id", notificationId); } export async function markAllNotificationsRead(): Promise { const { userId } = await auth(); if (!userId) return; const supabase = await createAdminClient(); await supabase .from("notification_events") .update({ read_at: new Date().toISOString() }) .eq("user_id", userId) .is("read_at", null); } export async function getUnreadNotificationCount(): Promise { const { userId } = await auth(); if (!userId) return 0; const supabase = await createAdminClient(); const { count } = await supabase .from("notification_events") .select("*", { count: "exact", head: true }) .is("read_at", null) .or(`user_id.eq.${userId},user_id.is.null`); return count || 0; } // ── Communication Groups ───────────────────────────────────── export async function createCommGroup( name: string, description: string, memberIds: string[] ): Promise<{ success: boolean; groupId?: string; error?: string }> { const { orgId, userId } = await getOrgAndUserId(); const supabase = await createAdminClient(); const { data, error } = await supabase .from("communication_groups") .insert({ organisation_id: orgId, name, description, created_by: userId }) .select("id") .single(); if (error) return { success: false, error: error.message }; if (memberIds.length > 0) { await supabase.from("communication_group_members").insert( memberIds.map((uid) => ({ group_id: data.id, user_id: uid })) ); } return { success: true, groupId: data.id }; } export async function fetchCommGroups(): Promise { const { orgId } = await getOrgAndUserId(); const supabase = await createAdminClient(); const { data } = await supabase .from("communication_groups") .select("*, members:communication_group_members(user_id), creator:profiles!communication_groups_created_by_fkey(full_name)") .eq("organisation_id", orgId) .order("created_at", { ascending: false }); return data || []; } export async function fetchGroupMessages(groupId: string): Promise { const { orgId } = await getOrgAndUserId(); const supabase = await createAdminClient(); const { userId } = await auth(); const { data: membership } = await supabase .from("communication_group_members") .select("id") .eq("group_id", groupId) .eq("user_id", userId) .single(); if (!membership) throw new Error("Not a member of this group"); const { data } = await supabase .from("group_messages") .select("*, sender:profiles(full_name)") .eq("group_id", groupId) .order("created_at", { ascending: true }); return data || []; } export async function sendGroupMessage( groupId: string, body: string ): Promise<{ success: boolean; error?: string }> { const { userId } = await auth(); if (!userId) return { success: false, error: "Unauthenticated" }; const supabase = await createAdminClient(); await supabase .from("group_messages") .insert({ group_id: groupId, sender_id: userId, body }); return { success: true }; } export async function joinCommGroup(groupId: string): Promise<{ success: boolean; error?: string }> { const { userId } = await auth(); if (!userId) return { success: false, error: "Unauthenticated" }; const supabase = await createAdminClient(); try { await supabase.from("communication_group_members").insert({ group_id: groupId, user_id: userId }); } catch { return { success: false, error: "Already a member or group not found" }; } return { success: true }; } // ── Pay Logs ───────────────────────────────────────────────── export async function createPayLog( employeeId: string, amountCents: number, periodStart: string, periodEnd: string, notes?: string ): Promise<{ success: boolean; error?: string }> { const { orgId, userId } = await getOrgAndUserId(); const supabase = await createAdminClient(); const { error } = await supabase.from("pay_logs").insert({ organisation_id: orgId, employee_id: employeeId, amount_cents: amountCents, period_start: periodStart, period_end: periodEnd, notes, processed_by: userId, status: "pending", }); if (error) return { success: false, error: error.message }; return { success: true }; } export async function fetchPayLogs(employeeId?: string): Promise { const { orgId } = await getOrgAndUserId(); const supabase = await createAdminClient(); let query = supabase .from("pay_logs") .select("*, employee:employees(first_name, last_name, email)") .eq("organisation_id", orgId) .order("created_at", { ascending: false }); if (employeeId) query = query.eq("employee_id", employeeId); const { data } = await query; return data || []; } export async function approvePayLog(logId: string): Promise<{ success: boolean; error?: string }> { const { userId } = await auth(); const supabase = await createAdminClient(); const { error } = await supabase .from("pay_logs") .update({ status: "paid", paid_at: new Date().toISOString(), processed_by: userId }) .eq("id", logId); if (error) return { success: false, error: error.message }; return { success: true }; } export async function rejectPayLog(logId: string): Promise<{ success: boolean; error?: string }> { const supabase = await createAdminClient(); const { error } = await supabase .from("pay_logs") .update({ status: "rejected" }) .eq("id", logId); if (error) return { success: false, error: error.message }; return { success: true }; }