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
This commit is contained in:
parent
9c3942ebbc
commit
0136d3d8a4
5 changed files with 1375 additions and 0 deletions
289
api/staff/onboarding.ts
Normal file
289
api/staff/onboarding.ts
Normal file
|
|
@ -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" },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -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 */}
|
||||
<Route
|
||||
path="/staff/onboarding"
|
||||
element={
|
||||
<RequireAccess>
|
||||
<StaffOnboarding />
|
||||
</RequireAccess>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/staff/onboarding/checklist"
|
||||
element={
|
||||
<RequireAccess>
|
||||
<StaffOnboardingChecklist />
|
||||
</RequireAccess>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Staff Management Routes */}
|
||||
<Route
|
||||
path="/staff/directory"
|
||||
|
|
|
|||
515
client/pages/staff/StaffOnboarding.tsx
Normal file
515
client/pages/staff/StaffOnboarding.tsx
Normal file
|
|
@ -0,0 +1,515 @@
|
|||
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 { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import {
|
||||
Rocket,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
Users,
|
||||
BookOpen,
|
||||
MessageSquare,
|
||||
Calendar,
|
||||
ArrowRight,
|
||||
Sparkles,
|
||||
Target,
|
||||
Coffee,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { aethexToast } from "@/components/ui/aethex-toast";
|
||||
|
||||
interface OnboardingData {
|
||||
progress: {
|
||||
day1: ChecklistItem[];
|
||||
week1: ChecklistItem[];
|
||||
month1: ChecklistItem[];
|
||||
};
|
||||
metadata: {
|
||||
start_date: string;
|
||||
manager_id: string | null;
|
||||
department: string | null;
|
||||
role_title: string | null;
|
||||
onboarding_completed: boolean;
|
||||
};
|
||||
staff_member: {
|
||||
full_name: string;
|
||||
department: string;
|
||||
role: string;
|
||||
avatar_url: string | null;
|
||||
} | null;
|
||||
manager: {
|
||||
full_name: string;
|
||||
email: string;
|
||||
avatar_url: string | null;
|
||||
} | null;
|
||||
summary: {
|
||||
completed: number;
|
||||
total: number;
|
||||
percentage: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface ChecklistItem {
|
||||
id: string;
|
||||
checklist_item: string;
|
||||
phase: string;
|
||||
completed: boolean;
|
||||
completed_at: string | null;
|
||||
}
|
||||
|
||||
export default function StaffOnboarding() {
|
||||
const { session } = useAuth();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [data, setData] = useState<OnboardingData | null>(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 (
|
||||
<Layout>
|
||||
<SEO
|
||||
title="Staff Onboarding"
|
||||
description="Welcome to AeThex - Your onboarding journey"
|
||||
/>
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 flex items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-emerald-400" />
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
const currentPhase = getCurrentPhase();
|
||||
const daysSinceStart = getDaysSinceStart();
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<SEO
|
||||
title="Staff Onboarding"
|
||||
description="Welcome to AeThex - Your onboarding journey"
|
||||
/>
|
||||
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900">
|
||||
{/* Background effects */}
|
||||
<div className="absolute inset-0 opacity-30">
|
||||
<div className="absolute top-20 left-10 w-96 h-96 bg-emerald-600 rounded-full mix-blend-multiply filter blur-3xl animate-pulse" />
|
||||
<div className="absolute bottom-20 right-10 w-96 h-96 bg-teal-600 rounded-full mix-blend-multiply filter blur-3xl animate-pulse" />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="container mx-auto max-w-6xl px-4 py-16">
|
||||
{/* Welcome Header */}
|
||||
<div className="mb-12">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-3 rounded-lg bg-emerald-500/20 border border-emerald-500/30">
|
||||
<Rocket className="h-6 w-6 text-emerald-400" />
|
||||
</div>
|
||||
<Badge className="bg-emerald-500/20 text-emerald-300 border-emerald-500/30">
|
||||
{currentPhase === "day1"
|
||||
? "Getting Started"
|
||||
: currentPhase === "week1"
|
||||
? "Week 1"
|
||||
: "Month 1"}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl font-bold text-emerald-100 mb-2">
|
||||
Welcome to AeThex
|
||||
{data?.staff_member?.full_name
|
||||
? `, ${data.staff_member.full_name.split(" ")[0]}!`
|
||||
: "!"}
|
||||
</h1>
|
||||
<p className="text-emerald-200/70 text-lg">
|
||||
{data?.summary?.percentage === 100
|
||||
? "Congratulations! You've completed your onboarding journey."
|
||||
: `Day ${daysSinceStart + 1} of your onboarding journey. Let's make it great!`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Progress Overview */}
|
||||
<Card className="bg-slate-800/50 border-emerald-500/30 mb-8">
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
|
||||
{/* Progress Ring */}
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="relative w-24 h-24">
|
||||
<svg className="w-24 h-24 transform -rotate-90">
|
||||
<circle
|
||||
className="text-slate-700"
|
||||
strokeWidth="8"
|
||||
stroke="currentColor"
|
||||
fill="transparent"
|
||||
r="40"
|
||||
cx="48"
|
||||
cy="48"
|
||||
/>
|
||||
<circle
|
||||
className="text-emerald-500"
|
||||
strokeWidth="8"
|
||||
strokeDasharray={251.2}
|
||||
strokeDashoffset={
|
||||
251.2 - (251.2 * (data?.summary?.percentage || 0)) / 100
|
||||
}
|
||||
strokeLinecap="round"
|
||||
stroke="currentColor"
|
||||
fill="transparent"
|
||||
r="40"
|
||||
cx="48"
|
||||
cy="48"
|
||||
/>
|
||||
</svg>
|
||||
<span className="absolute inset-0 flex items-center justify-center text-2xl font-bold text-emerald-100">
|
||||
{data?.summary?.percentage || 0}%
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-emerald-100 font-semibold text-lg">
|
||||
Onboarding Progress
|
||||
</p>
|
||||
<p className="text-slate-400">
|
||||
{data?.summary?.completed || 0} of{" "}
|
||||
{data?.summary?.total || 0} tasks completed
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Phase Progress */}
|
||||
<div className="flex gap-4">
|
||||
{["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 (
|
||||
<div
|
||||
key={phase}
|
||||
className={`text-center p-3 rounded-lg ${
|
||||
isCurrent
|
||||
? "bg-emerald-500/20 border border-emerald-500/30"
|
||||
: isComplete
|
||||
? "bg-green-500/10 border border-green-500/20"
|
||||
: "bg-slate-700/30 border border-slate-600/30"
|
||||
}`}
|
||||
>
|
||||
{isComplete ? (
|
||||
<CheckCircle2 className="h-5 w-5 text-green-400 mx-auto mb-1" />
|
||||
) : (
|
||||
<Clock className="h-5 w-5 text-slate-400 mx-auto mb-1" />
|
||||
)}
|
||||
<p className="text-sm font-medium text-emerald-100">
|
||||
{getPhaseLabel(phase)}
|
||||
</p>
|
||||
<p className="text-xs text-slate-400">
|
||||
{completed}/{total}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Quick Actions Grid */}
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
<Link href="/staff/onboarding/checklist">
|
||||
<Card className="bg-slate-800/50 border-slate-700/50 hover:border-emerald-500/50 transition-all cursor-pointer h-full">
|
||||
<CardContent className="pt-6">
|
||||
<div className="p-2 rounded bg-emerald-500/20 text-emerald-400 w-fit mb-3">
|
||||
<CheckCircle2 className="h-5 w-5" />
|
||||
</div>
|
||||
<h3 className="font-semibold text-emerald-100 mb-1">
|
||||
Complete Checklist
|
||||
</h3>
|
||||
<p className="text-sm text-slate-400">
|
||||
Track your onboarding tasks
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
<Link href="/staff/directory">
|
||||
<Card className="bg-slate-800/50 border-slate-700/50 hover:border-emerald-500/50 transition-all cursor-pointer h-full">
|
||||
<CardContent className="pt-6">
|
||||
<div className="p-2 rounded bg-blue-500/20 text-blue-400 w-fit mb-3">
|
||||
<Users className="h-5 w-5" />
|
||||
</div>
|
||||
<h3 className="font-semibold text-emerald-100 mb-1">
|
||||
Meet Your Team
|
||||
</h3>
|
||||
<p className="text-sm text-slate-400">
|
||||
Browse the staff directory
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
<Link href="/staff/learning">
|
||||
<Card className="bg-slate-800/50 border-slate-700/50 hover:border-emerald-500/50 transition-all cursor-pointer h-full">
|
||||
<CardContent className="pt-6">
|
||||
<div className="p-2 rounded bg-purple-500/20 text-purple-400 w-fit mb-3">
|
||||
<BookOpen className="h-5 w-5" />
|
||||
</div>
|
||||
<h3 className="font-semibold text-emerald-100 mb-1">
|
||||
Learning Portal
|
||||
</h3>
|
||||
<p className="text-sm text-slate-400">
|
||||
Training courses & resources
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
<Link href="/staff/handbook">
|
||||
<Card className="bg-slate-800/50 border-slate-700/50 hover:border-emerald-500/50 transition-all cursor-pointer h-full">
|
||||
<CardContent className="pt-6">
|
||||
<div className="p-2 rounded bg-orange-500/20 text-orange-400 w-fit mb-3">
|
||||
<Target className="h-5 w-5" />
|
||||
</div>
|
||||
<h3 className="font-semibold text-emerald-100 mb-1">
|
||||
Team Handbook
|
||||
</h3>
|
||||
<p className="text-sm text-slate-400">
|
||||
Policies & guidelines
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Main Content Grid */}
|
||||
<div className="grid lg:grid-cols-3 gap-6">
|
||||
{/* Current Phase Tasks */}
|
||||
<div className="lg:col-span-2">
|
||||
<Card className="bg-slate-800/50 border-slate-700/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-emerald-100 flex items-center gap-2">
|
||||
<Sparkles className="h-5 w-5 text-emerald-400" />
|
||||
Current Tasks - {getPhaseLabel(currentPhase)}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-slate-400">
|
||||
Focus on completing these tasks first
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{data?.progress?.[currentPhase as keyof typeof data.progress]
|
||||
?.slice(0, 5)
|
||||
.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`flex items-center gap-3 p-3 rounded-lg ${
|
||||
item.completed
|
||||
? "bg-green-500/10 border border-green-500/20"
|
||||
: "bg-slate-700/30 border border-slate-600/30"
|
||||
}`}
|
||||
>
|
||||
{item.completed ? (
|
||||
<CheckCircle2 className="h-5 w-5 text-green-400 flex-shrink-0" />
|
||||
) : (
|
||||
<div className="h-5 w-5 rounded-full border-2 border-slate-500 flex-shrink-0" />
|
||||
)}
|
||||
<span
|
||||
className={
|
||||
item.completed
|
||||
? "text-slate-400 line-through"
|
||||
: "text-emerald-100"
|
||||
}
|
||||
>
|
||||
{item.checklist_item}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Link href="/staff/onboarding/checklist">
|
||||
<Button className="w-full mt-4 bg-emerald-600 hover:bg-emerald-700">
|
||||
View Full Checklist
|
||||
<ArrowRight className="h-4 w-4 ml-2" />
|
||||
</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-6">
|
||||
{/* Manager Card */}
|
||||
{data?.manager && (
|
||||
<Card className="bg-slate-800/50 border-slate-700/50">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-emerald-100 text-lg flex items-center gap-2">
|
||||
<Coffee className="h-4 w-4 text-emerald-400" />
|
||||
Your Manager
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar className="h-12 w-12">
|
||||
<AvatarImage src={data.manager.avatar_url || ""} />
|
||||
<AvatarFallback className="bg-emerald-500/20 text-emerald-300">
|
||||
{getInitials(data.manager.full_name)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<p className="font-medium text-emerald-100">
|
||||
{data.manager.full_name}
|
||||
</p>
|
||||
<p className="text-sm text-slate-400">
|
||||
{data.manager.email}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full mt-4 border-emerald-500/30 text-emerald-300 hover:bg-emerald-500/10"
|
||||
>
|
||||
<MessageSquare className="h-4 w-4 mr-2" />
|
||||
Send Message
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Important Links */}
|
||||
<Card className="bg-slate-800/50 border-slate-700/50">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-emerald-100 text-lg">
|
||||
Quick Links
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<a
|
||||
href="https://discord.gg/aethex"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 p-2 rounded hover:bg-slate-700/50 text-slate-300 hover:text-emerald-300 transition-colors"
|
||||
>
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
Join Discord Server
|
||||
</a>
|
||||
<Link
|
||||
href="/staff/knowledge-base"
|
||||
className="flex items-center gap-2 p-2 rounded hover:bg-slate-700/50 text-slate-300 hover:text-emerald-300 transition-colors"
|
||||
>
|
||||
<BookOpen className="h-4 w-4" />
|
||||
Knowledge Base
|
||||
</Link>
|
||||
<Link
|
||||
href="/documentation"
|
||||
className="flex items-center gap-2 p-2 rounded hover:bg-slate-700/50 text-slate-300 hover:text-emerald-300 transition-colors"
|
||||
>
|
||||
<Target className="h-4 w-4" />
|
||||
Documentation
|
||||
</Link>
|
||||
<Link
|
||||
href="/staff/announcements"
|
||||
className="flex items-center gap-2 p-2 rounded hover:bg-slate-700/50 text-slate-300 hover:text-emerald-300 transition-colors"
|
||||
>
|
||||
<Calendar className="h-4 w-4" />
|
||||
Announcements
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Achievement */}
|
||||
{data?.summary?.percentage === 100 && (
|
||||
<Card className="bg-gradient-to-br from-emerald-500/20 to-teal-500/20 border-emerald-500/30">
|
||||
<CardContent className="pt-6 text-center">
|
||||
<div className="w-16 h-16 rounded-full bg-emerald-500/20 border border-emerald-500/30 flex items-center justify-center mx-auto mb-4">
|
||||
<Sparkles className="h-8 w-8 text-emerald-400" />
|
||||
</div>
|
||||
<h3 className="font-bold text-emerald-100 text-lg mb-1">
|
||||
Onboarding Complete!
|
||||
</h3>
|
||||
<p className="text-sm text-emerald-200/70">
|
||||
You've completed all onboarding tasks. Welcome to the
|
||||
team!
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
454
client/pages/staff/StaffOnboardingChecklist.tsx
Normal file
454
client/pages/staff/StaffOnboardingChecklist.tsx
Normal file
|
|
@ -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<string | null>(null);
|
||||
const [data, setData] = useState<OnboardingData | null>(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 (
|
||||
<Layout>
|
||||
<SEO
|
||||
title="Onboarding Checklist"
|
||||
description="Track your onboarding progress"
|
||||
/>
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 flex items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-emerald-400" />
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<SEO
|
||||
title="Onboarding Checklist"
|
||||
description="Track your onboarding progress"
|
||||
/>
|
||||
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900">
|
||||
{/* Background effects */}
|
||||
<div className="absolute inset-0 opacity-30">
|
||||
<div className="absolute top-20 left-10 w-96 h-96 bg-emerald-600 rounded-full mix-blend-multiply filter blur-3xl animate-pulse" />
|
||||
<div className="absolute bottom-20 right-10 w-96 h-96 bg-teal-600 rounded-full mix-blend-multiply filter blur-3xl animate-pulse" />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="container mx-auto max-w-4xl px-4 py-16">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<Link href="/staff/onboarding">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-emerald-300 hover:text-emerald-200 hover:bg-emerald-500/10 mb-4"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Back to Onboarding
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-3 rounded-lg bg-emerald-500/20 border border-emerald-500/30">
|
||||
<ClipboardCheck className="h-6 w-6 text-emerald-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-emerald-100">
|
||||
Onboarding Checklist
|
||||
</h1>
|
||||
<p className="text-emerald-200/70">
|
||||
Track and complete your onboarding tasks
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overall Progress */}
|
||||
<Card className="bg-slate-800/50 border-emerald-500/30">
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-emerald-100 font-medium">
|
||||
Overall Progress
|
||||
</span>
|
||||
<span className="text-emerald-300 font-bold">
|
||||
{data?.summary?.completed || 0}/{data?.summary?.total || 0}{" "}
|
||||
tasks ({data?.summary?.percentage || 0}%)
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={data?.summary?.percentage || 0}
|
||||
className="h-3"
|
||||
/>
|
||||
{data?.summary?.percentage === 100 && (
|
||||
<div className="flex items-center gap-2 mt-3 text-green-400">
|
||||
<Trophy className="h-5 w-5" />
|
||||
<span className="font-medium">
|
||||
All tasks completed! Welcome to the team!
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="w-full bg-slate-800/50 border border-slate-700/50 p-1 mb-6">
|
||||
{(["day1", "week1", "month1"] as const).map((phase) => {
|
||||
const info = PHASE_INFO[phase];
|
||||
const progress = getPhaseProgress(phase);
|
||||
const Icon = info.icon;
|
||||
|
||||
return (
|
||||
<TabsTrigger
|
||||
key={phase}
|
||||
value={phase}
|
||||
className="flex-1 data-[state=active]:bg-emerald-600 data-[state=active]:text-white"
|
||||
>
|
||||
<Icon className="h-4 w-4 mr-2" />
|
||||
{info.label}
|
||||
{progress.percentage === 100 && (
|
||||
<CheckCircle2 className="h-4 w-4 ml-2 text-green-400" />
|
||||
)}
|
||||
</TabsTrigger>
|
||||
);
|
||||
})}
|
||||
</TabsList>
|
||||
|
||||
{(["day1", "week1", "month1"] as const).map((phase) => {
|
||||
const info = PHASE_INFO[phase];
|
||||
const progress = getPhaseProgress(phase);
|
||||
const items = data?.progress[phase] || [];
|
||||
|
||||
return (
|
||||
<TabsContent key={phase} value={phase}>
|
||||
<Card className="bg-slate-800/50 border-slate-700/50">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-emerald-100 flex items-center gap-2">
|
||||
{info.label}
|
||||
{progress.percentage === 100 && (
|
||||
<Badge className="bg-green-500/20 text-green-300 border-green-500/30">
|
||||
Complete
|
||||
</Badge>
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-slate-400">
|
||||
{info.description}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-2xl font-bold text-emerald-100">
|
||||
{progress.percentage}%
|
||||
</p>
|
||||
<p className="text-sm text-slate-400">
|
||||
{progress.completed}/{progress.total} done
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Progress
|
||||
value={progress.percentage}
|
||||
className="h-2 mt-2"
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{items.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`flex items-start gap-4 p-4 rounded-lg transition-all ${
|
||||
item.completed
|
||||
? "bg-green-500/10 border border-green-500/20"
|
||||
: "bg-slate-700/30 border border-slate-600/30 hover:border-emerald-500/30"
|
||||
}`}
|
||||
>
|
||||
<div className="pt-0.5">
|
||||
{saving === item.id ? (
|
||||
<Loader2 className="h-5 w-5 animate-spin text-emerald-400" />
|
||||
) : (
|
||||
<Checkbox
|
||||
checked={item.completed}
|
||||
onCheckedChange={() => toggleItem(item)}
|
||||
className={`h-5 w-5 ${
|
||||
item.completed
|
||||
? "border-green-500 bg-green-500 data-[state=checked]:bg-green-500"
|
||||
: "border-slate-500"
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p
|
||||
className={`font-medium ${
|
||||
item.completed
|
||||
? "text-slate-400 line-through"
|
||||
: "text-emerald-100"
|
||||
}`}
|
||||
>
|
||||
{item.checklist_item}
|
||||
</p>
|
||||
{item.completed && item.completed_at && (
|
||||
<div className="flex items-center gap-1 mt-1 text-xs text-slate-500">
|
||||
<Clock className="h-3 w-3" />
|
||||
Completed {formatDate(item.completed_at)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{item.completed && (
|
||||
<CheckCircle2 className="h-5 w-5 text-green-400 flex-shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{progress.percentage === 100 && (
|
||||
<div className="mt-6 p-4 rounded-lg bg-gradient-to-r from-green-500/10 to-emerald-500/10 border border-green-500/20 text-center">
|
||||
<Trophy className="h-8 w-8 text-green-400 mx-auto mb-2" />
|
||||
<p className="font-medium text-green-300">
|
||||
{info.label} Complete!
|
||||
</p>
|
||||
<p className="text-sm text-slate-400">
|
||||
Great job completing all {info.label.toLowerCase()}{" "}
|
||||
tasks
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
|
||||
{/* Help Section */}
|
||||
<Card className="mt-6 bg-slate-800/30 border-slate-700/30">
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-2 rounded bg-blue-500/20 text-blue-400">
|
||||
<Calendar className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-emerald-100 mb-1">
|
||||
Need Help?
|
||||
</h3>
|
||||
<p className="text-sm text-slate-400">
|
||||
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{" "}
|
||||
<Link
|
||||
href="/staff/knowledge-base"
|
||||
className="text-emerald-400 hover:underline"
|
||||
>
|
||||
Knowledge Base
|
||||
</Link>{" "}
|
||||
for detailed guides.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
97
supabase/migrations/20260126_add_staff_onboarding.sql
Normal file
97
supabase/migrations/20260126_add_staff_onboarding.sql
Normal file
|
|
@ -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();
|
||||
Loading…
Reference in a new issue