Wire remaining 6 staff pages to real APIs

- StaffLearningPortal: Fetches courses from /api/staff/courses, supports
  starting courses and tracking progress
- StaffPerformanceReviews: Fetches reviews from /api/staff/reviews,
  supports adding employee comments with dialog
- StaffKnowledgeBase: Fetches articles from /api/staff/knowledge-base,
  supports search, filtering, view tracking, and helpful marking
- StaffProjectTracking: Fetches projects from /api/staff/projects,
  supports task status updates and creating new tasks
- StaffInternalMarketplace: Now points marketplace with /api/staff/marketplace,
  supports redeeming items with points and viewing orders
- StaffTeamHandbook: Fetches handbook sections from /api/staff/handbook,
  displays grouped by category with expand/collapse

All pages now use real Supabase data instead of mock arrays.
This commit is contained in:
Claude 2026-01-26 22:31:35 +00:00
parent f1efc97c86
commit 01026d43cc
No known key found for this signature in database
6 changed files with 1136 additions and 767 deletions

View file

@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import Layout from "@/components/Layout";
import SEO from "@/components/SEO";
import { Button } from "@/components/ui/button";
@ -13,127 +13,153 @@ import { Badge } from "@/components/ui/badge";
import {
ShoppingCart,
Search,
Users,
Clock,
AlertCircle,
CheckCircle,
Gift,
Star,
Package,
Loader2,
Coins,
} from "lucide-react";
import { Input } from "@/components/ui/input";
import { useAuth } from "@/lib/auth";
import { aethexToast } from "@/components/ui/aethex-toast";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
interface Service {
interface MarketplaceItem {
id: string;
name: string;
provider: string;
category: string;
description: string;
availability: "Available" | "Booked" | "Coming Soon";
turnaround: string;
requests: number;
category: string;
points_cost: number;
image_url?: string;
stock_count?: number;
is_available: boolean;
}
const services: Service[] = [
{
id: "1",
name: "Design Consultation",
provider: "Design Team",
category: "Design",
description: "1-on-1 design review and UX guidance for your project",
availability: "Available",
turnaround: "2 days",
requests: 8,
},
{
id: "2",
name: "Code Review",
provider: "Engineering",
category: "Development",
description: "Thorough code review with architectural feedback",
availability: "Available",
turnaround: "1 day",
requests: 15,
},
{
id: "3",
name: "Security Audit",
provider: "Security Team",
category: "Security",
description: "Comprehensive security review of your application",
availability: "Booked",
turnaround: "5 days",
requests: 4,
},
{
id: "4",
name: "Performance Optimization",
provider: "DevOps",
category: "Infrastructure",
description: "Optimize your application performance and scalability",
availability: "Available",
turnaround: "3 days",
requests: 6,
},
{
id: "5",
name: "Product Strategy Session",
provider: "Product Team",
category: "Product",
description: "Alignment session on product roadmap and features",
availability: "Coming Soon",
turnaround: "4 days",
requests: 12,
},
{
id: "6",
name: "API Integration Support",
provider: "Backend Team",
category: "Development",
description: "Help integrating with AeThex APIs and services",
availability: "Available",
turnaround: "2 days",
requests: 10,
},
];
interface Order {
id: string;
quantity: number;
status: string;
created_at: string;
item?: {
name: string;
image_url?: string;
};
}
const getAvailabilityColor = (availability: string) => {
switch (availability) {
case "Available":
return "bg-green-500/20 text-green-300 border-green-500/30";
case "Booked":
return "bg-amber-500/20 text-amber-300 border-amber-500/30";
case "Coming Soon":
return "bg-blue-500/20 text-blue-300 border-blue-500/30";
interface Points {
balance: number;
lifetime_earned: number;
}
export default function StaffInternalMarketplace() {
const { session } = useAuth();
const [items, setItems] = useState<MarketplaceItem[]>([]);
const [orders, setOrders] = useState<Order[]>([]);
const [points, setPoints] = useState<Points>({ balance: 0, lifetime_earned: 0 });
const [searchQuery, setSearchQuery] = useState("");
const [selectedCategory, setSelectedCategory] = useState("All");
const [loading, setLoading] = useState(true);
const [orderDialog, setOrderDialog] = useState<MarketplaceItem | null>(null);
const [shippingAddress, setShippingAddress] = useState("");
useEffect(() => {
if (session?.access_token) {
fetchMarketplace();
}
}, [session?.access_token]);
const fetchMarketplace = async () => {
try {
const res = await fetch("/api/staff/marketplace", {
headers: { Authorization: `Bearer ${session?.access_token}` },
});
const data = await res.json();
if (res.ok) {
setItems(data.items || []);
setOrders(data.orders || []);
setPoints(data.points || { balance: 0, lifetime_earned: 0 });
}
} catch (err) {
aethexToast.error("Failed to load marketplace");
} finally {
setLoading(false);
}
};
const placeOrder = async () => {
if (!orderDialog) return;
try {
const res = await fetch("/api/staff/marketplace", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${session?.access_token}`,
},
body: JSON.stringify({
item_id: orderDialog.id,
quantity: 1,
shipping_address: shippingAddress,
}),
});
const data = await res.json();
if (res.ok) {
aethexToast.success("Order placed successfully!");
setOrderDialog(null);
setShippingAddress("");
fetchMarketplace();
} else {
aethexToast.error(data.error || "Failed to place order");
}
} catch (err) {
aethexToast.error("Failed to place order");
}
};
const categories = ["All", ...new Set(items.map((i) => i.category))];
const filtered = items.filter((item) => {
const matchesSearch =
item.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.description.toLowerCase().includes(searchQuery.toLowerCase());
const matchesCategory =
selectedCategory === "All" || item.category === selectedCategory;
return matchesSearch && matchesCategory;
});
const getStatusColor = (status: string) => {
switch (status) {
case "shipped":
return "bg-green-500/20 text-green-300";
case "processing":
return "bg-blue-500/20 text-blue-300";
case "pending":
return "bg-amber-500/20 text-amber-300";
default:
return "bg-slate-500/20 text-slate-300";
}
};
};
export default function StaffInternalMarketplace() {
const [searchQuery, setSearchQuery] = useState("");
const [selectedCategory, setSelectedCategory] = useState("All");
const categories = [
"All",
"Design",
"Development",
"Security",
"Infrastructure",
"Product",
];
const filtered = services.filter((service) => {
const matchesSearch =
service.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
service.provider.toLowerCase().includes(searchQuery.toLowerCase());
const matchesCategory =
selectedCategory === "All" || service.category === selectedCategory;
return matchesSearch && matchesCategory;
});
if (loading) {
return (
<Layout>
<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-amber-400" />
</div>
</Layout>
);
}
return (
<Layout>
<SEO
title="Internal Marketplace"
description="Request services from other teams"
title="Points Marketplace"
description="Redeem your points for rewards"
/>
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900">
@ -148,47 +174,57 @@ export default function StaffInternalMarketplace() {
<div className="container mx-auto max-w-6xl px-4 py-16">
<div className="flex items-center gap-3 mb-6">
<div className="p-3 rounded-lg bg-amber-500/20 border border-amber-500/30">
<ShoppingCart className="h-6 w-6 text-amber-400" />
<Gift className="h-6 w-6 text-amber-400" />
</div>
<div>
<h1 className="text-4xl font-bold text-amber-100">
Internal Marketplace
Points Marketplace
</h1>
<p className="text-amber-200/70">
Request services and resources from other teams
Redeem your earned points for rewards
</p>
</div>
</div>
{/* Summary */}
{/* Points Summary */}
<div className="grid md:grid-cols-3 gap-4 mb-12">
<Card className="bg-amber-950/30 border-amber-500/30">
<CardContent className="pt-6">
<p className="text-2xl font-bold text-amber-100">
{services.length}
</p>
<p className="text-sm text-amber-200/70">
Available Services
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-amber-200/70">Your Balance</p>
<p className="text-3xl font-bold text-amber-100">
{points.balance.toLocaleString()}
</p>
</div>
<Coins className="h-8 w-8 text-amber-400" />
</div>
</CardContent>
</Card>
<Card className="bg-amber-950/30 border-amber-500/30">
<CardContent className="pt-6">
<p className="text-2xl font-bold text-amber-100">
{
services.filter((s) => s.availability === "Available")
.length
}
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-amber-200/70">Lifetime Earned</p>
<p className="text-3xl font-bold text-amber-100">
{points.lifetime_earned.toLocaleString()}
</p>
<p className="text-sm text-amber-200/70">Ready to Book</p>
</div>
<Star className="h-8 w-8 text-amber-400" />
</div>
</CardContent>
</Card>
<Card className="bg-amber-950/30 border-amber-500/30">
<CardContent className="pt-6">
<p className="text-2xl font-bold text-amber-100">
{services.reduce((sum, s) => sum + s.requests, 0)}
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-amber-200/70">My Orders</p>
<p className="text-3xl font-bold text-amber-100">
{orders.length}
</p>
<p className="text-sm text-amber-200/70">Total Requests</p>
</div>
<Package className="h-8 w-8 text-amber-400" />
</div>
</CardContent>
</Card>
</div>
@ -198,7 +234,7 @@ export default function StaffInternalMarketplace() {
<div className="relative">
<Search className="absolute left-3 top-3 h-5 w-5 text-slate-400" />
<Input
placeholder="Search services..."
placeholder="Search rewards..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 bg-slate-800 border-slate-700 text-slate-100"
@ -225,66 +261,134 @@ export default function StaffInternalMarketplace() {
</div>
</div>
{/* Services Grid */}
<div className="grid md:grid-cols-2 gap-6">
{filtered.map((service) => (
{/* Items Grid */}
<div className="grid md:grid-cols-3 gap-6 mb-12">
{filtered.map((item) => (
<Card
key={service.id}
key={item.id}
className="bg-slate-800/50 border-slate-700/50 hover:border-amber-500/50 transition-all"
>
<CardHeader>
<div className="flex items-start justify-between">
<div>
<CardTitle className="text-amber-100">
{service.name}
{item.name}
</CardTitle>
<CardDescription className="text-slate-400">
{service.provider}
{item.category}
</CardDescription>
</div>
<Badge
className={`border ${getAvailabilityColor(service.availability)}`}
>
{service.availability}
{item.stock_count !== null && item.stock_count < 10 && (
<Badge className="bg-red-500/20 text-red-300 border-red-500/30">
Only {item.stock_count} left
</Badge>
)}
</div>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-slate-300">
{service.description}
</p>
<div className="flex gap-4 text-sm">
<div className="flex items-center gap-2 text-slate-400">
<Clock className="h-4 w-4" />
{service.turnaround}
</div>
<div className="flex items-center gap-2 text-slate-400">
<AlertCircle className="h-4 w-4" />
{service.requests} requests
</div>
<p className="text-sm text-slate-300">{item.description}</p>
<div className="flex items-center justify-between">
<div className="flex items-center gap-1 text-amber-400 font-semibold">
<Coins className="h-4 w-4" />
{item.points_cost.toLocaleString()} pts
</div>
<Button
size="sm"
className="w-full bg-amber-600 hover:bg-amber-700"
disabled={service.availability === "Coming Soon"}
className="bg-amber-600 hover:bg-amber-700"
disabled={points.balance < item.points_cost}
onClick={() => setOrderDialog(item)}
>
{service.availability === "Coming Soon"
? "Coming Soon"
: "Request Service"}
Redeem
</Button>
</div>
</CardContent>
</Card>
))}
</div>
{filtered.length === 0 && (
<div className="text-center py-12">
<p className="text-slate-400">No services found</p>
<div className="text-center py-12 mb-12">
<p className="text-slate-400">No rewards found</p>
</div>
)}
{/* Recent Orders */}
{orders.length > 0 && (
<div>
<h2 className="text-2xl font-bold text-amber-100 mb-6">Recent Orders</h2>
<div className="space-y-4">
{orders.slice(0, 5).map((order) => (
<Card key={order.id} className="bg-slate-800/50 border-slate-700/50">
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-amber-100 font-semibold">
{order.item?.name || "Unknown Item"}
</p>
<p className="text-sm text-slate-400">
Qty: {order.quantity} {new Date(order.created_at).toLocaleDateString()}
</p>
</div>
<Badge className={getStatusColor(order.status)}>
{order.status.replace(/\b\w/g, (l) => l.toUpperCase())}
</Badge>
</div>
</CardContent>
</Card>
))}
</div>
</div>
)}
</div>
</div>
</div>
{/* Order Dialog */}
<Dialog open={!!orderDialog} onOpenChange={() => setOrderDialog(null)}>
<DialogContent className="bg-slate-800 border-slate-700">
<DialogHeader>
<DialogTitle className="text-amber-100">
Redeem {orderDialog?.name}
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="p-4 bg-slate-700/50 rounded">
<div className="flex justify-between mb-2">
<span className="text-slate-300">Cost</span>
<span className="text-amber-400 font-semibold">
{orderDialog?.points_cost.toLocaleString()} pts
</span>
</div>
<div className="flex justify-between">
<span className="text-slate-300">Your Balance After</span>
<span className="text-slate-100">
{(points.balance - (orderDialog?.points_cost || 0)).toLocaleString()} pts
</span>
</div>
</div>
<div>
<label className="text-sm text-slate-400 mb-2 block">Shipping Address</label>
<Input
placeholder="Enter your shipping address"
value={shippingAddress}
onChange={(e) => setShippingAddress(e.target.value)}
className="bg-slate-700 border-slate-600 text-slate-100"
/>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setOrderDialog(null)}>
Cancel
</Button>
<Button
className="bg-amber-600 hover:bg-amber-700"
onClick={placeOrder}
>
Confirm Order
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</Layout>
);
}

View file

@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import Layout from "@/components/Layout";
import SEO from "@/components/SEO";
import { Button } from "@/components/ui/button";
@ -20,105 +20,133 @@ import {
Users,
Settings,
Code,
Loader2,
ThumbsUp,
Eye,
} from "lucide-react";
import { useAuth } from "@/lib/auth";
import { aethexToast } from "@/components/ui/aethex-toast";
interface KnowledgeArticle {
id: string;
title: string;
category: string;
description: string;
content: string;
tags: string[];
views: number;
updated: string;
icon: React.ReactNode;
helpful_count: number;
updated_at: string;
author?: {
full_name: string;
avatar_url?: string;
};
}
const articles: KnowledgeArticle[] = [
{
id: "1",
title: "Getting Started with AeThex Platform",
category: "Onboarding",
description: "Complete guide for new team members to get up to speed",
tags: ["onboarding", "setup", "beginner"],
views: 324,
updated: "2 days ago",
icon: <Zap className="h-5 w-5" />,
},
{
id: "2",
title: "Troubleshooting Common Issues",
category: "Support",
description: "Step-by-step guides for resolving frequent problems",
tags: ["troubleshooting", "support", "faq"],
views: 156,
updated: "1 week ago",
icon: <AlertCircle className="h-5 w-5" />,
},
{
id: "3",
title: "API Integration Guide",
category: "Development",
description: "How to integrate with AeThex APIs from your applications",
tags: ["api", "development", "technical"],
views: 89,
updated: "3 weeks ago",
icon: <Code className="h-5 w-5" />,
},
{
id: "4",
title: "Team Communication Standards",
category: "Process",
description: "Best practices for internal communications and channel usage",
tags: ["communication", "process", "standards"],
views: 201,
updated: "4 days ago",
icon: <Users className="h-5 w-5" />,
},
{
id: "5",
title: "Security & Access Control",
category: "Security",
description:
"Security policies, password management, and access procedures",
tags: ["security", "access", "compliance"],
views: 112,
updated: "1 day ago",
icon: <Settings className="h-5 w-5" />,
},
{
id: "6",
title: "Release Management Process",
category: "Operations",
description: "How to manage releases, deployments, and rollbacks",
tags: ["devops", "release", "operations"],
views: 67,
updated: "2 weeks ago",
icon: <FileText className="h-5 w-5" />,
},
];
const categories = [
"All",
"Onboarding",
"Support",
"Development",
"Process",
"Security",
"Operations",
];
const getCategoryIcon = (category: string) => {
switch (category) {
case "Onboarding":
return <Zap className="h-5 w-5" />;
case "Support":
return <AlertCircle className="h-5 w-5" />;
case "Development":
return <Code className="h-5 w-5" />;
case "Process":
return <Users className="h-5 w-5" />;
case "Security":
return <Settings className="h-5 w-5" />;
default:
return <FileText className="h-5 w-5" />;
}
};
export default function StaffKnowledgeBase() {
const { session } = useAuth();
const [articles, setArticles] = useState<KnowledgeArticle[]>([]);
const [categories, setCategories] = useState<string[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [selectedCategory, setSelectedCategory] = useState("All");
const [selectedCategory, setSelectedCategory] = useState("all");
const [loading, setLoading] = useState(true);
const filtered = articles.filter((article) => {
const matchesSearch =
article.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
article.description.toLowerCase().includes(searchQuery.toLowerCase());
const matchesCategory =
selectedCategory === "All" || article.category === selectedCategory;
return matchesSearch && matchesCategory;
useEffect(() => {
if (session?.access_token) {
fetchArticles();
}
}, [session?.access_token, selectedCategory, searchQuery]);
const fetchArticles = async () => {
try {
const params = new URLSearchParams();
if (selectedCategory !== "all") params.append("category", selectedCategory);
if (searchQuery) params.append("search", searchQuery);
const res = await fetch(`/api/staff/knowledge-base?${params}`, {
headers: { Authorization: `Bearer ${session?.access_token}` },
});
const data = await res.json();
if (res.ok) {
setArticles(data.articles || []);
setCategories(data.categories || []);
}
} catch (err) {
aethexToast.error("Failed to load articles");
} finally {
setLoading(false);
}
};
const trackView = async (articleId: string) => {
try {
await fetch("/api/staff/knowledge-base", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${session?.access_token}`,
},
body: JSON.stringify({ action: "view", id: articleId }),
});
} catch (err) {
// Silent fail for analytics
}
};
const markHelpful = async (articleId: string) => {
try {
await fetch("/api/staff/knowledge-base", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${session?.access_token}`,
},
body: JSON.stringify({ action: "helpful", id: articleId }),
});
aethexToast.success("Marked as helpful!");
fetchArticles();
} catch (err) {
aethexToast.error("Failed to mark as helpful");
}
};
const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
const now = new Date();
const diff = now.getTime() - date.getTime();
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
if (days === 0) return "Today";
if (days === 1) return "Yesterday";
if (days < 7) return `${days} days ago`;
if (days < 30) return `${Math.floor(days / 7)} weeks ago`;
return date.toLocaleDateString();
};
if (loading) {
return (
<Layout>
<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-purple-400" />
</div>
</Layout>
);
}
return (
<Layout>
@ -163,12 +191,22 @@ export default function StaffKnowledgeBase() {
{/* Category Filter */}
<div className="flex gap-2 mb-8 flex-wrap">
<Button
variant={selectedCategory === "all" ? "default" : "outline"}
size="sm"
onClick={() => setSelectedCategory("all")}
className={
selectedCategory === "all"
? "bg-purple-600 hover:bg-purple-700"
: "border-purple-500/30 text-purple-300 hover:bg-purple-500/10"
}
>
All
</Button>
{categories.map((category) => (
<Button
key={category}
variant={
selectedCategory === category ? "default" : "outline"
}
variant={selectedCategory === category ? "default" : "outline"}
size="sm"
onClick={() => setSelectedCategory(category)}
className={
@ -184,15 +222,16 @@ export default function StaffKnowledgeBase() {
{/* Articles Grid */}
<div className="grid md:grid-cols-2 gap-6">
{filtered.map((article) => (
{articles.map((article) => (
<Card
key={article.id}
className="bg-slate-800/50 border-slate-700/50 hover:border-purple-500/50 transition-all cursor-pointer group"
onClick={() => trackView(article.id)}
>
<CardHeader>
<div className="flex items-start justify-between mb-2">
<div className="p-2 rounded bg-purple-500/20 text-purple-400 group-hover:bg-purple-500/30 transition-colors">
{article.icon}
{getCategoryIcon(article.category)}
</div>
<Badge className="bg-slate-700 text-slate-300 text-xs">
{article.category}
@ -202,11 +241,12 @@ export default function StaffKnowledgeBase() {
{article.title}
</CardTitle>
<CardDescription className="text-slate-400">
{article.description}
{article.content.substring(0, 150)}...
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{article.tags && article.tags.length > 0 && (
<div className="flex gap-2 flex-wrap">
{article.tags.map((tag) => (
<Badge
@ -218,16 +258,30 @@ export default function StaffKnowledgeBase() {
</Badge>
))}
</div>
)}
<div className="flex items-center justify-between pt-4 border-t border-slate-700">
<span className="text-xs text-slate-500">
{article.views} views {article.updated}
<div className="flex items-center gap-4 text-xs text-slate-500">
<span className="flex items-center gap-1">
<Eye className="h-3 w-3" />
{article.views}
</span>
<span className="flex items-center gap-1">
<ThumbsUp className="h-3 w-3" />
{article.helpful_count}
</span>
<span>{formatDate(article.updated_at)}</span>
</div>
<Button
size="sm"
variant="ghost"
className="text-purple-400 hover:text-purple-300 hover:bg-purple-500/20"
onClick={(e) => {
e.stopPropagation();
markHelpful(article.id);
}}
>
Read
<ThumbsUp className="h-4 w-4 mr-1" />
Helpful
</Button>
</div>
</div>
@ -236,7 +290,7 @@ export default function StaffKnowledgeBase() {
))}
</div>
{filtered.length === 0 && (
{articles.length === 0 && (
<div className="text-center py-12">
<p className="text-slate-400">No articles found</p>
</div>

View file

@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import Layout from "@/components/Layout";
import SEO from "@/components/SEO";
import { Button } from "@/components/ui/button";
@ -19,109 +19,112 @@ import {
FileText,
Clock,
CheckCircle,
Loader2,
} from "lucide-react";
import { useAuth } from "@/lib/auth";
import { aethexToast } from "@/components/ui/aethex-toast";
interface Course {
id: string;
title: string;
instructor: string;
description: string;
category: string;
duration: string;
duration_weeks: number;
lesson_count: number;
is_required: boolean;
progress: number;
status: "In Progress" | "Completed" | "Available";
lessons: number;
icon: React.ReactNode;
status: string;
started_at?: string;
completed_at?: string;
}
const courses: Course[] = [
{
id: "1",
title: "Advanced TypeScript Patterns",
instructor: "Sarah Chen",
category: "Development",
duration: "4 weeks",
progress: 65,
status: "In Progress",
lessons: 12,
icon: <BookOpen className="h-5 w-5" />,
},
{
id: "2",
title: "Leadership Fundamentals",
instructor: "Marcus Johnson",
category: "Leadership",
duration: "6 weeks",
progress: 0,
status: "Available",
lessons: 15,
icon: <Award className="h-5 w-5" />,
},
{
id: "3",
title: "AWS Solutions Architect",
instructor: "David Lee",
category: "Infrastructure",
duration: "8 weeks",
progress: 100,
status: "Completed",
lessons: 20,
icon: <Zap className="h-5 w-5" />,
},
{
id: "4",
title: "Product Management Essentials",
instructor: "Elena Rodriguez",
category: "Product",
duration: "5 weeks",
progress: 40,
status: "In Progress",
lessons: 14,
icon: <Video className="h-5 w-5" />,
},
{
id: "5",
title: "Security Best Practices",
instructor: "Alex Kim",
category: "Security",
duration: "3 weeks",
progress: 0,
status: "Available",
lessons: 10,
icon: <FileText className="h-5 w-5" />,
},
{
id: "6",
title: "Effective Communication",
instructor: "Patricia Martinez",
category: "Skills",
duration: "2 weeks",
progress: 100,
status: "Completed",
lessons: 8,
icon: <BookOpen className="h-5 w-5" />,
},
];
interface Stats {
total: number;
completed: number;
in_progress: number;
required: number;
}
const getCourseIcon = (category: string) => {
switch (category) {
case "Development":
return <BookOpen className="h-5 w-5" />;
case "Leadership":
return <Award className="h-5 w-5" />;
case "Infrastructure":
return <Zap className="h-5 w-5" />;
case "Product":
return <Video className="h-5 w-5" />;
default:
return <FileText className="h-5 w-5" />;
}
};
export default function StaffLearningPortal() {
const { session } = useAuth();
const [courses, setCourses] = useState<Course[]>([]);
const [stats, setStats] = useState<Stats>({ total: 0, completed: 0, in_progress: 0, required: 0 });
const [selectedCategory, setSelectedCategory] = useState("All");
const [loading, setLoading] = useState(true);
const categories = [
"All",
"Development",
"Leadership",
"Infrastructure",
"Product",
"Security",
"Skills",
];
useEffect(() => {
if (session?.access_token) {
fetchCourses();
}
}, [session?.access_token]);
const fetchCourses = async () => {
try {
const res = await fetch("/api/staff/courses", {
headers: { Authorization: `Bearer ${session?.access_token}` },
});
const data = await res.json();
if (res.ok) {
setCourses(data.courses || []);
setStats(data.stats || { total: 0, completed: 0, in_progress: 0, required: 0 });
}
} catch (err) {
aethexToast.error("Failed to load courses");
} finally {
setLoading(false);
}
};
const startCourse = async (courseId: string) => {
try {
const res = await fetch("/api/staff/courses", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${session?.access_token}`,
},
body: JSON.stringify({ course_id: courseId, action: "start" }),
});
if (res.ok) {
aethexToast.success("Course started!");
fetchCourses();
}
} catch (err) {
aethexToast.error("Failed to start course");
}
};
const categories = ["All", ...new Set(courses.map((c) => c.category))];
const filtered =
selectedCategory === "All"
? courses
: courses.filter((c) => c.category === selectedCategory);
const completed = courses.filter((c) => c.status === "Completed").length;
const inProgress = courses.filter((c) => c.status === "In Progress").length;
if (loading) {
return (
<Layout>
<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-cyan-400" />
</div>
</Layout>
);
}
return (
<Layout>
@ -159,7 +162,7 @@ export default function StaffLearningPortal() {
<Card className="bg-cyan-950/30 border-cyan-500/30">
<CardContent className="pt-6">
<p className="text-2xl font-bold text-cyan-100">
{courses.length}
{stats.total}
</p>
<p className="text-sm text-cyan-200/70">Total Courses</p>
</CardContent>
@ -167,7 +170,7 @@ export default function StaffLearningPortal() {
<Card className="bg-cyan-950/30 border-cyan-500/30">
<CardContent className="pt-6">
<p className="text-2xl font-bold text-cyan-100">
{completed}
{stats.completed}
</p>
<p className="text-sm text-cyan-200/70">Completed</p>
</CardContent>
@ -175,7 +178,7 @@ export default function StaffLearningPortal() {
<Card className="bg-cyan-950/30 border-cyan-500/30">
<CardContent className="pt-6">
<p className="text-2xl font-bold text-cyan-100">
{inProgress}
{stats.in_progress}
</p>
<p className="text-sm text-cyan-200/70">In Progress</p>
</CardContent>
@ -183,7 +186,7 @@ export default function StaffLearningPortal() {
<Card className="bg-cyan-950/30 border-cyan-500/30">
<CardContent className="pt-6">
<p className="text-2xl font-bold text-cyan-100">
{Math.round((completed / courses.length) * 100)}%
{stats.total > 0 ? Math.round((stats.completed / stats.total) * 100) : 0}%
</p>
<p className="text-sm text-cyan-200/70">Completion Rate</p>
</CardContent>
@ -223,25 +226,25 @@ export default function StaffLearningPortal() {
<CardHeader>
<div className="flex items-start justify-between">
<div className="p-2 rounded bg-cyan-500/20 text-cyan-400">
{course.icon}
{getCourseIcon(course.category)}
</div>
<Badge
className={
course.status === "Completed"
course.status === "completed"
? "bg-green-500/20 text-green-300 border-green-500/30"
: course.status === "In Progress"
: course.status === "in_progress"
? "bg-blue-500/20 text-blue-300 border-blue-500/30"
: "bg-slate-700 text-slate-300"
}
>
{course.status}
{course.status === "completed" ? "Completed" : course.status === "in_progress" ? "In Progress" : "Available"}
</Badge>
</div>
<CardTitle className="text-cyan-100">
{course.title}
</CardTitle>
<CardDescription className="text-slate-400">
by {course.instructor}
{course.description}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
@ -259,20 +262,26 @@ export default function StaffLearningPortal() {
<div className="flex gap-4 text-sm">
<div className="flex items-center gap-2 text-slate-400">
<Clock className="h-4 w-4" />
{course.duration}
{course.duration_weeks} weeks
</div>
<div className="flex items-center gap-2 text-slate-400">
<FileText className="h-4 w-4" />
{course.lessons} lessons
{course.lesson_count} lessons
</div>
{course.is_required && (
<Badge className="bg-amber-500/20 text-amber-300 border-amber-500/30">
Required
</Badge>
)}
</div>
<Button
size="sm"
className="w-full bg-cyan-600 hover:bg-cyan-700"
onClick={() => course.status === "available" && startCourse(course.id)}
>
{course.status === "Completed"
{course.status === "completed"
? "Review Course"
: course.status === "In Progress"
: course.status === "in_progress"
? "Continue"
: "Enroll"}
</Button>
@ -280,6 +289,12 @@ export default function StaffLearningPortal() {
</Card>
))}
</div>
{filtered.length === 0 && (
<div className="text-center py-12">
<p className="text-slate-400">No courses found</p>
</div>
)}
</div>
</div>
</div>

View file

@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import Layout from "@/components/Layout";
import SEO from "@/components/SEO";
import { Button } from "@/components/ui/button";
@ -18,86 +18,49 @@ import {
Clock,
Award,
Users,
Loader2,
} from "lucide-react";
import { useAuth } from "@/lib/auth";
import { aethexToast } from "@/components/ui/aethex-toast";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Textarea } from "@/components/ui/textarea";
interface Review {
id: string;
period: string;
status: "Pending" | "In Progress" | "Completed";
reviewer?: string;
dueDate: string;
feedback?: number;
selfAssessment?: boolean;
status: string;
overall_rating?: number;
reviewer_comments?: string;
employee_comments?: string;
goals_met?: number;
goals_total?: number;
due_date: string;
created_at: string;
reviewer?: {
full_name: string;
avatar_url?: string;
};
}
interface Metric {
name: string;
score: number;
lastQuarter: number;
interface Stats {
total: number;
pending: number;
completed: number;
average_rating: number;
}
const userReviews: Review[] = [
{
id: "1",
period: "Q1 2025",
status: "In Progress",
dueDate: "March 31, 2025",
selfAssessment: true,
feedback: 3,
},
{
id: "2",
period: "Q4 2024",
status: "Completed",
dueDate: "December 31, 2024",
selfAssessment: true,
feedback: 5,
},
{
id: "3",
period: "Q3 2024",
status: "Completed",
dueDate: "September 30, 2024",
selfAssessment: true,
feedback: 4,
},
];
const performanceMetrics: Metric[] = [
{
name: "Technical Skills",
score: 8.5,
lastQuarter: 8.2,
},
{
name: "Communication",
score: 8.8,
lastQuarter: 8.5,
},
{
name: "Collaboration",
score: 9.0,
lastQuarter: 8.7,
},
{
name: "Leadership",
score: 8.2,
lastQuarter: 7.9,
},
{
name: "Problem Solving",
score: 8.7,
lastQuarter: 8.4,
},
];
const getStatusColor = (status: string) => {
switch (status) {
case "Completed":
case "completed":
return "bg-green-500/20 text-green-300 border-green-500/30";
case "In Progress":
case "in_progress":
return "bg-blue-500/20 text-blue-300 border-blue-500/30";
case "Pending":
case "pending":
return "bg-amber-500/20 text-amber-300 border-amber-500/30";
default:
return "bg-slate-500/20 text-slate-300";
@ -105,14 +68,71 @@ const getStatusColor = (status: string) => {
};
export default function StaffPerformanceReviews() {
const { session } = useAuth();
const [reviews, setReviews] = useState<Review[]>([]);
const [stats, setStats] = useState<Stats>({ total: 0, pending: 0, completed: 0, average_rating: 0 });
const [selectedReview, setSelectedReview] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [commentDialog, setCommentDialog] = useState<Review | null>(null);
const [employeeComments, setEmployeeComments] = useState("");
const avgScore =
Math.round(
(performanceMetrics.reduce((sum, m) => sum + m.score, 0) /
performanceMetrics.length) *
10,
) / 10;
useEffect(() => {
if (session?.access_token) {
fetchReviews();
}
}, [session?.access_token]);
const fetchReviews = async () => {
try {
const res = await fetch("/api/staff/reviews", {
headers: { Authorization: `Bearer ${session?.access_token}` },
});
const data = await res.json();
if (res.ok) {
setReviews(data.reviews || []);
setStats(data.stats || { total: 0, pending: 0, completed: 0, average_rating: 0 });
}
} catch (err) {
aethexToast.error("Failed to load reviews");
} finally {
setLoading(false);
}
};
const submitComments = async () => {
if (!commentDialog) return;
try {
const res = await fetch("/api/staff/reviews", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${session?.access_token}`,
},
body: JSON.stringify({
review_id: commentDialog.id,
employee_comments: employeeComments,
}),
});
if (res.ok) {
aethexToast.success("Comments submitted");
setCommentDialog(null);
setEmployeeComments("");
fetchReviews();
}
} catch (err) {
aethexToast.error("Failed to submit comments");
}
};
if (loading) {
return (
<Layout>
<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-purple-400" />
</div>
</Layout>
);
}
return (
<Layout>
@ -151,20 +171,20 @@ export default function StaffPerformanceReviews() {
<div className="grid md:grid-cols-2 gap-8">
<div>
<p className="text-sm text-purple-200/70 mb-2">
Overall Rating
Average Rating
</p>
<p className="text-5xl font-bold text-purple-100 mb-4">
{avgScore}
{stats.average_rating.toFixed(1)}
</p>
<p className="text-slate-400">
Based on 5 performance dimensions
Based on {stats.completed} completed reviews
</p>
</div>
<div className="flex items-center justify-center">
<div className="text-center">
<Award className="h-16 w-16 text-purple-400 mx-auto mb-4" />
<p className="text-sm text-purple-200/70">
Exceeds Expectations
{stats.average_rating >= 4 ? "Exceeds Expectations" : stats.average_rating >= 3 ? "Meets Expectations" : "Needs Improvement"}
</p>
</div>
</div>
@ -172,39 +192,26 @@ export default function StaffPerformanceReviews() {
</CardContent>
</Card>
{/* Performance Metrics */}
<div className="mb-12">
<h2 className="text-2xl font-bold text-purple-100 mb-6">
Performance Dimensions
</h2>
<div className="space-y-4">
{performanceMetrics.map((metric) => (
<Card
key={metric.name}
className="bg-slate-800/50 border-slate-700/50"
>
{/* Stats */}
<div className="grid md:grid-cols-3 gap-4 mb-12">
<Card className="bg-purple-950/30 border-purple-500/30">
<CardContent className="pt-6">
<div className="flex items-center justify-between mb-3">
<div>
<p className="font-semibold text-purple-100">
{metric.name}
</p>
<p className="text-sm text-slate-400">
Last quarter: {metric.lastQuarter}
</p>
</div>
<p className="text-2xl font-bold text-purple-300">
{metric.score}
</p>
</div>
<Progress
value={(metric.score / 10) * 100}
className="h-2"
/>
<p className="text-2xl font-bold text-purple-100">{stats.total}</p>
<p className="text-sm text-purple-200/70">Total Reviews</p>
</CardContent>
</Card>
<Card className="bg-purple-950/30 border-purple-500/30">
<CardContent className="pt-6">
<p className="text-2xl font-bold text-purple-100">{stats.pending}</p>
<p className="text-sm text-purple-200/70">Pending</p>
</CardContent>
</Card>
<Card className="bg-purple-950/30 border-purple-500/30">
<CardContent className="pt-6">
<p className="text-2xl font-bold text-purple-100">{stats.completed}</p>
<p className="text-sm text-purple-200/70">Completed</p>
</CardContent>
</Card>
))}
</div>
</div>
{/* Review History */}
@ -213,7 +220,7 @@ export default function StaffPerformanceReviews() {
Review History
</h2>
<div className="space-y-4">
{userReviews.map((review) => (
{reviews.map((review) => (
<Card
key={review.id}
className="bg-slate-800/50 border-slate-700/50 hover:border-purple-500/50 transition-all cursor-pointer"
@ -230,65 +237,71 @@ export default function StaffPerformanceReviews() {
{review.period} Review
</CardTitle>
<CardDescription className="text-slate-400">
Due: {review.dueDate}
Due: {new Date(review.due_date).toLocaleDateString()}
{review.reviewer && ` • Reviewer: ${review.reviewer.full_name}`}
</CardDescription>
</div>
<Badge
className={`border ${getStatusColor(review.status)}`}
>
{review.status}
{review.status.replace("_", " ").replace(/\b\w/g, (l) => l.toUpperCase())}
</Badge>
</div>
</CardHeader>
{selectedReview === review.id && (
<CardContent className="space-y-4">
<div className="grid md:grid-cols-3 gap-4">
{review.selfAssessment && (
{review.overall_rating && (
<div className="flex items-center gap-3 p-3 bg-slate-700/30 rounded">
<Award className="h-5 w-5 text-purple-400" />
<div>
<p className="text-sm text-slate-300">Rating</p>
<p className="text-sm text-purple-300">
{review.overall_rating}/5
</p>
</div>
</div>
)}
{review.goals_total && (
<div className="flex items-center gap-3 p-3 bg-slate-700/30 rounded">
<CheckCircle className="h-5 w-5 text-purple-400" />
<div>
<p className="text-sm text-slate-300">Goals Met</p>
<p className="text-sm text-purple-300">
{review.goals_met}/{review.goals_total}
</p>
</div>
</div>
)}
<div className="flex items-center gap-3 p-3 bg-slate-700/30 rounded">
<MessageSquare className="h-5 w-5 text-purple-400" />
<div>
<p className="text-sm text-slate-300">
Self Assessment
</p>
<p className="text-sm text-slate-300">Your Comments</p>
<p className="text-sm text-purple-300">
Completed
{review.employee_comments ? "Submitted" : "Not submitted"}
</p>
</div>
</div>
</div>
{review.reviewer_comments && (
<div className="p-4 bg-slate-700/30 rounded">
<p className="text-sm text-slate-400 mb-2">Reviewer Comments:</p>
<p className="text-slate-200">{review.reviewer_comments}</p>
</div>
)}
{review.feedback && (
<div className="flex items-center gap-3 p-3 bg-slate-700/30 rounded">
<Users className="h-5 w-5 text-purple-400" />
<div>
<p className="text-sm text-slate-300">
360 Feedback
</p>
<p className="text-sm text-purple-300">
{review.feedback} responses
</p>
</div>
</div>
)}
{review.status === "Completed" && (
<div className="flex items-center gap-3 p-3 bg-slate-700/30 rounded">
<CheckCircle className="h-5 w-5 text-green-400" />
<div>
<p className="text-sm text-slate-300">
Manager Review
</p>
<p className="text-sm text-green-300">
Completed
</p>
</div>
</div>
)}
</div>
<div className="flex gap-2">
<Button
size="sm"
className="bg-purple-600 hover:bg-purple-700"
onClick={(e) => {
e.stopPropagation();
setCommentDialog(review);
setEmployeeComments(review.employee_comments || "");
}}
>
View Full Review
{review.employee_comments ? "Edit Comments" : "Add Comments"}
</Button>
</div>
</CardContent>
)}
</Card>
@ -296,39 +309,44 @@ export default function StaffPerformanceReviews() {
</div>
</div>
{/* Action Items */}
<Card className="bg-slate-800/50 border-purple-500/30">
<CardHeader>
<CardTitle className="text-purple-100">Next Steps</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-start gap-3">
<Clock className="h-5 w-5 text-purple-400 mt-0.5 flex-shrink-0" />
<div>
<p className="font-semibold text-purple-100">
Complete Q1 Self Assessment
</p>
<p className="text-sm text-slate-400">
Due by March 31, 2025
</p>
{reviews.length === 0 && (
<div className="text-center py-12">
<p className="text-slate-400">No reviews found</p>
</div>
</div>
<div className="flex items-start gap-3">
<MessageSquare className="h-5 w-5 text-purple-400 mt-0.5 flex-shrink-0" />
<div>
<p className="font-semibold text-purple-100">
Schedule 1:1 with Manager
</p>
<p className="text-sm text-slate-400">
Discuss Q1 progress and goals
</p>
</div>
</div>
</CardContent>
</Card>
)}
</div>
</div>
</div>
{/* Comment Dialog */}
<Dialog open={!!commentDialog} onOpenChange={() => setCommentDialog(null)}>
<DialogContent className="bg-slate-800 border-slate-700">
<DialogHeader>
<DialogTitle className="text-purple-100">
{commentDialog?.period} Review Comments
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<Textarea
placeholder="Add your comments about this review..."
value={employeeComments}
onChange={(e) => setEmployeeComments(e.target.value)}
className="bg-slate-700 border-slate-600 text-slate-100 min-h-[150px]"
/>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setCommentDialog(null)}>
Cancel
</Button>
<Button
className="bg-purple-600 hover:bg-purple-700"
onClick={submitComments}
>
Submit
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</Layout>
);
}

View file

@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import Layout from "@/components/Layout";
import SEO from "@/components/SEO";
import { Button } from "@/components/ui/button";
@ -12,110 +12,189 @@ import {
import { Badge } from "@/components/ui/badge";
import { Progress } from "@/components/ui/progress";
import {
BarChart,
Target,
TrendingUp,
Zap,
Users,
CheckCircle,
Loader2,
Plus,
Calendar,
} from "lucide-react";
import { useAuth } from "@/lib/auth";
import { aethexToast } from "@/components/ui/aethex-toast";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
interface OKR {
interface Task {
id: string;
title: string;
description: string;
owner: string;
progress: number;
status: "On Track" | "At Risk" | "Completed";
quarter: string;
team: string;
description?: string;
status: string;
priority: string;
due_date?: string;
completed_at?: string;
}
const okrs: OKR[] = [
{
id: "1",
title: "Improve Platform Performance by 40%",
description: "Reduce page load time and increase throughput",
owner: "Engineering",
progress: 75,
status: "On Track",
quarter: "Q1 2025",
team: "DevOps",
},
{
id: "2",
title: "Expand Creator Network to 5K Members",
description: "Grow creator base through partnerships and incentives",
owner: "Community",
progress: 62,
status: "On Track",
quarter: "Q1 2025",
team: "Growth",
},
{
id: "3",
title: "Launch New Learning Curriculum",
description: "Complete redesign of Foundation learning paths",
owner: "Foundation",
progress: 45,
status: "At Risk",
quarter: "Q1 2025",
team: "Education",
},
{
id: "4",
title: "Achieve 99.99% Uptime",
description: "Maintain service reliability and reduce downtime",
owner: "Infrastructure",
progress: 88,
status: "On Track",
quarter: "Q1 2025",
team: "Ops",
},
{
id: "5",
title: "Launch Roblox Game Studio Partnership",
description: "Formalize GameForge partnerships with major studios",
owner: "GameForge",
progress: 30,
status: "On Track",
quarter: "Q1 2025",
team: "Partnerships",
},
];
interface Project {
id: string;
name: string;
description: string;
status: string;
start_date: string;
end_date?: string;
lead?: {
full_name: string;
avatar_url?: string;
};
tasks: Task[];
task_stats: {
total: number;
done: number;
};
}
interface Stats {
total: number;
active: number;
completed: number;
}
const getStatusColor = (status: string) => {
switch (status) {
case "On Track":
case "active":
return "bg-green-500/20 text-green-300 border-green-500/30";
case "At Risk":
return "bg-amber-500/20 text-amber-300 border-amber-500/30";
case "Completed":
case "completed":
return "bg-blue-500/20 text-blue-300 border-blue-500/30";
case "on_hold":
return "bg-amber-500/20 text-amber-300 border-amber-500/30";
default:
return "bg-slate-500/20 text-slate-300 border-slate-500/30";
}
};
const getTaskStatusColor = (status: string) => {
switch (status) {
case "done":
return "bg-green-500/20 text-green-300";
case "in_progress":
return "bg-blue-500/20 text-blue-300";
case "todo":
return "bg-slate-500/20 text-slate-300";
default:
return "bg-slate-500/20 text-slate-300";
}
};
export default function StaffProjectTracking() {
const [selectedTeam, setSelectedTeam] = useState<string | null>(null);
const { session } = useAuth();
const [projects, setProjects] = useState<Project[]>([]);
const [stats, setStats] = useState<Stats>({ total: 0, active: 0, completed: 0 });
const [selectedProject, setSelectedProject] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [taskDialog, setTaskDialog] = useState<string | null>(null);
const [newTask, setNewTask] = useState({ title: "", description: "", priority: "medium", due_date: "" });
const teams = Array.from(new Set(okrs.map((okr) => okr.team)));
useEffect(() => {
if (session?.access_token) {
fetchProjects();
}
}, [session?.access_token]);
const filtered = selectedTeam
? okrs.filter((okr) => okr.team === selectedTeam)
: okrs;
const fetchProjects = async () => {
try {
const res = await fetch("/api/staff/projects", {
headers: { Authorization: `Bearer ${session?.access_token}` },
});
const data = await res.json();
if (res.ok) {
setProjects(data.projects || []);
setStats(data.stats || { total: 0, active: 0, completed: 0 });
}
} catch (err) {
aethexToast.error("Failed to load projects");
} finally {
setLoading(false);
}
};
const avgProgress =
Math.round(
filtered.reduce((sum, okr) => sum + okr.progress, 0) / filtered.length,
) || 0;
const updateTaskStatus = async (taskId: string, status: string) => {
try {
const res = await fetch("/api/staff/projects", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${session?.access_token}`,
},
body: JSON.stringify({ action: "update_task", task_id: taskId, status }),
});
if (res.ok) {
aethexToast.success("Task updated");
fetchProjects();
}
} catch (err) {
aethexToast.error("Failed to update task");
}
};
const createTask = async () => {
if (!taskDialog || !newTask.title) return;
try {
const res = await fetch("/api/staff/projects", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${session?.access_token}`,
},
body: JSON.stringify({
action: "create_task",
project_id: taskDialog,
...newTask,
}),
});
if (res.ok) {
aethexToast.success("Task created");
setTaskDialog(null);
setNewTask({ title: "", description: "", priority: "medium", due_date: "" });
fetchProjects();
}
} catch (err) {
aethexToast.error("Failed to create task");
}
};
const avgProgress = projects.length > 0
? Math.round(
projects.reduce((sum, p) => sum + (p.task_stats.total > 0 ? (p.task_stats.done / p.task_stats.total) * 100 : 0), 0) / projects.length
)
: 0;
if (loading) {
return (
<Layout>
<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-indigo-400" />
</div>
</Layout>
);
}
return (
<Layout>
<SEO
title="Project Tracking"
description="AeThex OKRs, initiatives, and roadmap"
description="AeThex projects, tasks, and roadmap"
/>
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900">
@ -137,7 +216,7 @@ export default function StaffProjectTracking() {
Project Tracking
</h1>
<p className="text-indigo-200/70">
OKRs, initiatives, and company-wide roadmap
Your projects, tasks, and progress
</p>
</div>
</div>
@ -148,9 +227,9 @@ export default function StaffProjectTracking() {
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-indigo-200/70">Active OKRs</p>
<p className="text-sm text-indigo-200/70">My Projects</p>
<p className="text-3xl font-bold text-indigo-100">
{filtered.length}
{stats.total}
</p>
</div>
<Target className="h-8 w-8 text-indigo-400" />
@ -174,9 +253,9 @@ export default function StaffProjectTracking() {
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-indigo-200/70">On Track</p>
<p className="text-sm text-indigo-200/70">Active</p>
<p className="text-3xl font-bold text-indigo-100">
{filtered.filter((o) => o.status === "On Track").length}
{stats.active}
</p>
</div>
<CheckCircle className="h-8 w-8 text-indigo-400" />
@ -185,93 +264,180 @@ export default function StaffProjectTracking() {
</Card>
</div>
{/* Team Filter */}
<div className="mb-8">
<p className="text-sm text-indigo-200/70 mb-3">Filter by Team:</p>
<div className="flex gap-2 flex-wrap">
<Button
variant={selectedTeam === null ? "default" : "outline"}
size="sm"
onClick={() => setSelectedTeam(null)}
className={
selectedTeam === null
? "bg-indigo-600 hover:bg-indigo-700"
: "border-indigo-500/30 text-indigo-300 hover:bg-indigo-500/10"
}
>
All Teams
</Button>
{teams.map((team) => (
<Button
key={team}
variant={selectedTeam === team ? "default" : "outline"}
size="sm"
onClick={() => setSelectedTeam(team)}
className={
selectedTeam === team
? "bg-indigo-600 hover:bg-indigo-700"
: "border-indigo-500/30 text-indigo-300 hover:bg-indigo-500/10"
}
>
{team}
</Button>
))}
</div>
</div>
{/* OKRs */}
{/* Projects */}
<div className="space-y-6">
{filtered.map((okr) => (
{projects.map((project) => (
<Card
key={okr.id}
key={project.id}
className="bg-slate-800/50 border-slate-700/50 hover:border-indigo-500/50 transition-all"
>
<CardHeader>
<CardHeader
className="cursor-pointer"
onClick={() => setSelectedProject(selectedProject === project.id ? null : project.id)}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<CardTitle className="text-indigo-100">
{okr.title}
{project.name}
</CardTitle>
<CardDescription className="text-slate-400">
{okr.description}
{project.description}
</CardDescription>
</div>
<Badge className={`border ${getStatusColor(okr.status)}`}>
{okr.status}
<Badge className={`border ${getStatusColor(project.status)}`}>
{project.status.replace("_", " ").replace(/\b\w/g, (l) => l.toUpperCase())}
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-slate-400">Progress</span>
<span className="text-slate-400">Tasks Progress</span>
<span className="text-indigo-300 font-semibold">
{okr.progress}%
{project.task_stats.done}/{project.task_stats.total}
</span>
</div>
<Progress value={okr.progress} className="h-2" />
<Progress
value={project.task_stats.total > 0 ? (project.task_stats.done / project.task_stats.total) * 100 : 0}
className="h-2"
/>
</div>
<div className="flex gap-4 flex-wrap">
<div className="flex gap-4 flex-wrap text-sm">
{project.lead && (
<div>
<p className="text-xs text-slate-500">Owner</p>
<p className="text-sm text-indigo-300">{okr.owner}</p>
<p className="text-xs text-slate-500">Lead</p>
<p className="text-indigo-300">{project.lead.full_name}</p>
</div>
)}
<div>
<p className="text-xs text-slate-500">Quarter</p>
<p className="text-sm text-indigo-300">{okr.quarter}</p>
<p className="text-xs text-slate-500">Start Date</p>
<p className="text-indigo-300">{new Date(project.start_date).toLocaleDateString()}</p>
</div>
{project.end_date && (
<div>
<p className="text-xs text-slate-500">Team</p>
<p className="text-sm text-indigo-300">{okr.team}</p>
<p className="text-xs text-slate-500">End Date</p>
<p className="text-indigo-300">{new Date(project.end_date).toLocaleDateString()}</p>
</div>
)}
</div>
{selectedProject === project.id && (
<div className="pt-4 border-t border-slate-700">
<div className="flex items-center justify-between mb-4">
<h4 className="text-lg font-semibold text-indigo-100">Tasks</h4>
<Button
size="sm"
className="bg-indigo-600 hover:bg-indigo-700"
onClick={() => setTaskDialog(project.id)}
>
<Plus className="h-4 w-4 mr-1" />
Add Task
</Button>
</div>
<div className="space-y-2">
{project.tasks.map((task) => (
<div
key={task.id}
className="flex items-center justify-between p-3 bg-slate-700/30 rounded"
>
<div className="flex-1">
<p className="text-slate-200">{task.title}</p>
{task.due_date && (
<p className="text-xs text-slate-500 flex items-center gap-1">
<Calendar className="h-3 w-3" />
Due: {new Date(task.due_date).toLocaleDateString()}
</p>
)}
</div>
<Select
value={task.status}
onValueChange={(value) => updateTaskStatus(task.id, value)}
>
<SelectTrigger className={`w-32 ${getTaskStatusColor(task.status)}`}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="todo">To Do</SelectItem>
<SelectItem value="in_progress">In Progress</SelectItem>
<SelectItem value="done">Done</SelectItem>
</SelectContent>
</Select>
</div>
))}
{project.tasks.length === 0 && (
<p className="text-slate-500 text-sm text-center py-4">No tasks yet</p>
)}
</div>
</div>
)}
</CardContent>
</Card>
))}
</div>
{projects.length === 0 && (
<div className="text-center py-12">
<p className="text-slate-400">No projects found</p>
</div>
)}
</div>
</div>
</div>
{/* Create Task Dialog */}
<Dialog open={!!taskDialog} onOpenChange={() => setTaskDialog(null)}>
<DialogContent className="bg-slate-800 border-slate-700">
<DialogHeader>
<DialogTitle className="text-indigo-100">Add New Task</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<Input
placeholder="Task title"
value={newTask.title}
onChange={(e) => setNewTask({ ...newTask, title: e.target.value })}
className="bg-slate-700 border-slate-600 text-slate-100"
/>
<Textarea
placeholder="Description (optional)"
value={newTask.description}
onChange={(e) => setNewTask({ ...newTask, description: e.target.value })}
className="bg-slate-700 border-slate-600 text-slate-100"
/>
<div className="grid grid-cols-2 gap-4">
<Select
value={newTask.priority}
onValueChange={(value) => setNewTask({ ...newTask, priority: value })}
>
<SelectTrigger className="bg-slate-700 border-slate-600 text-slate-100">
<SelectValue placeholder="Priority" />
</SelectTrigger>
<SelectContent>
<SelectItem value="low">Low</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="high">High</SelectItem>
</SelectContent>
</Select>
<Input
type="date"
value={newTask.due_date}
onChange={(e) => setNewTask({ ...newTask, due_date: e.target.value })}
className="bg-slate-700 border-slate-600 text-slate-100"
/>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setTaskDialog(null)}>
Cancel
</Button>
<Button
className="bg-indigo-600 hover:bg-indigo-700"
onClick={createTask}
>
Create Task
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</Layout>
);
}

View file

@ -1,3 +1,4 @@
import { useState, useEffect } from "react";
import Layout from "@/components/Layout";
import SEO from "@/components/SEO";
import { Button } from "@/components/ui/button";
@ -11,94 +12,88 @@ import {
import { Badge } from "@/components/ui/badge";
import {
Heart,
DollarSign,
Calendar,
MapPin,
Users,
Shield,
Zap,
Award,
Loader2,
ChevronDown,
ChevronUp,
} from "lucide-react";
import { useAuth } from "@/lib/auth";
import { aethexToast } from "@/components/ui/aethex-toast";
interface HandbookSection {
id: string;
category: string;
title: string;
icon: React.ReactNode;
content: string;
subsections: string[];
order_index: number;
}
const sections: HandbookSection[] = [
{
id: "1",
title: "Benefits & Compensation",
icon: <Heart className="h-6 w-6" />,
content: "Comprehensive benefits package including health, dental, vision",
subsections: [
"Health Insurance",
"Retirement Plans",
"Stock Options",
"Flexible PTO",
],
},
{
id: "2",
title: "Company Policies",
icon: <Shield className="h-6 w-6" />,
content: "Core policies governing workplace conduct and expectations",
subsections: [
"Code of Conduct",
"Harassment Policy",
"Confidentiality",
"Data Security",
],
},
{
id: "3",
title: "Time Off & Leave",
icon: <Calendar className="h-6 w-6" />,
content: "Vacation, sick leave, parental leave, and special circumstances",
subsections: [
"Paid Time Off",
"Sick Leave",
"Parental Leave",
"Sabbatical",
],
},
{
id: "4",
title: "Remote Work & Flexibility",
icon: <MapPin className="h-6 w-6" />,
content: "Work from home policies, office hours, and location flexibility",
subsections: ["WFH Policy", "Core Hours", "Office Access", "Equipment"],
},
{
id: "5",
title: "Professional Development",
icon: <Zap className="h-6 w-6" />,
content: "Learning opportunities, training budgets, and career growth",
subsections: [
"Training Budget",
"Conference Attendance",
"Internal Training",
"Mentorship",
],
},
{
id: "6",
title: "Recognition & Awards",
icon: <Award className="h-6 w-6" />,
content: "Employee recognition programs and performance incentives",
subsections: [
"Spot Bonuses",
"Team Awards",
"Anniversary Recognition",
"Excellence Awards",
],
},
];
const getCategoryIcon = (category: string) => {
switch (category) {
case "Benefits":
return <Heart className="h-6 w-6" />;
case "Policies":
return <Shield className="h-6 w-6" />;
case "Time Off":
return <Calendar className="h-6 w-6" />;
case "Remote Work":
return <MapPin className="h-6 w-6" />;
case "Development":
return <Zap className="h-6 w-6" />;
case "Recognition":
return <Award className="h-6 w-6" />;
default:
return <Users className="h-6 w-6" />;
}
};
export default function StaffTeamHandbook() {
const { session } = useAuth();
const [sections, setSections] = useState<HandbookSection[]>([]);
const [grouped, setGrouped] = useState<Record<string, HandbookSection[]>>({});
const [categories, setCategories] = useState<string[]>([]);
const [expandedCategory, setExpandedCategory] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (session?.access_token) {
fetchHandbook();
}
}, [session?.access_token]);
const fetchHandbook = async () => {
try {
const res = await fetch("/api/staff/handbook", {
headers: { Authorization: `Bearer ${session?.access_token}` },
});
const data = await res.json();
if (res.ok) {
setSections(data.sections || []);
setGrouped(data.grouped || {});
setCategories(data.categories || []);
}
} catch (err) {
aethexToast.error("Failed to load handbook");
} finally {
setLoading(false);
}
};
if (loading) {
return (
<Layout>
<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-blue-400" />
</div>
</Layout>
);
}
return (
<Layout>
<SEO
@ -158,50 +153,67 @@ export default function StaffTeamHandbook() {
</Card>
</div>
{/* Handbook Sections */}
{/* Handbook Sections by Category */}
<div className="space-y-6">
{sections.map((section) => (
{categories.map((category) => (
<Card
key={section.id}
key={category}
className="bg-slate-800/50 border-slate-700/50 hover:border-blue-500/50 transition-all"
>
<CardHeader>
<div className="flex items-start justify-between">
<CardHeader
className="cursor-pointer"
onClick={() => setExpandedCategory(expandedCategory === category ? null : category)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-blue-500/20 text-blue-400">
{section.icon}
{getCategoryIcon(category)}
</div>
<div>
<CardTitle className="text-blue-100">
{section.title}
{category}
</CardTitle>
<CardDescription className="text-slate-400">
{section.content}
{grouped[category]?.length || 0} sections
</CardDescription>
</div>
</div>
{expandedCategory === category ? (
<ChevronUp className="h-5 w-5 text-blue-400" />
) : (
<ChevronDown className="h-5 w-5 text-blue-400" />
)}
</div>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2 mb-4">
{section.subsections.map((subsection) => (
<Badge
key={subsection}
variant="secondary"
className="bg-slate-700/50 text-slate-300"
{expandedCategory === category && (
<CardContent className="pt-0">
<div className="space-y-4 pl-14">
{grouped[category]?.map((section) => (
<div
key={section.id}
className="p-4 bg-slate-700/30 rounded-lg"
>
{subsection}
</Badge>
<h4 className="font-semibold text-blue-100 mb-2">
{section.title}
</h4>
<p className="text-slate-300 text-sm whitespace-pre-line">
{section.content}
</p>
</div>
))}
</div>
<Button size="sm" className="bg-blue-600 hover:bg-blue-700">
View Details
</Button>
</CardContent>
)}
</Card>
))}
</div>
{categories.length === 0 && (
<div className="text-center py-12">
<p className="text-slate-400">No handbook sections found</p>
</div>
)}
{/* Additional Resources */}
<div className="mt-12 p-6 rounded-lg bg-slate-800/50 border border-blue-500/30">
<h2 className="text-xl font-bold text-blue-100 mb-4">