feat: Initial commit of KROW Workforce Web client (Base44 export)
This commit is contained in:
154
src/pages/ActivityLog.jsx
Normal file
154
src/pages/ActivityLog.jsx
Normal file
@@ -0,0 +1,154 @@
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { base44 } from "@/api/base44Client";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
Activity,
|
||||
Calendar,
|
||||
UserPlus,
|
||||
FileText,
|
||||
MessageSquare,
|
||||
AlertCircle,
|
||||
CheckCircle,
|
||||
Search,
|
||||
Filter
|
||||
} from "lucide-react";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import PageHeader from "../components/common/PageHeader";
|
||||
|
||||
const iconMap = {
|
||||
calendar: Calendar,
|
||||
user: UserPlus,
|
||||
invoice: FileText,
|
||||
message: MessageSquare,
|
||||
alert: AlertCircle,
|
||||
check: CheckCircle,
|
||||
};
|
||||
|
||||
const colorMap = {
|
||||
blue: "bg-blue-100 text-blue-600",
|
||||
red: "bg-red-100 text-red-600",
|
||||
green: "bg-green-100 text-green-600",
|
||||
yellow: "bg-yellow-100 text-yellow-600",
|
||||
purple: "bg-purple-100 text-purple-600",
|
||||
};
|
||||
|
||||
export default function ActivityLog() {
|
||||
const [activeTab, setActiveTab] = useState("all");
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: user } = useQuery({
|
||||
queryKey: ['current-user-activity'],
|
||||
queryFn: () => base44.auth.me(),
|
||||
});
|
||||
|
||||
const { data: activities = [], isLoading } = useQuery({
|
||||
queryKey: ['activity-logs', user?.id],
|
||||
queryFn: () => base44.entities.ActivityLog.filter({ user_id: user?.id }, '-created_date', 100),
|
||||
enabled: !!user?.id,
|
||||
initialData: [],
|
||||
});
|
||||
|
||||
const filteredActivities = activities.filter(activity => {
|
||||
const matchesSearch = !searchTerm ||
|
||||
activity.title?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
activity.description?.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
|
||||
const matchesTab = activeTab === "all" ||
|
||||
(activeTab === "unread" && !activity.is_read) ||
|
||||
(activeTab === "read" && activity.is_read);
|
||||
|
||||
return matchesSearch && matchesTab;
|
||||
});
|
||||
|
||||
const unreadCount = activities.filter(a => !a.is_read).length;
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-8 bg-slate-50 min-h-screen">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<PageHeader
|
||||
title="Activity Log"
|
||||
subtitle={`${activities.length} total activities • ${unreadCount} unread`}
|
||||
/>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="mb-6 space-y-4">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="bg-white border border-slate-200">
|
||||
<TabsTrigger value="all">
|
||||
All <Badge variant="secondary" className="ml-2">{activities.length}</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="unread">
|
||||
Unread <Badge variant="secondary" className="ml-2">{unreadCount}</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="read">
|
||||
Read <Badge variant="secondary" className="ml-2">{activities.length - unreadCount}</Badge>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-slate-400" />
|
||||
<Input
|
||||
placeholder="Search activities..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Activities List */}
|
||||
<div className="space-y-3">
|
||||
{filteredActivities.length > 0 ? (
|
||||
filteredActivities.map((activity) => {
|
||||
const Icon = iconMap[activity.icon_type] || AlertCircle;
|
||||
const colorClass = colorMap[activity.icon_color] || colorMap.blue;
|
||||
|
||||
return (
|
||||
<Card key={activity.id} className={`border-2 ${activity.is_read ? 'border-slate-200 opacity-70' : 'border-blue-200'}`}>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex gap-4">
|
||||
<div className={`w-14 h-14 rounded-full ${colorClass} flex items-center justify-center flex-shrink-0`}>
|
||||
<Icon className="w-7 h-7" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<h3 className="font-bold text-[#1C323E] text-lg">{activity.title}</h3>
|
||||
<span className="text-sm text-slate-500 whitespace-nowrap ml-4">
|
||||
{formatDistanceToNow(new Date(activity.created_date), { addSuffix: true })}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-slate-600 mb-4">{activity.description}</p>
|
||||
<div className="flex items-center gap-3">
|
||||
{!activity.is_read && (
|
||||
<Badge className="bg-blue-500 text-white">Unread</Badge>
|
||||
)}
|
||||
<Badge variant="outline">{activity.activity_type?.replace('_', ' ')}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="p-12 text-center">
|
||||
<Activity className="w-16 h-16 mx-auto text-slate-300 mb-4" />
|
||||
<h3 className="text-xl font-semibold text-slate-900 mb-2">No activities found</h3>
|
||||
<p className="text-slate-600">Try adjusting your filters</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user