/** * Documents module — Supabase Storage + metadata table. * Files are stored in the "documents" bucket, metadata in the "documents" table. */ "use server"; import { createAdminClient } from "@/lib/supabase/server"; import { auth } from "@clerk/nextjs/server"; async function getOrganisationId(): Promise { const { userId } = await auth(); if (!userId) throw new Error("Unauthenticated"); const supabase = await createAdminClient(); const { data: member, error } = await supabase .from("organisation_members") .select("organisation_id") .eq("user_id", userId) .single(); if (error || !member) throw new Error("No organisation found"); return member.organisation_id; } export interface DocumentRecord { id: string; organisation_id: string; employee_id: string | null; name: string; file_path: string; file_type: string | null; file_size_bytes: number | null; category: string; uploaded_by: string | null; created_at: string; employee_name: string | null; } export interface CreateDocumentInput { name: string; category: string; employee_id: string | null; file: File; } const BUCKET_NAME = "documents"; /** * Upload a file to Supabase Storage and create a metadata record. * File path: {orgId}/{category}/{timestamp}-{filename} */ export async function uploadDocument( input: CreateDocumentInput ): Promise<{ success: boolean; documentId?: string; error?: string }> { const { userId } = await auth(); if (!userId) return { success: false, error: "Unauthenticated" }; const orgId = await getOrganisationId(); const supabase = await createAdminClient(); // Ensure bucket exists (idempotent) const { data: buckets } = await supabase.storage.listBuckets(); if (!buckets?.find((b) => b.name === BUCKET_NAME)) { await supabase.storage.createBucket(BUCKET_NAME, { public: false, fileSizeLimit: 52428800, // 50 MB }); } // Upload file to Storage const timestamp = Date.now(); const ext = input.file.name.split(".").pop() || ""; const filePath = `${orgId}/${input.category}/${timestamp}-${input.file.name}`; const arrayBuffer = await input.file.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); const { error: uploadError } = await supabase.storage .from(BUCKET_NAME) .upload(filePath, buffer, { contentType: input.file.type || "application/octet-stream", upsert: false, }); if (uploadError) return { success: false, error: uploadError.message }; // Create metadata record const { data, error } = await supabase .from("documents") .insert({ organisation_id: orgId, employee_id: input.employee_id || null, name: input.name, file_path: filePath, file_type: input.file.type, file_size_bytes: input.file.size, category: input.category, uploaded_by: userId, }) .select("id") .single(); if (error) { // Clean up the uploaded file if metadata insert fails await supabase.storage.from(BUCKET_NAME).remove([filePath]); return { success: false, error: error.message }; } return { success: true, documentId: data.id }; } /** * Fetch all documents for the current organisation. */ export async function fetchDocuments(category?: string): Promise { const orgId = await getOrganisationId(); const supabase = await createAdminClient(); let query = supabase .from("documents") .select("*, employee:employees(first_name, last_name)") .eq("organisation_id", orgId) .order("created_at", { ascending: false }); if (category && category !== "all") { query = query.eq("category", category); } const { data, error } = await query; if (error) throw error; return (data || []).map((d: any) => ({ id: d.id, organisation_id: d.organisation_id, employee_id: d.employee_id, name: d.name, file_path: d.file_path, file_type: d.file_type, file_size_bytes: d.file_size_bytes ? Number(d.file_size_bytes) : null, category: d.category, uploaded_by: d.uploaded_by, created_at: d.created_at, employee_name: d.employee ? `${d.employee.first_name} ${d.employee.last_name}` : null, })); } /** * Delete a document (both metadata record and storage file). */ export async function deleteDocument(documentId: string): Promise<{ success: boolean; error?: string }> { const orgId = await getOrganisationId(); const supabase = await createAdminClient(); const { data: doc } = await supabase .from("documents") .select("file_path") .eq("id", documentId) .eq("organisation_id", orgId) .single(); if (!doc) return { success: false, error: "Document not found" }; // Delete from storage await supabase.storage.from(BUCKET_NAME).remove([doc.file_path]); // Delete metadata const { error } = await supabase .from("documents") .delete() .eq("id", documentId) .eq("organisation_id", orgId); if (error) return { success: false, error: error.message }; return { success: true }; } /** * Generate a signed URL for downloading a file (valid for 60 seconds). */ export async function getDocumentDownloadUrl(documentId: string): Promise<{ url: string; error?: string }> { const orgId = await getOrganisationId(); const supabase = await createAdminClient(); const { data: doc } = await supabase .from("documents") .select("file_path") .eq("id", documentId) .eq("organisation_id", orgId) .single(); if (!doc) return { url: "", error: "Document not found" }; const { data, error } = await supabase.storage .from(BUCKET_NAME) .createSignedUrl(doc.file_path, 60); if (error) return { url: "", error: error.message }; return { url: data.signedUrl }; }