From 0136d3d8a43fe14549b80362cf45ba177f08d636 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Jan 2026 21:14:44 +0000 Subject: [PATCH] Build complete Staff Onboarding Portal - Add StaffOnboarding.tsx main hub with welcome banner, progress ring, and quick action cards - Add StaffOnboardingChecklist.tsx with interactive Day 1/Week 1/Month 1 checklist that saves progress to database - Add database migration for staff_onboarding_progress and staff_onboarding_metadata tables with RLS policies - Add API endpoint /api/staff/onboarding for fetching and updating onboarding progress with admin view for managers - Add routes to App.tsx for /staff/onboarding and /staff/onboarding/checklist --- api/staff/onboarding.ts | 289 ++++++++++ client/App.tsx | 20 + client/pages/staff/StaffOnboarding.tsx | 515 ++++++++++++++++++ .../pages/staff/StaffOnboardingChecklist.tsx | 454 +++++++++++++++ .../20260126_add_staff_onboarding.sql | 97 ++++ 5 files changed, 1375 insertions(+) create mode 100644 api/staff/onboarding.ts create mode 100644 client/pages/staff/StaffOnboarding.tsx create mode 100644 client/pages/staff/StaffOnboardingChecklist.tsx create mode 100644 supabase/migrations/20260126_add_staff_onboarding.sql diff --git a/api/staff/onboarding.ts b/api/staff/onboarding.ts new file mode 100644 index 00000000..8aac6a8f --- /dev/null +++ b/api/staff/onboarding.ts @@ -0,0 +1,289 @@ +import { supabase } from "../_supabase.js"; + +interface ChecklistItem { + id: string; + checklist_item: string; + phase: string; + completed: boolean; + completed_at: string | null; + notes: string | null; +} + +interface OnboardingMetadata { + start_date: string; + manager_id: string | null; + department: string | null; + role_title: string | null; + onboarding_completed: boolean; +} + +// Default checklist items for new staff +const DEFAULT_CHECKLIST_ITEMS = [ + // Day 1 + { item: "Complete HR paperwork", phase: "day1" }, + { item: "Set up workstation", phase: "day1" }, + { item: "Join Discord server", phase: "day1" }, + { item: "Meet your manager", phase: "day1" }, + { item: "Review company handbook", phase: "day1" }, + { item: "Set up email and accounts", phase: "day1" }, + // Week 1 + { item: "Complete security training", phase: "week1" }, + { item: "Set up development environment", phase: "week1" }, + { item: "Review codebase architecture", phase: "week1" }, + { item: "Attend team standup", phase: "week1" }, + { item: "Complete first small task", phase: "week1" }, + { item: "Meet team members", phase: "week1" }, + // Month 1 + { item: "Complete onboarding course", phase: "month1" }, + { item: "Contribute to first sprint", phase: "month1" }, + { item: "30-day check-in with manager", phase: "month1" }, + { item: "Set Q1 OKRs", phase: "month1" }, + { item: "Shadow a senior team member", phase: "month1" }, +]; + +export default async (req: Request) => { + const token = req.headers.get("Authorization")?.replace("Bearer ", ""); + if (!token) { + return new Response(JSON.stringify({ error: "Unauthorized" }), { + status: 401, + headers: { "Content-Type": "application/json" }, + }); + } + + const { data: userData } = await supabase.auth.getUser(token); + if (!userData.user) { + return new Response(JSON.stringify({ error: "Unauthorized" }), { + status: 401, + headers: { "Content-Type": "application/json" }, + }); + } + + const userId = userData.user.id; + const url = new URL(req.url); + + try { + // GET - Fetch onboarding progress + if (req.method === "GET") { + // Check for admin view (managers viewing team progress) + if (url.pathname.endsWith("/admin")) { + // Get team members for this manager + const { data: teamMembers, error: teamError } = await supabase + .from("staff_members") + .select("user_id, full_name, email, avatar_url, start_date") + .eq("manager_id", userId); + + if (teamError) { + return new Response(JSON.stringify({ error: teamError.message }), { + status: 500, + headers: { "Content-Type": "application/json" }, + }); + } + + if (!teamMembers || teamMembers.length === 0) { + return new Response(JSON.stringify({ team: [] }), { + headers: { "Content-Type": "application/json" }, + }); + } + + // Get progress for all team members + const userIds = teamMembers.map((m) => m.user_id); + const { data: progressData } = await supabase + .from("staff_onboarding_progress") + .select("*") + .in("user_id", userIds); + + // Calculate completion for each team member + const teamProgress = teamMembers.map((member) => { + const memberProgress = progressData?.filter( + (p) => p.user_id === member.user_id, + ); + const completed = + memberProgress?.filter((p) => p.completed).length || 0; + const total = DEFAULT_CHECKLIST_ITEMS.length; + return { + ...member, + progress_completed: completed, + progress_total: total, + progress_percentage: Math.round((completed / total) * 100), + }; + }); + + return new Response(JSON.stringify({ team: teamProgress }), { + headers: { "Content-Type": "application/json" }, + }); + } + + // Regular user view - get own progress + const { data: progress, error: progressError } = await supabase + .from("staff_onboarding_progress") + .select("*") + .eq("user_id", userId) + .order("created_at", { ascending: true }); + + // Get or create metadata + let { data: metadata, error: metadataError } = await supabase + .from("staff_onboarding_metadata") + .select("*") + .eq("user_id", userId) + .single(); + + // If no metadata exists, create it + if (!metadata && metadataError?.code === "PGRST116") { + const { data: newMetadata } = await supabase + .from("staff_onboarding_metadata") + .insert({ user_id: userId }) + .select() + .single(); + metadata = newMetadata; + } + + // Get staff member info for name/department + const { data: staffMember } = await supabase + .from("staff_members") + .select("full_name, department, role, avatar_url") + .eq("user_id", userId) + .single(); + + // Get manager info if exists + let managerInfo = null; + if (metadata?.manager_id) { + const { data: manager } = await supabase + .from("staff_members") + .select("full_name, email, avatar_url") + .eq("user_id", metadata.manager_id) + .single(); + managerInfo = manager; + } + + // If no progress exists, initialize with default items + let progressItems = progress || []; + if (!progress || progress.length === 0) { + const itemsToInsert = DEFAULT_CHECKLIST_ITEMS.map((item) => ({ + user_id: userId, + checklist_item: item.item, + phase: item.phase, + completed: false, + })); + + const { data: insertedItems } = await supabase + .from("staff_onboarding_progress") + .insert(itemsToInsert) + .select(); + + progressItems = insertedItems || []; + } + + // Group by phase + const groupedProgress = { + day1: progressItems.filter((p) => p.phase === "day1"), + week1: progressItems.filter((p) => p.phase === "week1"), + month1: progressItems.filter((p) => p.phase === "month1"), + }; + + // Calculate overall progress + const completed = progressItems.filter((p) => p.completed).length; + const total = progressItems.length; + + return new Response( + JSON.stringify({ + progress: groupedProgress, + metadata: metadata || { start_date: new Date().toISOString() }, + staff_member: staffMember, + manager: managerInfo, + summary: { + completed, + total, + percentage: total > 0 ? Math.round((completed / total) * 100) : 0, + }, + }), + { + headers: { "Content-Type": "application/json" }, + }, + ); + } + + // POST - Mark item complete/incomplete + if (req.method === "POST") { + const body = await req.json(); + const { checklist_item, completed, notes } = body; + + if (!checklist_item) { + return new Response( + JSON.stringify({ error: "checklist_item is required" }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + // Upsert the progress item + const { data, error } = await supabase + .from("staff_onboarding_progress") + .upsert( + { + user_id: userId, + checklist_item, + phase: + DEFAULT_CHECKLIST_ITEMS.find((i) => i.item === checklist_item) + ?.phase || "day1", + completed: completed ?? true, + completed_at: completed ? new Date().toISOString() : null, + notes: notes || null, + }, + { + onConflict: "user_id,checklist_item", + }, + ) + .select() + .single(); + + if (error) { + return new Response(JSON.stringify({ error: error.message }), { + status: 500, + headers: { "Content-Type": "application/json" }, + }); + } + + // Check if all items are complete + const { data: allProgress } = await supabase + .from("staff_onboarding_progress") + .select("completed") + .eq("user_id", userId); + + const allCompleted = allProgress?.every((p) => p.completed); + + // Update metadata if all completed + if (allCompleted) { + await supabase + .from("staff_onboarding_metadata") + .update({ + onboarding_completed: true, + onboarding_completed_at: new Date().toISOString(), + }) + .eq("user_id", userId); + } + + return new Response( + JSON.stringify({ + item: data, + all_completed: allCompleted, + }), + { + headers: { "Content-Type": "application/json" }, + }, + ); + } + + return new Response(JSON.stringify({ error: "Method not allowed" }), { + status: 405, + headers: { "Content-Type": "application/json" }, + }); + } catch (err: any) { + console.error("Onboarding API error:", err); + return new Response(JSON.stringify({ error: err.message }), { + status: 500, + headers: { "Content-Type": "application/json" }, + }); + } +}; diff --git a/client/App.tsx b/client/App.tsx index b937d151..4ec9b1f9 100644 --- a/client/App.tsx +++ b/client/App.tsx @@ -159,6 +159,8 @@ import StaffLearningPortal from "./pages/staff/StaffLearningPortal"; import StaffPerformanceReviews from "./pages/staff/StaffPerformanceReviews"; import StaffProjectTracking from "./pages/staff/StaffProjectTracking"; import StaffTeamHandbook from "./pages/staff/StaffTeamHandbook"; +import StaffOnboarding from "./pages/staff/StaffOnboarding"; +import StaffOnboardingChecklist from "./pages/staff/StaffOnboardingChecklist"; const queryClient = new QueryClient(); @@ -412,6 +414,24 @@ const App = () => ( } /> + {/* Staff Onboarding Routes */} + + + + } + /> + + + + } + /> + {/* Staff Management Routes */} (null); + + useEffect(() => { + if (session?.access_token) { + fetchOnboardingData(); + } + }, [session?.access_token]); + + const fetchOnboardingData = async () => { + try { + const response = await fetch("/api/staff/onboarding", { + headers: { + Authorization: `Bearer ${session?.access_token}`, + }, + }); + if (!response.ok) throw new Error("Failed to fetch onboarding data"); + const result = await response.json(); + setData(result); + } catch (error) { + console.error("Error fetching onboarding:", error); + aethexToast.error("Failed to load onboarding data"); + } finally { + setLoading(false); + } + }; + + const getCurrentPhase = () => { + if (!data) return "day1"; + const { day1, week1 } = data.progress; + const day1Complete = day1.every((item) => item.completed); + const week1Complete = week1.every((item) => item.completed); + if (!day1Complete) return "day1"; + if (!week1Complete) return "week1"; + return "month1"; + }; + + const getPhaseLabel = (phase: string) => { + switch (phase) { + case "day1": + return "Day 1"; + case "week1": + return "Week 1"; + case "month1": + return "Month 1"; + default: + return phase; + } + }; + + const getDaysSinceStart = () => { + if (!data?.metadata?.start_date) return 0; + const start = new Date(data.metadata.start_date); + const now = new Date(); + const diff = Math.floor( + (now.getTime() - start.getTime()) / (1000 * 60 * 60 * 24), + ); + return diff; + }; + + const getInitials = (name: string) => { + return name + .split(" ") + .map((n) => n[0]) + .join("") + .toUpperCase(); + }; + + if (loading) { + return ( + + +
+ +
+
+ ); + } + + const currentPhase = getCurrentPhase(); + const daysSinceStart = getDaysSinceStart(); + + return ( + + + +
+ {/* Background effects */} +
+
+
+
+ +
+
+ {/* Welcome Header */} +
+
+
+ +
+ + {currentPhase === "day1" + ? "Getting Started" + : currentPhase === "week1" + ? "Week 1" + : "Month 1"} + +
+ +

+ Welcome to AeThex + {data?.staff_member?.full_name + ? `, ${data.staff_member.full_name.split(" ")[0]}!` + : "!"} +

+

+ {data?.summary?.percentage === 100 + ? "Congratulations! You've completed your onboarding journey." + : `Day ${daysSinceStart + 1} of your onboarding journey. Let's make it great!`} +

+
+ + {/* Progress Overview */} + + +
+ {/* Progress Ring */} +
+
+ + + + + + {data?.summary?.percentage || 0}% + +
+
+

+ Onboarding Progress +

+

+ {data?.summary?.completed || 0} of{" "} + {data?.summary?.total || 0} tasks completed +

+
+
+ + {/* Phase Progress */} +
+ {["day1", "week1", "month1"].map((phase) => { + const items = data?.progress?.[phase as keyof typeof data.progress] || []; + const completed = items.filter((i) => i.completed).length; + const total = items.length; + const isComplete = completed === total && total > 0; + const isCurrent = phase === currentPhase; + + return ( +
+ {isComplete ? ( + + ) : ( + + )} +

+ {getPhaseLabel(phase)} +

+

+ {completed}/{total} +

+
+ ); + })} +
+
+
+
+ + {/* Quick Actions Grid */} +
+ + + +
+ +
+

+ Complete Checklist +

+

+ Track your onboarding tasks +

+
+
+ + + + + +
+ +
+

+ Meet Your Team +

+

+ Browse the staff directory +

+
+
+ + + + + +
+ +
+

+ Learning Portal +

+

+ Training courses & resources +

+
+
+ + + + + +
+ +
+

+ Team Handbook +

+

+ Policies & guidelines +

+
+
+ +
+ + {/* Main Content Grid */} +
+ {/* Current Phase Tasks */} +
+ + + + + Current Tasks - {getPhaseLabel(currentPhase)} + + + Focus on completing these tasks first + + + +
+ {data?.progress?.[currentPhase as keyof typeof data.progress] + ?.slice(0, 5) + .map((item) => ( +
+ {item.completed ? ( + + ) : ( +
+ )} + + {item.checklist_item} + +
+ ))} +
+ + + + + +
+ + {/* Sidebar */} +
+ {/* Manager Card */} + {data?.manager && ( + + + + + Your Manager + + + +
+ + + + {getInitials(data.manager.full_name)} + + +
+

+ {data.manager.full_name} +

+

+ {data.manager.email} +

+
+
+ +
+
+ )} + + {/* Important Links */} + + + + Quick Links + + + + + + Join Discord Server + + + + Knowledge Base + + + + Documentation + + + + Announcements + + + + + {/* Achievement */} + {data?.summary?.percentage === 100 && ( + + +
+ +
+

+ Onboarding Complete! +

+

+ You've completed all onboarding tasks. Welcome to the + team! +

+
+
+ )} +
+
+
+
+
+ + ); +} diff --git a/client/pages/staff/StaffOnboardingChecklist.tsx b/client/pages/staff/StaffOnboardingChecklist.tsx new file mode 100644 index 00000000..d37ad355 --- /dev/null +++ b/client/pages/staff/StaffOnboardingChecklist.tsx @@ -0,0 +1,454 @@ +import { useState, useEffect } from "react"; +import { Link } from "wouter"; +import Layout from "@/components/Layout"; +import SEO from "@/components/SEO"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Progress } from "@/components/ui/progress"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + ClipboardCheck, + CheckCircle2, + Circle, + ArrowLeft, + Loader2, + Calendar, + Clock, + Trophy, + Sun, + Briefcase, + Target, +} from "lucide-react"; +import { useAuth } from "@/hooks/useAuth"; +import { aethexToast } from "@/components/ui/aethex-toast"; + +interface ChecklistItem { + id: string; + checklist_item: string; + phase: string; + completed: boolean; + completed_at: string | null; + notes: string | null; +} + +interface OnboardingData { + progress: { + day1: ChecklistItem[]; + week1: ChecklistItem[]; + month1: ChecklistItem[]; + }; + metadata: { + start_date: string; + onboarding_completed: boolean; + }; + summary: { + completed: number; + total: number; + percentage: number; + }; +} + +const PHASE_INFO = { + day1: { + label: "Day 1", + icon: Sun, + description: "First day essentials - get set up and meet the team", + color: "emerald", + }, + week1: { + label: "Week 1", + icon: Briefcase, + description: "Dive into tools, processes, and your first tasks", + color: "blue", + }, + month1: { + label: "Month 1", + icon: Target, + description: "Build momentum and complete your onboarding journey", + color: "purple", + }, +}; + +export default function StaffOnboardingChecklist() { + const { session } = useAuth(); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(null); + const [data, setData] = useState(null); + const [activeTab, setActiveTab] = useState("day1"); + + useEffect(() => { + if (session?.access_token) { + fetchOnboardingData(); + } + }, [session?.access_token]); + + const fetchOnboardingData = async () => { + try { + const response = await fetch("/api/staff/onboarding", { + headers: { + Authorization: `Bearer ${session?.access_token}`, + }, + }); + if (!response.ok) throw new Error("Failed to fetch onboarding data"); + const result = await response.json(); + setData(result); + + // Set active tab to current phase + const day1Complete = result.progress.day1.every( + (i: ChecklistItem) => i.completed, + ); + const week1Complete = result.progress.week1.every( + (i: ChecklistItem) => i.completed, + ); + if (!day1Complete) setActiveTab("day1"); + else if (!week1Complete) setActiveTab("week1"); + else setActiveTab("month1"); + } catch (error) { + console.error("Error fetching onboarding:", error); + aethexToast.error("Failed to load onboarding data"); + } finally { + setLoading(false); + } + }; + + const toggleItem = async (item: ChecklistItem) => { + if (!session?.access_token) return; + setSaving(item.id); + + try { + const response = await fetch("/api/staff/onboarding", { + method: "POST", + headers: { + Authorization: `Bearer ${session.access_token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + checklist_item: item.checklist_item, + completed: !item.completed, + }), + }); + + if (!response.ok) throw new Error("Failed to update item"); + + const result = await response.json(); + + // Update local state + if (data) { + const phase = item.phase as keyof typeof data.progress; + const updatedItems = data.progress[phase].map((i) => + i.id === item.id + ? { ...i, completed: !item.completed, completed_at: !item.completed ? new Date().toISOString() : null } + : i, + ); + + const newCompleted = Object.values({ + ...data.progress, + [phase]: updatedItems, + }).flat().filter((i) => i.completed).length; + + setData({ + ...data, + progress: { + ...data.progress, + [phase]: updatedItems, + }, + summary: { + ...data.summary, + completed: newCompleted, + percentage: Math.round((newCompleted / data.summary.total) * 100), + }, + }); + } + + if (result.all_completed) { + aethexToast.success( + "Congratulations! You've completed all onboarding tasks!", + ); + } else if (!item.completed) { + aethexToast.success("Task completed!"); + } + } catch (error) { + console.error("Error updating item:", error); + aethexToast.error("Failed to update task"); + } finally { + setSaving(null); + } + }; + + const getPhaseProgress = (phase: keyof typeof data.progress) => { + if (!data) return { completed: 0, total: 0, percentage: 0 }; + const items = data.progress[phase]; + const completed = items.filter((i) => i.completed).length; + return { + completed, + total: items.length, + percentage: items.length > 0 ? Math.round((completed / items.length) * 100) : 0, + }; + }; + + const formatDate = (dateString: string | null) => { + if (!dateString) return null; + return new Date(dateString).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); + }; + + if (loading) { + return ( + + +
+ +
+
+ ); + } + + return ( + + + +
+ {/* Background effects */} +
+
+
+
+ +
+
+ {/* Header */} +
+ + + + +
+
+ +
+
+

+ Onboarding Checklist +

+

+ Track and complete your onboarding tasks +

+
+
+ + {/* Overall Progress */} + + +
+ + Overall Progress + + + {data?.summary?.completed || 0}/{data?.summary?.total || 0}{" "} + tasks ({data?.summary?.percentage || 0}%) + +
+ + {data?.summary?.percentage === 100 && ( +
+ + + All tasks completed! Welcome to the team! + +
+ )} +
+
+
+ + {/* Tabs */} + + + {(["day1", "week1", "month1"] as const).map((phase) => { + const info = PHASE_INFO[phase]; + const progress = getPhaseProgress(phase); + const Icon = info.icon; + + return ( + + + {info.label} + {progress.percentage === 100 && ( + + )} + + ); + })} + + + {(["day1", "week1", "month1"] as const).map((phase) => { + const info = PHASE_INFO[phase]; + const progress = getPhaseProgress(phase); + const items = data?.progress[phase] || []; + + return ( + + + +
+
+ + {info.label} + {progress.percentage === 100 && ( + + Complete + + )} + + + {info.description} + +
+
+

+ {progress.percentage}% +

+

+ {progress.completed}/{progress.total} done +

+
+
+ +
+ +
+ {items.map((item) => ( +
+
+ {saving === item.id ? ( + + ) : ( + toggleItem(item)} + className={`h-5 w-5 ${ + item.completed + ? "border-green-500 bg-green-500 data-[state=checked]:bg-green-500" + : "border-slate-500" + }`} + /> + )} +
+
+

+ {item.checklist_item} +

+ {item.completed && item.completed_at && ( +
+ + Completed {formatDate(item.completed_at)} +
+ )} +
+ {item.completed && ( + + )} +
+ ))} +
+ + {progress.percentage === 100 && ( +
+ +

+ {info.label} Complete! +

+

+ Great job completing all {info.label.toLowerCase()}{" "} + tasks +

+
+ )} +
+
+
+ ); + })} +
+ + {/* Help Section */} + + +
+
+ +
+
+

+ Need Help? +

+

+ If you're stuck on any task or need clarification, don't + hesitate to reach out to your manager or team members. You + can also check the{" "} + + Knowledge Base + {" "} + for detailed guides. +

+
+
+
+
+
+
+
+ + ); +} diff --git a/supabase/migrations/20260126_add_staff_onboarding.sql b/supabase/migrations/20260126_add_staff_onboarding.sql new file mode 100644 index 00000000..565fb4d0 --- /dev/null +++ b/supabase/migrations/20260126_add_staff_onboarding.sql @@ -0,0 +1,97 @@ +-- Staff Onboarding Progress Table +-- Tracks individual checklist item completion for new staff members + +CREATE TABLE IF NOT EXISTS staff_onboarding_progress ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, + checklist_item TEXT NOT NULL, + phase TEXT NOT NULL CHECK (phase IN ('day1', 'week1', 'month1')), + completed BOOLEAN DEFAULT FALSE, + completed_at TIMESTAMPTZ, + notes TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(user_id, checklist_item) +); + +-- Create index for faster lookups +CREATE INDEX idx_staff_onboarding_user_id ON staff_onboarding_progress(user_id); +CREATE INDEX idx_staff_onboarding_phase ON staff_onboarding_progress(phase); + +-- Enable RLS +ALTER TABLE staff_onboarding_progress ENABLE ROW LEVEL SECURITY; + +-- Staff can view and update their own onboarding progress +CREATE POLICY "Staff can view own onboarding progress" + ON staff_onboarding_progress + FOR SELECT + USING (auth.uid() = user_id); + +CREATE POLICY "Staff can update own onboarding progress" + ON staff_onboarding_progress + FOR UPDATE + USING (auth.uid() = user_id); + +CREATE POLICY "Staff can insert own onboarding progress" + ON staff_onboarding_progress + FOR INSERT + WITH CHECK (auth.uid() = user_id); + +-- Managers can view team onboarding progress (staff members table has manager info) +CREATE POLICY "Managers can view team onboarding progress" + ON staff_onboarding_progress + FOR SELECT + USING ( + EXISTS ( + SELECT 1 FROM staff_members sm + WHERE sm.user_id = staff_onboarding_progress.user_id + AND sm.manager_id = auth.uid() + ) + ); + +-- Staff onboarding metadata for start date and manager assignment +CREATE TABLE IF NOT EXISTS staff_onboarding_metadata ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE UNIQUE, + start_date DATE NOT NULL DEFAULT CURRENT_DATE, + manager_id UUID REFERENCES auth.users(id), + department TEXT, + role_title TEXT, + onboarding_completed BOOLEAN DEFAULT FALSE, + onboarding_completed_at TIMESTAMPTZ, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Enable RLS for metadata +ALTER TABLE staff_onboarding_metadata ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "Staff can view own metadata" + ON staff_onboarding_metadata + FOR SELECT + USING (auth.uid() = user_id); + +CREATE POLICY "Managers can view team metadata" + ON staff_onboarding_metadata + FOR SELECT + USING (auth.uid() = manager_id); + +-- Function to update updated_at timestamp +CREATE OR REPLACE FUNCTION update_staff_onboarding_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Triggers for updated_at +CREATE TRIGGER staff_onboarding_progress_updated_at + BEFORE UPDATE ON staff_onboarding_progress + FOR EACH ROW + EXECUTE FUNCTION update_staff_onboarding_updated_at(); + +CREATE TRIGGER staff_onboarding_metadata_updated_at + BEFORE UPDATE ON staff_onboarding_metadata + FOR EACH ROW + EXECUTE FUNCTION update_staff_onboarding_updated_at();