Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect } from "react"; | |
| import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; | |
| import { Input } from "@/components/ui/input"; | |
| import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; | |
| import { PlusCircle, Search, Loader2, Edit, Trash2, Save, X, Plus } from "lucide-react"; | |
| import { Label } from "@/components/ui/label"; | |
| import CustomDialog from "@/components/CustomDialog"; | |
| import { featureApi } from "@/services/featureApi"; | |
| import { Feature, FeatureCreateRequest, FeatureUpdateRequest } from "@/types"; | |
| import Header from "@/components/layout/header"; | |
| import Sidebar from "@/components/layout/sidebar"; | |
| import { useIsMobile } from "@/hooks/use-mobile"; | |
| import { toast } from "@/lib/custom-toast"; | |
| import { Checkbox } from "@/components/ui/checkbox"; | |
| import { Badge } from "@/components/ui/badge"; | |
| const Features = () => { | |
| const queryClient = useQueryClient(); | |
| const isMobile = useIsMobile(); | |
| const [isSidebarOpen, setIsSidebarOpen] = useState(!isMobile); | |
| const [searchQuery, setSearchQuery] = useState(""); | |
| const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); | |
| const [newFeatureName, setNewFeatureName] = useState(""); | |
| const [newFeatureDescription, setNewFeatureDescription] = useState(""); | |
| const [newFeatureCategory, setNewFeatureCategory] = useState(""); | |
| const [newFeatureStatus, setNewFeatureStatus] = useState(true); | |
| const [editingFeature, setEditingFeature] = useState<Feature | null>(null); | |
| const [editFeatureName, setEditFeatureName] = useState(""); | |
| const [editFeatureDescription, setEditFeatureDescription] = useState(""); | |
| const [editFeatureCategory, setEditFeatureCategory] = useState(""); | |
| const [editFeatureStatus, setEditFeatureStatus] = useState(true); | |
| // Fetch features | |
| const { data: features = [], isLoading, error, refetch } = useQuery({ | |
| queryKey: ["features"], | |
| queryFn: featureApi.getAll, | |
| }); | |
| // Create feature mutation | |
| const createMutation = useMutation({ | |
| mutationFn: featureApi.create, | |
| onSuccess: () => { | |
| toast.success("Feature created successfully"); | |
| queryClient.invalidateQueries({ queryKey: ["features"] }); | |
| setIsAddDialogOpen(false); | |
| setNewFeatureName(""); | |
| }, | |
| onError: (error) => { | |
| toast.error("Failed to create feature"); | |
| console.error("Error creating feature:", error); | |
| } | |
| }); | |
| // Update feature mutation | |
| const updateMutation = useMutation({ | |
| mutationFn: ({ id, feature }: { id: number, feature: FeatureUpdateRequest }) => | |
| featureApi.update(id, feature), | |
| onSuccess: () => { | |
| toast.success("Feature updated successfully"); | |
| queryClient.invalidateQueries({ queryKey: ["features"] }); | |
| setEditingFeature(null); | |
| }, | |
| onError: (error) => { | |
| toast.error("Failed to update feature"); | |
| console.error("Error updating feature:", error); | |
| } | |
| }); | |
| // Delete feature mutation | |
| const deleteMutation = useMutation({ | |
| mutationFn: featureApi.delete, | |
| onSuccess: () => { | |
| toast.success("Feature deleted successfully"); | |
| queryClient.invalidateQueries({ queryKey: ["features"] }); | |
| }, | |
| onError: (error) => { | |
| toast.error("Failed to delete feature"); | |
| console.error("Error deleting feature:", error); | |
| } | |
| }); | |
| // Toggle sidebar | |
| const toggleSidebar = () => { | |
| setIsSidebarOpen(!isSidebarOpen); | |
| }; | |
| // Handle create feature | |
| const handleCreateFeature = (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| if (!newFeatureName.trim()) { | |
| toast.error("Feature name cannot be empty"); | |
| return; | |
| } | |
| const newFeature: FeatureCreateRequest = { | |
| featureId: 0, // This will be assigned by the server | |
| featureName: newFeatureName.trim(), | |
| description: newFeatureDescription.trim(), | |
| category: newFeatureCategory.trim(), | |
| isActive: newFeatureStatus | |
| }; | |
| createMutation.mutate(newFeature); | |
| }; | |
| // Handle create feature input change | |
| const handleCreateChange = (e: React.ChangeEvent<HTMLInputElement>) => { | |
| const { name, value } = e.target; | |
| switch(name) { | |
| case 'featureName': | |
| setNewFeatureName(value); | |
| break; | |
| case 'featureDescription': | |
| setNewFeatureDescription(value); | |
| break; | |
| case 'featureCategory': | |
| setNewFeatureCategory(value); | |
| break; | |
| default: | |
| break; | |
| } | |
| }; | |
| // Handle edit feature | |
| const handleEditFeature = (feature: Feature) => { | |
| setEditingFeature(feature); | |
| setEditFeatureName(feature.featureName); | |
| setEditFeatureDescription(feature.description || ""); | |
| setEditFeatureCategory(feature.category || ""); | |
| setEditFeatureStatus(feature.isActive !== undefined ? feature.isActive : true); | |
| }; | |
| // Handle update feature | |
| const handleUpdateFeature = () => { | |
| if (!editingFeature) return; | |
| if (!editFeatureName.trim()) { | |
| toast.error("Feature name cannot be empty"); | |
| return; | |
| } | |
| const updatedFeature: FeatureUpdateRequest = { | |
| featureId: editingFeature.featureId, | |
| featureName: editFeatureName.trim(), | |
| description: editFeatureDescription.trim(), | |
| category: editFeatureCategory.trim(), | |
| isActive: editFeatureStatus | |
| }; | |
| updateMutation.mutate({ | |
| id: editingFeature.featureId, | |
| feature: updatedFeature | |
| }); | |
| }; | |
| // Handle delete feature | |
| const handleDeleteFeature = (id: number) => { | |
| if (window.confirm("Are you sure you want to delete this feature?")) { | |
| deleteMutation.mutate(id); | |
| } | |
| }; | |
| // Cancel editing | |
| const cancelEditing = () => { | |
| setEditingFeature(null); | |
| }; | |
| // Filter features based on search query | |
| const filteredFeatures = features?.filter((feature: Feature) => { | |
| if (!searchQuery) return true; | |
| const query = searchQuery.toLowerCase(); | |
| return feature.featureName.toLowerCase().includes(query); | |
| }); | |
| // Function to ensure all standard features exist in the database | |
| const ensureAllMissingFeatures = async () => { | |
| try { | |
| const existingFeatures = await featureApi.getAll(); | |
| // Define all required features with unique IDs (1-53) | |
| const requiredFeatures = [ | |
| // Core features | |
| { featureId: 1, featureName: "Dashboard", description: "Main dashboard with overview statistics", category: "Core", isActive: true }, | |
| { featureId: 2, featureName: "Projects", description: "Project management and tracking", category: "Core", isActive: true }, | |
| { featureId: 3, featureName: "Deliverables", description: "Project deliverables management", category: "Project", isActive: true }, | |
| { featureId: 4, featureName: "Teams", description: "Team management and organization", category: "Core", isActive: true }, | |
| { featureId: 5, featureName: "Employees", description: "Employee management and profiles", category: "HR", isActive: true }, | |
| // Admin features | |
| { featureId: 6, featureName: "Roles", description: "Role management and permissions", category: "Administration", isActive: true }, | |
| { featureId: 7, featureName: "Permissions", description: "Permissions management", category: "Administration", isActive: true }, | |
| { featureId: 8, featureName: "Role Features", description: "Manage feature access for roles", category: "Administration", isActive: true }, | |
| // Project features | |
| { featureId: 9, featureName: "Backlog", description: "Product backlog management", category: "Project", isActive: true }, | |
| { featureId: 10, featureName: "Issues", description: "Issue tracking and resolution", category: "Project", isActive: true }, | |
| { featureId: 11, featureName: "Tasks", description: "Task tracking and assignment", category: "Project", isActive: true }, | |
| { featureId: 12, featureName: "Time Logs", description: "Time tracking for tasks and projects", category: "Project", isActive: true }, | |
| // Additional core features | |
| { featureId: 13, featureName: "Calendar", description: "Calendar view for events and deadlines", category: "Core", isActive: true }, | |
| { featureId: 14, featureName: "Reports", description: "Reporting and analytics", category: "Management", isActive: true }, | |
| { featureId: 15, featureName: "Messages", description: "Internal messaging system", category: "Communication", isActive: true }, | |
| { featureId: 16, featureName: "Settings", description: "System settings and configuration", category: "Administration", isActive: true }, | |
| { featureId: 17, featureName: "Notifications", description: "User notifications", category: "Core", isActive: true }, | |
| { featureId: 18, featureName: "Audit Logs", description: "Audit logs and security tracking", category: "Administration", isActive: true }, | |
| { featureId: 19, featureName: "Integrations", description: "Third-party system integrations", category: "Administration", isActive: true }, | |
| { featureId: 20, featureName: "File Management", description: "Document and file storage system", category: "Core", isActive: true }, | |
| // HR management features | |
| { featureId: 21, featureName: "Attendance", description: "Employee attendance tracking", category: "HR", isActive: true }, | |
| { featureId: 22, featureName: "Leaves", description: "Employee leave requests", category: "HR", isActive: true }, | |
| { featureId: 23, featureName: "Leaves Management", description: "Administrative leave management", category: "HR", isActive: true }, | |
| { featureId: 24, featureName: "Leave Types", description: "Configure leave types and policies", category: "HR", isActive: true }, | |
| { featureId: 42, featureName: "Leave Balances", description: "View your leave balances and history", category: "HR", isActive: true }, | |
| // Productivity features | |
| { featureId: 25, featureName: "Todos", description: "Personal to-do list management", category: "Productivity", isActive: true }, | |
| { featureId: 26, featureName: "Holidays", description: "Company holidays management", category: "HR", isActive: true }, | |
| { featureId: 27, featureName: "Bugs", description: "Bug tracking and resolution", category: "Project", isActive: true }, | |
| { featureId: 39, featureName: "Production Bugs", description: "Track and manage production bugs", category: "Project", isActive: true }, | |
| { featureId: 40, featureName: "Test Cases", description: "Create and manage test cases", category: "Project", isActive: true }, | |
| // Appraisal system features | |
| { featureId: 28, featureName: "My Appraisals", description: "Access to your own appraisals", category: "Appraisal", isActive: true }, | |
| { featureId: 29, featureName: "Appraisal Cycles", description: "Manage appraisal cycle periods", category: "Appraisal", isActive: true }, | |
| { featureId: 30, featureName: "Assessment Areas", description: "Manage assessment areas for appraisals", category: "Appraisal", isActive: true }, | |
| { featureId: 31, featureName: "Competencies", description: "Manage competencies for appraisals", category: "Appraisal", isActive: true }, | |
| { featureId: 32, featureName: "Manager Appraisals", description: "Access to team member appraisals for managers", category: "Appraisal", isActive: true }, | |
| { featureId: 36, featureName: "Role Assessment Questions", description: "Configure assessment questions per role", category: "Appraisal", isActive: true }, | |
| { featureId: 37, featureName: "HR Remarks", description: "HR comments on appraisals", category: "Appraisal", isActive: true }, | |
| { featureId: 38, featureName: "Management Remarks", description: "Management comments on appraisals", category: "Appraisal", isActive: true }, | |
| // Facilities features | |
| { featureId: 33, featureName: "Meeting Rooms", description: "Meeting room management", category: "Facilities", isActive: true }, | |
| { featureId: 34, featureName: "Room Bookings", description: "Book and schedule meeting rooms", category: "Facilities", isActive: true }, | |
| // Communication features | |
| { featureId: 35, featureName: "Communications", description: "Communication tools and channels", category: "Communication", isActive: true }, | |
| // Other Project & Management features | |
| { featureId: 41, featureName: "My Profile", description: "Access and manage your personal profile", category: "Core", isActive: true }, | |
| { featureId: 43, featureName: "Employee Projects", description: "Manage projects assigned to employees", category: "Project", isActive: true }, | |
| { featureId: 44, featureName: "Health Dashboard", description: "System and project health overview", category: "Management", isActive: true }, | |
| { featureId: 50, featureName: "Gantt Chart", description: "Visual project timeline and schedule", category: "Project", isActive: true }, | |
| { featureId: 51, featureName: "Resource Allocation", description: "Manage and track resource distribution", category: "Project", isActive: true }, | |
| // Asset Management features | |
| { featureId: 45, featureName: "Assets", description: "General asset management", category: "Asset Management", isActive: true }, | |
| { featureId: 46, featureName: "System Details", description: "System and hardware details for assets", category: "Asset Management", isActive: true }, | |
| { featureId: 47, featureName: "Asset Assignments", description: "Track asset assignments to employees", category: "Asset Management", isActive: true }, | |
| { featureId: 48, featureName: "My Asset Requests", description: "Employee interface for requesting assets", category: "Asset Management", isActive: true }, | |
| { featureId: 49, featureName: "Asset Request Management", description: "Admin interface for managing asset requests", category: "Asset Management", isActive: true }, | |
| // New Admin features | |
| { featureId: 52, featureName: "Feature Management", description: "System features and functionality management", category: "Administration", isActive: true }, | |
| { featureId: 53, featureName: "Role Permissions", description: "Manage permissions for system roles", category: "Administration", isActive: true }, | |
| ]; | |
| let createdCount = 0; | |
| for (const reqFeature of requiredFeatures) { | |
| const exists = existingFeatures.some(f => f.featureId === reqFeature.featureId); | |
| if (!exists) { | |
| console.log(`Creating missing feature: ${reqFeature.featureName}`); | |
| await featureApi.create(reqFeature); | |
| createdCount++; | |
| } | |
| } | |
| queryClient.invalidateQueries({ queryKey: ["features"] }); | |
| if (createdCount > 0) { | |
| toast.success(`Added ${createdCount} missing features`, { | |
| description: "All standard features have been added to the system." | |
| }); | |
| } else { | |
| toast.info("All features already exist", { | |
| description: "No new features were added." | |
| }); | |
| } | |
| } catch (error) { | |
| console.error("Error ensuring features:", error); | |
| toast.error("Failed to create features", { | |
| description: "An error occurred while creating the required features." | |
| }); | |
| } | |
| }; | |
| return ( | |
| <div className="flex min-h-screen flex-col"> | |
| <Header toggleSidebar={toggleSidebar} isSidebarOpen={isSidebarOpen} /> | |
| <Sidebar isOpen={isSidebarOpen} setIsOpen={setIsSidebarOpen} /> | |
| <main className={`flex-1 p-6 pt-20 md:p-10 md:pt-24 ${isSidebarOpen ? "md:ml-64" : ""} transition-all duration-300`}> | |
| <div className="mx-auto max-w-6xl"> | |
| <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between mb-8 gap-4"> | |
| <div> | |
| <h2 className="text-3xl font-bold tracking-tight">Features</h2> | |
| <p className="text-muted-foreground"> | |
| System features and functionality | |
| </p> | |
| </div> | |
| <div className="flex flex-wrap gap-2"> | |
| <Button onClick={() => ensureAllMissingFeatures()}> | |
| <Plus className="mr-2 h-4 w-4" /> | |
| Add All Missing Features | |
| </Button> | |
| <Button onClick={(e) => { | |
| e.stopPropagation(); | |
| setIsAddDialogOpen(true); | |
| }}> | |
| <PlusCircle className="mr-2 h-4 w-4" /> | |
| Add Feature | |
| </Button> | |
| </div> | |
| <CustomDialog | |
| isOpen={isAddDialogOpen} | |
| onClose={() => !createMutation.isPending && setIsAddDialogOpen(false)} | |
| title="Create New Feature" | |
| description="Add a new feature to the system" | |
| allowClose={!createMutation.isPending} | |
| isPending={createMutation.isPending} | |
| > | |
| <form onSubmit={handleCreateFeature}> | |
| <div className="grid gap-4 py-4"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="featureName">Feature Name*</Label> | |
| <Input | |
| id="featureName" | |
| name="featureName" | |
| value={newFeatureName} | |
| onChange={handleCreateChange} | |
| placeholder="Dashboard, Projects, Teams, etc." | |
| required | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="featureDescription">Feature Description</Label> | |
| <Input | |
| id="featureDescription" | |
| name="featureDescription" | |
| value={newFeatureDescription} | |
| onChange={handleCreateChange} | |
| placeholder="Brief description of this feature" | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="featureCategory">Category</Label> | |
| <Input | |
| id="featureCategory" | |
| name="featureCategory" | |
| value={newFeatureCategory} | |
| onChange={handleCreateChange} | |
| placeholder="Admin, User, Reports, etc." | |
| /> | |
| </div> | |
| <div className="flex items-center space-x-2"> | |
| <Checkbox | |
| id="featureStatus" | |
| checked={newFeatureStatus} | |
| onCheckedChange={(checked) => setNewFeatureStatus(checked as boolean)} | |
| /> | |
| <Label htmlFor="featureStatus">Active</Label> | |
| </div> | |
| </div> | |
| <div className="flex justify-end gap-2 mt-4"> | |
| <Button type="button" variant="outline" onClick={() => !createMutation.isPending && setIsAddDialogOpen(false)}> | |
| Cancel | |
| </Button> | |
| <Button type="submit" disabled={createMutation.isPending || !newFeatureName.trim()}> | |
| {createMutation.isPending ? ( | |
| <> | |
| <Loader2 className="mr-2 h-4 w-4 animate-spin" /> | |
| Creating... | |
| </> | |
| ) : "Create Feature"} | |
| </Button> | |
| </div> | |
| </form> | |
| </CustomDialog> | |
| </div> | |
| <Card> | |
| <CardHeader> | |
| <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4"> | |
| <div> | |
| <CardTitle>Feature Management</CardTitle> | |
| <CardDescription> | |
| {features?.length || 0} features defined in the system | |
| </CardDescription> | |
| </div> | |
| <div className="w-full sm:w-auto relative"> | |
| <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> | |
| <Input | |
| placeholder="Search features..." | |
| className="pl-9 w-full sm:w-[250px]" | |
| value={searchQuery} | |
| onChange={(e) => setSearchQuery(e.target.value)} | |
| /> | |
| </div> | |
| </div> | |
| </CardHeader> | |
| <CardContent> | |
| {isLoading ? ( | |
| <div className="flex justify-center items-center h-64"> | |
| <Loader2 className="h-8 w-8 animate-spin text-primary" /> | |
| </div> | |
| ) : error ? ( | |
| <div className="flex justify-center items-center h-64 text-destructive"> | |
| <p>Error loading features. Please try again later.</p> | |
| </div> | |
| ) : filteredFeatures?.length === 0 ? ( | |
| <div className="flex justify-center items-center h-64 text-muted-foreground"> | |
| <p>No features found.</p> | |
| </div> | |
| ) : ( | |
| <div className="rounded-md border"> | |
| <Table> | |
| <TableHeader> | |
| <TableRow> | |
| <TableHead>ID</TableHead> | |
| <TableHead>Feature Name</TableHead> | |
| <TableHead>Description</TableHead> | |
| <TableHead>Category</TableHead> | |
| <TableHead>Status</TableHead> | |
| <TableHead className="w-24 text-right">Actions</TableHead> | |
| </TableRow> | |
| </TableHeader> | |
| <TableBody> | |
| {filteredFeatures?.map((feature: Feature) => ( | |
| <TableRow key={feature.featureId}> | |
| <TableCell>{feature.featureId}</TableCell> | |
| {editingFeature?.featureId === feature.featureId ? ( | |
| <TableCell> | |
| <Input | |
| value={editFeatureName} | |
| onChange={(e) => setEditFeatureName(e.target.value)} | |
| className="w-full" | |
| /> | |
| </TableCell> | |
| ) : ( | |
| <TableCell className="font-medium">{feature.featureName}</TableCell> | |
| )} | |
| {editingFeature?.featureId === feature.featureId ? ( | |
| <TableCell> | |
| <Input | |
| value={editFeatureDescription} | |
| onChange={(e) => setEditFeatureDescription(e.target.value)} | |
| className="w-full" | |
| /> | |
| </TableCell> | |
| ) : ( | |
| <TableCell>{feature.description || "-"}</TableCell> | |
| )} | |
| {editingFeature?.featureId === feature.featureId ? ( | |
| <TableCell> | |
| <Input | |
| value={editFeatureCategory} | |
| onChange={(e) => setEditFeatureCategory(e.target.value)} | |
| className="w-full" | |
| /> | |
| </TableCell> | |
| ) : ( | |
| <TableCell>{feature.category || "-"}</TableCell> | |
| )} | |
| {editingFeature?.featureId === feature.featureId ? ( | |
| <TableCell> | |
| <div className="flex items-center space-x-2"> | |
| <Checkbox | |
| id={`edit-status-${feature.featureId}`} | |
| checked={editFeatureStatus} | |
| onCheckedChange={(checked) => setEditFeatureStatus(checked as boolean)} | |
| /> | |
| <Label htmlFor={`edit-status-${feature.featureId}`}>Active</Label> | |
| </div> | |
| </TableCell> | |
| ) : ( | |
| <TableCell> | |
| <Badge variant={feature.isActive ? "success" : "secondary"}> | |
| {feature.isActive ? "Active" : "Inactive"} | |
| </Badge> | |
| </TableCell> | |
| )} | |
| <TableCell className="text-right"> | |
| {editingFeature?.featureId === feature.featureId ? ( | |
| <div className="flex justify-end gap-2"> | |
| <Button | |
| size="sm" | |
| variant="outline" | |
| onClick={cancelEditing} | |
| > | |
| <X className="h-4 w-4" /> | |
| </Button> | |
| <Button | |
| size="sm" | |
| onClick={handleUpdateFeature} | |
| > | |
| <Save className="h-4 w-4" /> | |
| </Button> | |
| </div> | |
| ) : ( | |
| <div className="flex justify-end gap-2"> | |
| <Button | |
| size="sm" | |
| variant="outline" | |
| onClick={() => handleEditFeature(feature)} | |
| > | |
| <Edit className="h-4 w-4" /> | |
| </Button> | |
| <Button | |
| size="sm" | |
| variant="destructive" | |
| onClick={() => handleDeleteFeature(feature.featureId)} | |
| > | |
| <Trash2 className="h-4 w-4" /> | |
| </Button> | |
| </div> | |
| )} | |
| </TableCell> | |
| </TableRow> | |
| ))} | |
| </TableBody> | |
| </Table> | |
| </div> | |
| )} | |
| </CardContent> | |
| </Card> | |
| </div> | |
| </main> | |
| </div> | |
| ); | |
| }; | |
| export default Features; |