- 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
289 lines
9 KiB
TypeScript
289 lines
9 KiB
TypeScript
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" },
|
|
});
|
|
}
|
|
};
|