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 Layout from "@/components/Layout";
import SEO from "@/components/SEO"; import SEO from "@/components/SEO";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -13,127 +13,153 @@ import { Badge } from "@/components/ui/badge";
import { import {
ShoppingCart, ShoppingCart,
Search, Search,
Users,
Clock, Clock,
AlertCircle, Gift,
CheckCircle, Star,
Package,
Loader2,
Coins,
} from "lucide-react"; } from "lucide-react";
import { Input } from "@/components/ui/input"; 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; id: string;
name: string; name: string;
provider: string;
category: string;
description: string; description: string;
availability: "Available" | "Booked" | "Coming Soon"; category: string;
turnaround: string; points_cost: number;
requests: number; image_url?: string;
stock_count?: number;
is_available: boolean;
} }
const services: Service[] = [ interface Order {
{ id: string;
id: "1", quantity: number;
name: "Design Consultation", status: string;
provider: "Design Team", created_at: string;
category: "Design", item?: {
description: "1-on-1 design review and UX guidance for your project", name: string;
availability: "Available", image_url?: string;
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,
},
];
const getAvailabilityColor = (availability: string) => { interface Points {
switch (availability) { balance: number;
case "Available": lifetime_earned: number;
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";
default:
return "bg-slate-500/20 text-slate-300";
}
};
export default function StaffInternalMarketplace() { 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 [searchQuery, setSearchQuery] = useState("");
const [selectedCategory, setSelectedCategory] = useState("All"); const [selectedCategory, setSelectedCategory] = useState("All");
const [loading, setLoading] = useState(true);
const [orderDialog, setOrderDialog] = useState<MarketplaceItem | null>(null);
const [shippingAddress, setShippingAddress] = useState("");
const categories = [ useEffect(() => {
"All", if (session?.access_token) {
"Design", fetchMarketplace();
"Development", }
"Security", }, [session?.access_token]);
"Infrastructure",
"Product",
];
const filtered = services.filter((service) => { 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 = const matchesSearch =
service.name.toLowerCase().includes(searchQuery.toLowerCase()) || item.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
service.provider.toLowerCase().includes(searchQuery.toLowerCase()); item.description.toLowerCase().includes(searchQuery.toLowerCase());
const matchesCategory = const matchesCategory =
selectedCategory === "All" || service.category === selectedCategory; selectedCategory === "All" || item.category === selectedCategory;
return matchesSearch && matchesCategory; 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";
}
};
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 ( return (
<Layout> <Layout>
<SEO <SEO
title="Internal Marketplace" title="Points Marketplace"
description="Request services from other teams" description="Redeem your points for rewards"
/> />
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900"> <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="container mx-auto max-w-6xl px-4 py-16">
<div className="flex items-center gap-3 mb-6"> <div className="flex items-center gap-3 mb-6">
<div className="p-3 rounded-lg bg-amber-500/20 border border-amber-500/30"> <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>
<div> <div>
<h1 className="text-4xl font-bold text-amber-100"> <h1 className="text-4xl font-bold text-amber-100">
Internal Marketplace Points Marketplace
</h1> </h1>
<p className="text-amber-200/70"> <p className="text-amber-200/70">
Request services and resources from other teams Redeem your earned points for rewards
</p> </p>
</div> </div>
</div> </div>
{/* Summary */} {/* Points Summary */}
<div className="grid md:grid-cols-3 gap-4 mb-12"> <div className="grid md:grid-cols-3 gap-4 mb-12">
<Card className="bg-amber-950/30 border-amber-500/30"> <Card className="bg-amber-950/30 border-amber-500/30">
<CardContent className="pt-6"> <CardContent className="pt-6">
<p className="text-2xl font-bold text-amber-100"> <div className="flex items-center justify-between">
{services.length} <div>
</p> <p className="text-sm text-amber-200/70">Your Balance</p>
<p className="text-sm text-amber-200/70"> <p className="text-3xl font-bold text-amber-100">
Available Services {points.balance.toLocaleString()}
</p> </p>
</div>
<Coins className="h-8 w-8 text-amber-400" />
</div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-amber-950/30 border-amber-500/30"> <Card className="bg-amber-950/30 border-amber-500/30">
<CardContent className="pt-6"> <CardContent className="pt-6">
<p className="text-2xl font-bold text-amber-100"> <div className="flex items-center justify-between">
{ <div>
services.filter((s) => s.availability === "Available") <p className="text-sm text-amber-200/70">Lifetime Earned</p>
.length <p className="text-3xl font-bold text-amber-100">
} {points.lifetime_earned.toLocaleString()}
</p> </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> </CardContent>
</Card> </Card>
<Card className="bg-amber-950/30 border-amber-500/30"> <Card className="bg-amber-950/30 border-amber-500/30">
<CardContent className="pt-6"> <CardContent className="pt-6">
<p className="text-2xl font-bold text-amber-100"> <div className="flex items-center justify-between">
{services.reduce((sum, s) => sum + s.requests, 0)} <div>
</p> <p className="text-sm text-amber-200/70">My Orders</p>
<p className="text-sm text-amber-200/70">Total Requests</p> <p className="text-3xl font-bold text-amber-100">
{orders.length}
</p>
</div>
<Package className="h-8 w-8 text-amber-400" />
</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@ -198,7 +234,7 @@ export default function StaffInternalMarketplace() {
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-3 h-5 w-5 text-slate-400" /> <Search className="absolute left-3 top-3 h-5 w-5 text-slate-400" />
<Input <Input
placeholder="Search services..." placeholder="Search rewards..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 bg-slate-800 border-slate-700 text-slate-100" className="pl-10 bg-slate-800 border-slate-700 text-slate-100"
@ -225,66 +261,134 @@ export default function StaffInternalMarketplace() {
</div> </div>
</div> </div>
{/* Services Grid */} {/* Items Grid */}
<div className="grid md:grid-cols-2 gap-6"> <div className="grid md:grid-cols-3 gap-6 mb-12">
{filtered.map((service) => ( {filtered.map((item) => (
<Card <Card
key={service.id} key={item.id}
className="bg-slate-800/50 border-slate-700/50 hover:border-amber-500/50 transition-all" className="bg-slate-800/50 border-slate-700/50 hover:border-amber-500/50 transition-all"
> >
<CardHeader> <CardHeader>
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div> <div>
<CardTitle className="text-amber-100"> <CardTitle className="text-amber-100">
{service.name} {item.name}
</CardTitle> </CardTitle>
<CardDescription className="text-slate-400"> <CardDescription className="text-slate-400">
{service.provider} {item.category}
</CardDescription> </CardDescription>
</div> </div>
<Badge {item.stock_count !== null && item.stock_count < 10 && (
className={`border ${getAvailabilityColor(service.availability)}`} <Badge className="bg-red-500/20 text-red-300 border-red-500/30">
> Only {item.stock_count} left
{service.availability} </Badge>
</Badge> )}
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<p className="text-sm text-slate-300"> <p className="text-sm text-slate-300">{item.description}</p>
{service.description} <div className="flex items-center justify-between">
</p> <div className="flex items-center gap-1 text-amber-400 font-semibold">
<div className="flex gap-4 text-sm"> <Coins className="h-4 w-4" />
<div className="flex items-center gap-2 text-slate-400"> {item.points_cost.toLocaleString()} pts
<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> </div>
<Button
size="sm"
className="bg-amber-600 hover:bg-amber-700"
disabled={points.balance < item.points_cost}
onClick={() => setOrderDialog(item)}
>
Redeem
</Button>
</div> </div>
<Button
size="sm"
className="w-full bg-amber-600 hover:bg-amber-700"
disabled={service.availability === "Coming Soon"}
>
{service.availability === "Coming Soon"
? "Coming Soon"
: "Request Service"}
</Button>
</CardContent> </CardContent>
</Card> </Card>
))} ))}
</div> </div>
{filtered.length === 0 && ( {filtered.length === 0 && (
<div className="text-center py-12"> <div className="text-center py-12 mb-12">
<p className="text-slate-400">No services found</p> <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>
</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> </Layout>
); );
} }

View file

@ -1,4 +1,4 @@
import { useState } from "react"; import { useState, useEffect } from "react";
import Layout from "@/components/Layout"; import Layout from "@/components/Layout";
import SEO from "@/components/SEO"; import SEO from "@/components/SEO";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -20,105 +20,133 @@ import {
Users, Users,
Settings, Settings,
Code, Code,
Loader2,
ThumbsUp,
Eye,
} from "lucide-react"; } from "lucide-react";
import { useAuth } from "@/lib/auth";
import { aethexToast } from "@/components/ui/aethex-toast";
interface KnowledgeArticle { interface KnowledgeArticle {
id: string; id: string;
title: string; title: string;
category: string; category: string;
description: string; content: string;
tags: string[]; tags: string[];
views: number; views: number;
updated: string; helpful_count: number;
icon: React.ReactNode; updated_at: string;
author?: {
full_name: string;
avatar_url?: string;
};
} }
const articles: KnowledgeArticle[] = [ const getCategoryIcon = (category: string) => {
{ switch (category) {
id: "1", case "Onboarding":
title: "Getting Started with AeThex Platform", return <Zap className="h-5 w-5" />;
category: "Onboarding", case "Support":
description: "Complete guide for new team members to get up to speed", return <AlertCircle className="h-5 w-5" />;
tags: ["onboarding", "setup", "beginner"], case "Development":
views: 324, return <Code className="h-5 w-5" />;
updated: "2 days ago", case "Process":
icon: <Zap className="h-5 w-5" />, return <Users className="h-5 w-5" />;
}, case "Security":
{ return <Settings className="h-5 w-5" />;
id: "2", default:
title: "Troubleshooting Common Issues", return <FileText className="h-5 w-5" />;
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",
];
export default function StaffKnowledgeBase() { export default function StaffKnowledgeBase() {
const { session } = useAuth();
const [articles, setArticles] = useState<KnowledgeArticle[]>([]);
const [categories, setCategories] = useState<string[]>([]);
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [selectedCategory, setSelectedCategory] = useState("All"); const [selectedCategory, setSelectedCategory] = useState("all");
const [loading, setLoading] = useState(true);
const filtered = articles.filter((article) => { useEffect(() => {
const matchesSearch = if (session?.access_token) {
article.title.toLowerCase().includes(searchQuery.toLowerCase()) || fetchArticles();
article.description.toLowerCase().includes(searchQuery.toLowerCase()); }
const matchesCategory = }, [session?.access_token, selectedCategory, searchQuery]);
selectedCategory === "All" || article.category === selectedCategory;
return matchesSearch && matchesCategory; 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 ( return (
<Layout> <Layout>
@ -163,12 +191,22 @@ export default function StaffKnowledgeBase() {
{/* Category Filter */} {/* Category Filter */}
<div className="flex gap-2 mb-8 flex-wrap"> <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) => ( {categories.map((category) => (
<Button <Button
key={category} key={category}
variant={ variant={selectedCategory === category ? "default" : "outline"}
selectedCategory === category ? "default" : "outline"
}
size="sm" size="sm"
onClick={() => setSelectedCategory(category)} onClick={() => setSelectedCategory(category)}
className={ className={
@ -184,15 +222,16 @@ export default function StaffKnowledgeBase() {
{/* Articles Grid */} {/* Articles Grid */}
<div className="grid md:grid-cols-2 gap-6"> <div className="grid md:grid-cols-2 gap-6">
{filtered.map((article) => ( {articles.map((article) => (
<Card <Card
key={article.id} key={article.id}
className="bg-slate-800/50 border-slate-700/50 hover:border-purple-500/50 transition-all cursor-pointer group" className="bg-slate-800/50 border-slate-700/50 hover:border-purple-500/50 transition-all cursor-pointer group"
onClick={() => trackView(article.id)}
> >
<CardHeader> <CardHeader>
<div className="flex items-start justify-between mb-2"> <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"> <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> </div>
<Badge className="bg-slate-700 text-slate-300 text-xs"> <Badge className="bg-slate-700 text-slate-300 text-xs">
{article.category} {article.category}
@ -202,32 +241,47 @@ export default function StaffKnowledgeBase() {
{article.title} {article.title}
</CardTitle> </CardTitle>
<CardDescription className="text-slate-400"> <CardDescription className="text-slate-400">
{article.description} {article.content.substring(0, 150)}...
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex gap-2 flex-wrap"> {article.tags && article.tags.length > 0 && (
{article.tags.map((tag) => ( <div className="flex gap-2 flex-wrap">
<Badge {article.tags.map((tag) => (
key={tag} <Badge
variant="secondary" key={tag}
className="bg-slate-700/50 text-slate-300 text-xs" variant="secondary"
> className="bg-slate-700/50 text-slate-300 text-xs"
{tag} >
</Badge> {tag}
))} </Badge>
</div> ))}
</div>
)}
<div className="flex items-center justify-between pt-4 border-t border-slate-700"> <div className="flex items-center justify-between pt-4 border-t border-slate-700">
<span className="text-xs text-slate-500"> <div className="flex items-center gap-4 text-xs text-slate-500">
{article.views} views {article.updated} <span className="flex items-center gap-1">
</span> <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 <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
className="text-purple-400 hover:text-purple-300 hover:bg-purple-500/20" 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> </Button>
</div> </div>
</div> </div>
@ -236,7 +290,7 @@ export default function StaffKnowledgeBase() {
))} ))}
</div> </div>
{filtered.length === 0 && ( {articles.length === 0 && (
<div className="text-center py-12"> <div className="text-center py-12">
<p className="text-slate-400">No articles found</p> <p className="text-slate-400">No articles found</p>
</div> </div>

View file

@ -1,4 +1,4 @@
import { useState } from "react"; import { useState, useEffect } from "react";
import Layout from "@/components/Layout"; import Layout from "@/components/Layout";
import SEO from "@/components/SEO"; import SEO from "@/components/SEO";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -19,109 +19,112 @@ import {
FileText, FileText,
Clock, Clock,
CheckCircle, CheckCircle,
Loader2,
} from "lucide-react"; } from "lucide-react";
import { useAuth } from "@/lib/auth";
import { aethexToast } from "@/components/ui/aethex-toast";
interface Course { interface Course {
id: string; id: string;
title: string; title: string;
instructor: string; description: string;
category: string; category: string;
duration: string; duration_weeks: number;
lesson_count: number;
is_required: boolean;
progress: number; progress: number;
status: "In Progress" | "Completed" | "Available"; status: string;
lessons: number; started_at?: string;
icon: React.ReactNode; completed_at?: string;
} }
const courses: Course[] = [ interface Stats {
{ total: number;
id: "1", completed: number;
title: "Advanced TypeScript Patterns", in_progress: number;
instructor: "Sarah Chen", required: number;
category: "Development", }
duration: "4 weeks",
progress: 65, const getCourseIcon = (category: string) => {
status: "In Progress", switch (category) {
lessons: 12, case "Development":
icon: <BookOpen className="h-5 w-5" />, return <BookOpen className="h-5 w-5" />;
}, case "Leadership":
{ return <Award className="h-5 w-5" />;
id: "2", case "Infrastructure":
title: "Leadership Fundamentals", return <Zap className="h-5 w-5" />;
instructor: "Marcus Johnson", case "Product":
category: "Leadership", return <Video className="h-5 w-5" />;
duration: "6 weeks", default:
progress: 0, return <FileText className="h-5 w-5" />;
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" />,
},
];
export default function StaffLearningPortal() { 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 [selectedCategory, setSelectedCategory] = useState("All");
const [loading, setLoading] = useState(true);
const categories = [ useEffect(() => {
"All", if (session?.access_token) {
"Development", fetchCourses();
"Leadership", }
"Infrastructure", }, [session?.access_token]);
"Product",
"Security", const fetchCourses = async () => {
"Skills", 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 = const filtered =
selectedCategory === "All" selectedCategory === "All"
? courses ? courses
: courses.filter((c) => c.category === selectedCategory); : courses.filter((c) => c.category === selectedCategory);
const completed = courses.filter((c) => c.status === "Completed").length; if (loading) {
const inProgress = courses.filter((c) => c.status === "In Progress").length; 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 ( return (
<Layout> <Layout>
@ -159,7 +162,7 @@ export default function StaffLearningPortal() {
<Card className="bg-cyan-950/30 border-cyan-500/30"> <Card className="bg-cyan-950/30 border-cyan-500/30">
<CardContent className="pt-6"> <CardContent className="pt-6">
<p className="text-2xl font-bold text-cyan-100"> <p className="text-2xl font-bold text-cyan-100">
{courses.length} {stats.total}
</p> </p>
<p className="text-sm text-cyan-200/70">Total Courses</p> <p className="text-sm text-cyan-200/70">Total Courses</p>
</CardContent> </CardContent>
@ -167,7 +170,7 @@ export default function StaffLearningPortal() {
<Card className="bg-cyan-950/30 border-cyan-500/30"> <Card className="bg-cyan-950/30 border-cyan-500/30">
<CardContent className="pt-6"> <CardContent className="pt-6">
<p className="text-2xl font-bold text-cyan-100"> <p className="text-2xl font-bold text-cyan-100">
{completed} {stats.completed}
</p> </p>
<p className="text-sm text-cyan-200/70">Completed</p> <p className="text-sm text-cyan-200/70">Completed</p>
</CardContent> </CardContent>
@ -175,7 +178,7 @@ export default function StaffLearningPortal() {
<Card className="bg-cyan-950/30 border-cyan-500/30"> <Card className="bg-cyan-950/30 border-cyan-500/30">
<CardContent className="pt-6"> <CardContent className="pt-6">
<p className="text-2xl font-bold text-cyan-100"> <p className="text-2xl font-bold text-cyan-100">
{inProgress} {stats.in_progress}
</p> </p>
<p className="text-sm text-cyan-200/70">In Progress</p> <p className="text-sm text-cyan-200/70">In Progress</p>
</CardContent> </CardContent>
@ -183,7 +186,7 @@ export default function StaffLearningPortal() {
<Card className="bg-cyan-950/30 border-cyan-500/30"> <Card className="bg-cyan-950/30 border-cyan-500/30">
<CardContent className="pt-6"> <CardContent className="pt-6">
<p className="text-2xl font-bold text-cyan-100"> <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>
<p className="text-sm text-cyan-200/70">Completion Rate</p> <p className="text-sm text-cyan-200/70">Completion Rate</p>
</CardContent> </CardContent>
@ -223,25 +226,25 @@ export default function StaffLearningPortal() {
<CardHeader> <CardHeader>
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="p-2 rounded bg-cyan-500/20 text-cyan-400"> <div className="p-2 rounded bg-cyan-500/20 text-cyan-400">
{course.icon} {getCourseIcon(course.category)}
</div> </div>
<Badge <Badge
className={ className={
course.status === "Completed" course.status === "completed"
? "bg-green-500/20 text-green-300 border-green-500/30" ? "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-blue-500/20 text-blue-300 border-blue-500/30"
: "bg-slate-700 text-slate-300" : "bg-slate-700 text-slate-300"
} }
> >
{course.status} {course.status === "completed" ? "Completed" : course.status === "in_progress" ? "In Progress" : "Available"}
</Badge> </Badge>
</div> </div>
<CardTitle className="text-cyan-100"> <CardTitle className="text-cyan-100">
{course.title} {course.title}
</CardTitle> </CardTitle>
<CardDescription className="text-slate-400"> <CardDescription className="text-slate-400">
by {course.instructor} {course.description}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
@ -259,20 +262,26 @@ export default function StaffLearningPortal() {
<div className="flex gap-4 text-sm"> <div className="flex gap-4 text-sm">
<div className="flex items-center gap-2 text-slate-400"> <div className="flex items-center gap-2 text-slate-400">
<Clock className="h-4 w-4" /> <Clock className="h-4 w-4" />
{course.duration} {course.duration_weeks} weeks
</div> </div>
<div className="flex items-center gap-2 text-slate-400"> <div className="flex items-center gap-2 text-slate-400">
<FileText className="h-4 w-4" /> <FileText className="h-4 w-4" />
{course.lessons} lessons {course.lesson_count} lessons
</div> </div>
{course.is_required && (
<Badge className="bg-amber-500/20 text-amber-300 border-amber-500/30">
Required
</Badge>
)}
</div> </div>
<Button <Button
size="sm" size="sm"
className="w-full bg-cyan-600 hover:bg-cyan-700" 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" ? "Review Course"
: course.status === "In Progress" : course.status === "in_progress"
? "Continue" ? "Continue"
: "Enroll"} : "Enroll"}
</Button> </Button>
@ -280,6 +289,12 @@ export default function StaffLearningPortal() {
</Card> </Card>
))} ))}
</div> </div>
{filtered.length === 0 && (
<div className="text-center py-12">
<p className="text-slate-400">No courses found</p>
</div>
)}
</div> </div>
</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 Layout from "@/components/Layout";
import SEO from "@/components/SEO"; import SEO from "@/components/SEO";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -18,86 +18,49 @@ import {
Clock, Clock,
Award, Award,
Users, Users,
Loader2,
} from "lucide-react"; } 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 { interface Review {
id: string; id: string;
period: string; period: string;
status: "Pending" | "In Progress" | "Completed"; status: string;
reviewer?: string; overall_rating?: number;
dueDate: string; reviewer_comments?: string;
feedback?: number; employee_comments?: string;
selfAssessment?: boolean; goals_met?: number;
goals_total?: number;
due_date: string;
created_at: string;
reviewer?: {
full_name: string;
avatar_url?: string;
};
} }
interface Metric { interface Stats {
name: string; total: number;
score: number; pending: number;
lastQuarter: 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) => { const getStatusColor = (status: string) => {
switch (status) { switch (status) {
case "Completed": case "completed":
return "bg-green-500/20 text-green-300 border-green-500/30"; 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"; 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"; return "bg-amber-500/20 text-amber-300 border-amber-500/30";
default: default:
return "bg-slate-500/20 text-slate-300"; return "bg-slate-500/20 text-slate-300";
@ -105,14 +68,71 @@ const getStatusColor = (status: string) => {
}; };
export default function StaffPerformanceReviews() { 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 [selectedReview, setSelectedReview] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [commentDialog, setCommentDialog] = useState<Review | null>(null);
const [employeeComments, setEmployeeComments] = useState("");
const avgScore = useEffect(() => {
Math.round( if (session?.access_token) {
(performanceMetrics.reduce((sum, m) => sum + m.score, 0) / fetchReviews();
performanceMetrics.length) * }
10, }, [session?.access_token]);
) / 10;
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 ( return (
<Layout> <Layout>
@ -151,20 +171,20 @@ export default function StaffPerformanceReviews() {
<div className="grid md:grid-cols-2 gap-8"> <div className="grid md:grid-cols-2 gap-8">
<div> <div>
<p className="text-sm text-purple-200/70 mb-2"> <p className="text-sm text-purple-200/70 mb-2">
Overall Rating Average Rating
</p> </p>
<p className="text-5xl font-bold text-purple-100 mb-4"> <p className="text-5xl font-bold text-purple-100 mb-4">
{avgScore} {stats.average_rating.toFixed(1)}
</p> </p>
<p className="text-slate-400"> <p className="text-slate-400">
Based on 5 performance dimensions Based on {stats.completed} completed reviews
</p> </p>
</div> </div>
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
<div className="text-center"> <div className="text-center">
<Award className="h-16 w-16 text-purple-400 mx-auto mb-4" /> <Award className="h-16 w-16 text-purple-400 mx-auto mb-4" />
<p className="text-sm text-purple-200/70"> <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> </p>
</div> </div>
</div> </div>
@ -172,39 +192,26 @@ export default function StaffPerformanceReviews() {
</CardContent> </CardContent>
</Card> </Card>
{/* Performance Metrics */} {/* Stats */}
<div className="mb-12"> <div className="grid md:grid-cols-3 gap-4 mb-12">
<h2 className="text-2xl font-bold text-purple-100 mb-6"> <Card className="bg-purple-950/30 border-purple-500/30">
Performance Dimensions <CardContent className="pt-6">
</h2> <p className="text-2xl font-bold text-purple-100">{stats.total}</p>
<div className="space-y-4"> <p className="text-sm text-purple-200/70">Total Reviews</p>
{performanceMetrics.map((metric) => ( </CardContent>
<Card </Card>
key={metric.name} <Card className="bg-purple-950/30 border-purple-500/30">
className="bg-slate-800/50 border-slate-700/50" <CardContent className="pt-6">
> <p className="text-2xl font-bold text-purple-100">{stats.pending}</p>
<CardContent className="pt-6"> <p className="text-sm text-purple-200/70">Pending</p>
<div className="flex items-center justify-between mb-3"> </CardContent>
<div> </Card>
<p className="font-semibold text-purple-100"> <Card className="bg-purple-950/30 border-purple-500/30">
{metric.name} <CardContent className="pt-6">
</p> <p className="text-2xl font-bold text-purple-100">{stats.completed}</p>
<p className="text-sm text-slate-400"> <p className="text-sm text-purple-200/70">Completed</p>
Last quarter: {metric.lastQuarter} </CardContent>
</p> </Card>
</div>
<p className="text-2xl font-bold text-purple-300">
{metric.score}
</p>
</div>
<Progress
value={(metric.score / 10) * 100}
className="h-2"
/>
</CardContent>
</Card>
))}
</div>
</div> </div>
{/* Review History */} {/* Review History */}
@ -213,7 +220,7 @@ export default function StaffPerformanceReviews() {
Review History Review History
</h2> </h2>
<div className="space-y-4"> <div className="space-y-4">
{userReviews.map((review) => ( {reviews.map((review) => (
<Card <Card
key={review.id} key={review.id}
className="bg-slate-800/50 border-slate-700/50 hover:border-purple-500/50 transition-all cursor-pointer" 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 {review.period} Review
</CardTitle> </CardTitle>
<CardDescription className="text-slate-400"> <CardDescription className="text-slate-400">
Due: {review.dueDate} Due: {new Date(review.due_date).toLocaleDateString()}
{review.reviewer && ` • Reviewer: ${review.reviewer.full_name}`}
</CardDescription> </CardDescription>
</div> </div>
<Badge <Badge
className={`border ${getStatusColor(review.status)}`} className={`border ${getStatusColor(review.status)}`}
> >
{review.status} {review.status.replace("_", " ").replace(/\b\w/g, (l) => l.toUpperCase())}
</Badge> </Badge>
</div> </div>
</CardHeader> </CardHeader>
{selectedReview === review.id && ( {selectedReview === review.id && (
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="grid md:grid-cols-3 gap-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"> <div className="flex items-center gap-3 p-3 bg-slate-700/30 rounded">
<MessageSquare className="h-5 w-5 text-purple-400" /> <Award className="h-5 w-5 text-purple-400" />
<div> <div>
<p className="text-sm text-slate-300"> <p className="text-sm text-slate-300">Rating</p>
Self Assessment
</p>
<p className="text-sm text-purple-300"> <p className="text-sm text-purple-300">
Completed {review.overall_rating}/5
</p> </p>
</div> </div>
</div> </div>
)} )}
{review.feedback && ( {review.goals_total && (
<div className="flex items-center gap-3 p-3 bg-slate-700/30 rounded"> <div className="flex items-center gap-3 p-3 bg-slate-700/30 rounded">
<Users className="h-5 w-5 text-purple-400" /> <CheckCircle className="h-5 w-5 text-purple-400" />
<div> <div>
<p className="text-sm text-slate-300"> <p className="text-sm text-slate-300">Goals Met</p>
360 Feedback
</p>
<p className="text-sm text-purple-300"> <p className="text-sm text-purple-300">
{review.feedback} responses {review.goals_met}/{review.goals_total}
</p> </p>
</div> </div>
</div> </div>
)} )}
{review.status === "Completed" && ( <div className="flex items-center gap-3 p-3 bg-slate-700/30 rounded">
<div className="flex items-center gap-3 p-3 bg-slate-700/30 rounded"> <MessageSquare className="h-5 w-5 text-purple-400" />
<CheckCircle className="h-5 w-5 text-green-400" /> <div>
<div> <p className="text-sm text-slate-300">Your Comments</p>
<p className="text-sm text-slate-300"> <p className="text-sm text-purple-300">
Manager Review {review.employee_comments ? "Submitted" : "Not submitted"}
</p> </p>
<p className="text-sm text-green-300">
Completed
</p>
</div>
</div> </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>
)}
<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 || "");
}}
>
{review.employee_comments ? "Edit Comments" : "Add Comments"}
</Button>
</div> </div>
<Button
size="sm"
className="bg-purple-600 hover:bg-purple-700"
>
View Full Review
</Button>
</CardContent> </CardContent>
)} )}
</Card> </Card>
@ -296,39 +309,44 @@ export default function StaffPerformanceReviews() {
</div> </div>
</div> </div>
{/* Action Items */} {reviews.length === 0 && (
<Card className="bg-slate-800/50 border-purple-500/30"> <div className="text-center py-12">
<CardHeader> <p className="text-slate-400">No reviews found</p>
<CardTitle className="text-purple-100">Next Steps</CardTitle> </div>
</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>
</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> </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> </Layout>
); );
} }

View file

@ -1,4 +1,4 @@
import { useState } from "react"; import { useState, useEffect } from "react";
import Layout from "@/components/Layout"; import Layout from "@/components/Layout";
import SEO from "@/components/SEO"; import SEO from "@/components/SEO";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -12,110 +12,189 @@ import {
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import { import {
BarChart,
Target, Target,
TrendingUp, TrendingUp,
Zap,
Users,
CheckCircle, CheckCircle,
Loader2,
Plus,
Calendar,
} from "lucide-react"; } 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; id: string;
title: string; title: string;
description: string; description?: string;
owner: string; status: string;
progress: number; priority: string;
status: "On Track" | "At Risk" | "Completed"; due_date?: string;
quarter: string; completed_at?: string;
team: string;
} }
const okrs: OKR[] = [ interface Project {
{ id: string;
id: "1", name: string;
title: "Improve Platform Performance by 40%", description: string;
description: "Reduce page load time and increase throughput", status: string;
owner: "Engineering", start_date: string;
progress: 75, end_date?: string;
status: "On Track", lead?: {
quarter: "Q1 2025", full_name: string;
team: "DevOps", avatar_url?: string;
}, };
{ tasks: Task[];
id: "2", task_stats: {
title: "Expand Creator Network to 5K Members", total: number;
description: "Grow creator base through partnerships and incentives", done: number;
owner: "Community", };
progress: 62, }
status: "On Track",
quarter: "Q1 2025", interface Stats {
team: "Growth", total: number;
}, active: number;
{ completed: number;
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",
},
];
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
switch (status) { switch (status) {
case "On Track": case "active":
return "bg-green-500/20 text-green-300 border-green-500/30"; return "bg-green-500/20 text-green-300 border-green-500/30";
case "At Risk": case "completed":
return "bg-amber-500/20 text-amber-300 border-amber-500/30";
case "Completed":
return "bg-blue-500/20 text-blue-300 border-blue-500/30"; 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: default:
return "bg-slate-500/20 text-slate-300 border-slate-500/30"; 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() { 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 const fetchProjects = async () => {
? okrs.filter((okr) => okr.team === selectedTeam) try {
: okrs; 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 = const updateTaskStatus = async (taskId: string, status: string) => {
Math.round( try {
filtered.reduce((sum, okr) => sum + okr.progress, 0) / filtered.length, const res = await fetch("/api/staff/projects", {
) || 0; 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 ( return (
<Layout> <Layout>
<SEO <SEO
title="Project Tracking" 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"> <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 Project Tracking
</h1> </h1>
<p className="text-indigo-200/70"> <p className="text-indigo-200/70">
OKRs, initiatives, and company-wide roadmap Your projects, tasks, and progress
</p> </p>
</div> </div>
</div> </div>
@ -148,9 +227,9 @@ export default function StaffProjectTracking() {
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <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"> <p className="text-3xl font-bold text-indigo-100">
{filtered.length} {stats.total}
</p> </p>
</div> </div>
<Target className="h-8 w-8 text-indigo-400" /> <Target className="h-8 w-8 text-indigo-400" />
@ -174,9 +253,9 @@ export default function StaffProjectTracking() {
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <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"> <p className="text-3xl font-bold text-indigo-100">
{filtered.filter((o) => o.status === "On Track").length} {stats.active}
</p> </p>
</div> </div>
<CheckCircle className="h-8 w-8 text-indigo-400" /> <CheckCircle className="h-8 w-8 text-indigo-400" />
@ -185,93 +264,180 @@ export default function StaffProjectTracking() {
</Card> </Card>
</div> </div>
{/* Team Filter */} {/* Projects */}
<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 */}
<div className="space-y-6"> <div className="space-y-6">
{filtered.map((okr) => ( {projects.map((project) => (
<Card <Card
key={okr.id} key={project.id}
className="bg-slate-800/50 border-slate-700/50 hover:border-indigo-500/50 transition-all" 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 items-start justify-between">
<div className="flex-1"> <div className="flex-1">
<CardTitle className="text-indigo-100"> <CardTitle className="text-indigo-100">
{okr.title} {project.name}
</CardTitle> </CardTitle>
<CardDescription className="text-slate-400"> <CardDescription className="text-slate-400">
{okr.description} {project.description}
</CardDescription> </CardDescription>
</div> </div>
<Badge className={`border ${getStatusColor(okr.status)}`}> <Badge className={`border ${getStatusColor(project.status)}`}>
{okr.status} {project.status.replace("_", " ").replace(/\b\w/g, (l) => l.toUpperCase())}
</Badge> </Badge>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex justify-between text-sm"> <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"> <span className="text-indigo-300 font-semibold">
{okr.progress}% {project.task_stats.done}/{project.task_stats.total}
</span> </span>
</div> </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>
<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">Lead</p>
<p className="text-indigo-300">{project.lead.full_name}</p>
</div>
)}
<div> <div>
<p className="text-xs text-slate-500">Owner</p> <p className="text-xs text-slate-500">Start Date</p>
<p className="text-sm text-indigo-300">{okr.owner}</p> <p className="text-indigo-300">{new Date(project.start_date).toLocaleDateString()}</p>
</div>
<div>
<p className="text-xs text-slate-500">Quarter</p>
<p className="text-sm text-indigo-300">{okr.quarter}</p>
</div>
<div>
<p className="text-xs text-slate-500">Team</p>
<p className="text-sm text-indigo-300">{okr.team}</p>
</div> </div>
{project.end_date && (
<div>
<p className="text-xs text-slate-500">End Date</p>
<p className="text-indigo-300">{new Date(project.end_date).toLocaleDateString()}</p>
</div>
)}
</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> </CardContent>
</Card> </Card>
))} ))}
</div> </div>
{projects.length === 0 && (
<div className="text-center py-12">
<p className="text-slate-400">No projects found</p>
</div>
)}
</div> </div>
</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> </Layout>
); );
} }

View file

@ -1,3 +1,4 @@
import { useState, useEffect } from "react";
import Layout from "@/components/Layout"; import Layout from "@/components/Layout";
import SEO from "@/components/SEO"; import SEO from "@/components/SEO";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -11,94 +12,88 @@ import {
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { import {
Heart, Heart,
DollarSign,
Calendar, Calendar,
MapPin, MapPin,
Users, Users,
Shield, Shield,
Zap, Zap,
Award, Award,
Loader2,
ChevronDown,
ChevronUp,
} from "lucide-react"; } from "lucide-react";
import { useAuth } from "@/lib/auth";
import { aethexToast } from "@/components/ui/aethex-toast";
interface HandbookSection { interface HandbookSection {
id: string; id: string;
category: string;
title: string; title: string;
icon: React.ReactNode;
content: string; content: string;
subsections: string[]; order_index: number;
} }
const sections: HandbookSection[] = [ const getCategoryIcon = (category: string) => {
{ switch (category) {
id: "1", case "Benefits":
title: "Benefits & Compensation", return <Heart className="h-6 w-6" />;
icon: <Heart className="h-6 w-6" />, case "Policies":
content: "Comprehensive benefits package including health, dental, vision", return <Shield className="h-6 w-6" />;
subsections: [ case "Time Off":
"Health Insurance", return <Calendar className="h-6 w-6" />;
"Retirement Plans", case "Remote Work":
"Stock Options", return <MapPin className="h-6 w-6" />;
"Flexible PTO", case "Development":
], return <Zap className="h-6 w-6" />;
}, case "Recognition":
{ return <Award className="h-6 w-6" />;
id: "2", default:
title: "Company Policies", return <Users className="h-6 w-6" />;
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",
],
},
];
export default function StaffTeamHandbook() { 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 ( return (
<Layout> <Layout>
<SEO <SEO
@ -158,50 +153,67 @@ export default function StaffTeamHandbook() {
</Card> </Card>
</div> </div>
{/* Handbook Sections */} {/* Handbook Sections by Category */}
<div className="space-y-6"> <div className="space-y-6">
{sections.map((section) => ( {categories.map((category) => (
<Card <Card
key={section.id} key={category}
className="bg-slate-800/50 border-slate-700/50 hover:border-blue-500/50 transition-all" className="bg-slate-800/50 border-slate-700/50 hover:border-blue-500/50 transition-all"
> >
<CardHeader> <CardHeader
<div className="flex items-start justify-between"> 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="flex items-center gap-3">
<div className="p-2 rounded-lg bg-blue-500/20 text-blue-400"> <div className="p-2 rounded-lg bg-blue-500/20 text-blue-400">
{section.icon} {getCategoryIcon(category)}
</div> </div>
<div> <div>
<CardTitle className="text-blue-100"> <CardTitle className="text-blue-100">
{section.title} {category}
</CardTitle> </CardTitle>
<CardDescription className="text-slate-400"> <CardDescription className="text-slate-400">
{section.content} {grouped[category]?.length || 0} sections
</CardDescription> </CardDescription>
</div> </div>
</div> </div>
{expandedCategory === category ? (
<ChevronUp className="h-5 w-5 text-blue-400" />
) : (
<ChevronDown className="h-5 w-5 text-blue-400" />
)}
</div> </div>
</CardHeader> </CardHeader>
<CardContent> {expandedCategory === category && (
<div className="flex flex-wrap gap-2 mb-4"> <CardContent className="pt-0">
{section.subsections.map((subsection) => ( <div className="space-y-4 pl-14">
<Badge {grouped[category]?.map((section) => (
key={subsection} <div
variant="secondary" key={section.id}
className="bg-slate-700/50 text-slate-300" className="p-4 bg-slate-700/30 rounded-lg"
> >
{subsection} <h4 className="font-semibold text-blue-100 mb-2">
</Badge> {section.title}
))} </h4>
</div> <p className="text-slate-300 text-sm whitespace-pre-line">
<Button size="sm" className="bg-blue-600 hover:bg-blue-700"> {section.content}
View Details </p>
</Button> </div>
</CardContent> ))}
</div>
</CardContent>
)}
</Card> </Card>
))} ))}
</div> </div>
{categories.length === 0 && (
<div className="text-center py-12">
<p className="text-slate-400">No handbook sections found</p>
</div>
)}
{/* Additional Resources */} {/* Additional Resources */}
<div className="mt-12 p-6 rounded-lg bg-slate-800/50 border border-blue-500/30"> <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"> <h2 className="text-xl font-bold text-blue-100 mb-4">