Tuesday, April 15, 2025

ReactTS: Employee Management System: SETUP and LIST

 To set up and build a React + TypeScript project named emsv2025-ts using React 18.3.1. This includes:

  1. ✅ Project setup with essential dependencies

  2. 📁 Recommended folder structure

  3. 🧠 Explanation of App.tsx, routing, and NaviBar.tsxEmployeeList.tsx components


✅ 1. Project Setup: EMSV2025 (React + TypeScript)


# Create the React TypeScript project 
npx create-react-app emsv2025-ts --template typescript 
cd emsv2025-ts 

# Install dependencies 
npm install axios react-router-dom lodash react-paginate bootstrap react-bootstrap @fortawesome/fontawesome-free 

# Install type definitions 
npm install --save-dev @types/react-bootstrap @types/lodash

📁 2. Folder Structure

│ ├── public/ ├── src/ │ ├── components/ │ │ ├── Header.tsx │ │ │ │ ├── EmployeeList.tsx │ │ ├── AddEmployee.tsx │ │ └── EditEmployee.tsx │ │ │ ├── models/ │ │ ├── Employee.ts │ │ └── Department.ts │ │ │ ├── App.tsx │ ├── index.tsx │ └── custom.d.ts (if needed for fontawesome types) │ ├── package.json └── tsconfig.json

🧠 3. App.tsx (Main Router Configuration)


// Import React core library import React from "react"; // Import routing components from react-router-dom import { BrowserRouter as Router, Routes, Route, useLocation } from "react-router-dom"; // Import Font Awesome CSS (commented out Bootstrap import shows correct import order) import "@fortawesome/fontawesome-free/css/all.min.css"; // Import application components import AddEmployee from "./components/AddEnployee"; import EmployeeList from "./components/EmployeeList"; import EditEmployee from "./components/EditEmployee"; import NaviBar from "./components/NaviBar"; // Main content component that handles route-specific logic const AppContent: React.FC = () => { // Temporary hardcoded user (would normally come from auth context) const user = "Sanjay"; // Get current location using react-router's useLocation hook const location = useLocation(); // Determine if header should be shown (not on login page) const showHeader = location.pathname !== "/login"; return ( // React Fragment (<> </>) to group elements without extra DOM node <> {/* Conditionally render NaviBar based on route */} {showHeader && <NaviBar />} {/* Main content container with top margin */} <div className="container mt-4"> {/* Routes component defines all route paths */} <Routes> {/* Default route (/) shows EmployeeList */} <Route path="/" element={<EmployeeList />} /> {/* Alternate path also showing EmployeeList */} <Route path="/list" element={<EmployeeList />} /> {/* Add employee form route */} <Route path="/add" element={<AddEmployee />} /> {/* Edit employee route with dynamic parameter (empId) */} <Route path="/edit/:empId" element={<EditEmployee />} /> </Routes> </div> </> ); }; // Root application component that wraps everything with Router const App: React.FC = () => ( {/* Router component provides routing context to all child components */} <Router> <AppContent /> </Router> ); // Export the App component as default export default App;

Key Features Explained:

  1. Routing Setup:

    • Router wraps the entire application to enable client-side routing

    • Routes defines all possible route paths

    • Route components map paths to specific components

  2. Conditional Rendering:

    • Uses useLocation to check current path

    • Conditionally shows/hides NaviBar based on route (would hide on /login)

  3. Route Parameters:

    • Dynamic route /edit/:empId captures employee ID in URL

    • The EditEmployee component can access this via useParams()

  4. Layout Structure:

    • Navbar appears at top (when shown)

    • Main content has consistent container styling with mt-4 margin

  5. Scalability:

    • Easy to add new routes by importing components and adding Route entries

    • Clear separation between layout (AppContent) and routing setup (App)


Important Notes:

  1. The commented out Bootstrap import shows the correct order when you need both Bootstrap and custom CSS

  2. The user constant is currently hardcoded but would typically come from:

    • Authentication context

    • Redux store

    • API call

  3. The commented out Header component shows where you might add additional layout elements

  4. This structure follows React Router v6 conventions, which differ from v5:

    • Switch replaced with Routes

    • Different pattern for route parameters

    • element prop instead of component


🧭 4. EmployeeList.tsx (List Employees)

📄 File: src/components/EmployeeList.tsx


Key Features :


  1. State Management: Tracks employees, loading states, errors, and pagination

  2. API Integration: Uses Axios for CRUD operations with error handling

  3. Search Functionality: Implements debounced search to filter employees efficiently

  4. Pagination: Splits data into pages with ReactPaginate

  5. UI Components: Includes loading spinners, error messages, and empty states

  6. Type Safety: Uses TypeScript interfaces for employee data

  7. Responsive Design: Uses Bootstrap classes for responsive layout

// Import necessary React hooks and libraries
import React, { useEffect, useState, useCallback } from 'react';  
// Core React functionality
import axios from 'axios';  // For making HTTP requests
import { Link } from 'react-router-dom';  // For client-side navigation
import { debounce } from 'lodash';  // Utility for debouncing search input
import ReactPaginate from 'react-paginate';  // Pagination component
import { Employee } from '../models/Employee';  // Type definition for Employee

// API endpoint configuration
const API_URL = "http://localhost:9090/api/employees";  // Base URL for employee API

// Main component definition
const EmployeeList: React.FC = () => {
    
// State management for various component needs
    const [employees, setEmployees] = useState<Employee[]>([]);  // Stores all employees from API
    const [filteredEmployees, setFilteredEmployees] = useState<Employee[]>([]);  // Stores filtered employees
    const [search, setSearch] = useState("");  // Current search term
    const [pageNumber, setPageNumber] = useState(0);  // Current page number for pagination
    const employeesPerPage = 5;  // Number of employees to show per page
    const [isLoading, setIsLoading] = useState(true);  // Loading state flag
    const [error, setError] = useState<string | null>(null);  // Error message storage

    // Memoized function to fetch employees from API
    const fetchEmployees = useCallback(async () => {
        try {
            setIsLoading(true);  // Set loading state
            const response = await axios.get<Employee[]>(API_URL);  // API call
            setEmployees(response.data);  // Store all employees
            setFilteredEmployees(response.data);  // Initialize filtered list
            setError(null);  // Clear any previous errors
        } catch (err) {
            setError("Failed to fetch employees. Please try again later.");  
            console.error("Error fetching employees:", err);  // Log error
        } finally {
            setIsLoading(false);  // Always turn off loading state
        }
    }, []);  // Empty dependency array means this is created once

    // Effect hook to fetch employees when component mounts
    useEffect(() => {
        fetchEmployees();  // Call the fetch function
    }, [fetchEmployees]);  // Only re-run if fetchEmployees changes

    // Debounced search handler with memoization
    const handleSearchChange = useCallback(
        debounce((value: string) => {
            setSearch(value);  // Update search term
            setFilteredEmployees(
                value 
                    ? employees.filter(emp =>  // Filter employees if search term exists
                        emp.empName.toLowerCase().includes(value.toLowerCase()) ||  // Name match
                        emp.empPhoneNumber.includes(value)  // Phone match
                    : employees  // Return all employees if no search term
            );
            setPageNumber(0);  // Reset to first page when searching
        }, 300),  // 300ms debounce delay
        [employees]  // Recreate when employees list changes
    );

    // Function to toggle employee active status
    const handleToggle = async (empId: number, currentStatus: boolean) => {
        try {
            await axios.put(`${API_URL}/disable/${empId}/${!currentStatus}`);  // Toggle status
            fetchEmployees();  // Refresh list
        } catch (err) {
            setError("Failed to update employee status");  // Set error
            console.error("Error updating status:", err);  // Log error
        }
    };

    // Pagination calculations
    const pagesVisited = pageNumber * employeesPerPage;  // Current page offset
    const displayEmployees = filteredEmployees.slice(pagesVisited, pagesVisited + employeesPerPage);  // Employees to show
    const pageCount = Math.ceil(filteredEmployees.length / employeesPerPage);  // Total pages needed
    const changePage = ({ selected }: { selected: number }) => setPageNumber(selected);  // Page change handler

    // Component rendering
    return (
        <div className="container-fluid mt-4">
            <div className="card shadow p-4">
                {/* Error display */}
                {error && (
                    <div className="alert alert-danger">
                        {error}
                        <button 
                            className="btn btn-sm btn-light ms-3"
                            onClick={fetchEmployees}
                        >
                            Retry
                        </button>
                    </div>
                )}

                {/* Search and add employee section */}
                <div className="d-flex justify-content-between align-items-center mb-3">
                    <div className="input-group w-50">
                        <span className="input-group-text">
                            <i className="bi bi-search"></i>  {/* Search icon */}
                        </span>
                        <input
                            type="text"
                            className="form-control"
                            placeholder="Search employees..."
                            onChange={(e) => handleSearchChange(e.target.value)}  // Trigger search
                            disabled={isLoading}  // Disable during loading
                        />
                    </div>

                    {/* Add employee button */}
                    <Link to="/add" className="btn btn-success">
                        <i className="bi bi-person-plus"></i> Add Employee
                    </Link>
                </div>

                {/* Loading state */}
                {isLoading ? (
                    <div className="text-center my-5">
                        <div className="spinner-border text-primary" role="status">
                            <span className="visually-hidden">Loading...</span>
                        </div>
                    </div>
                ) : (
                    <>
                        {/* Employee table */}
                        <div className="table-responsive">
                            <table className="table table-striped table-hover">
                                <thead className="table-dark">
                                    <tr>
                                        <th>Name</th>
                                        <th>Designation</th>
                                        <th>Date of Join</th>
                                        <th>Contact</th>
                                        <th>Department</th>
                                        <th>Status</th>
                                        <th>Actions</th>
                                    </tr>
                                </thead>
                                <tbody>
                                    {/* Map through employees to display rows */}
                                    {displayEmployees.map((emp) => (
                                        <tr key={emp.empId}>
                                            <td>{emp.empName}</td>
                                            <td>{emp.designation}</td>
                                            <td>{new Date(emp.dateOfJoining).toLocaleDateString()}</td>
                                            <td>{emp.empPhoneNumber}</td>
                                            <td>{emp.department?.deptName}</td>
                                            <td>
                                                {/* Status toggle switch */}
                                                <div className="form-check form-switch">
                                                    <input
                                                        className="form-check-input"
                                                        type="checkbox"
                                                        checked={emp.isActive}
                                                        onChange={() => handleToggle(emp.empId, emp.isActive)}
                                                        style={{
                                                            backgroundColor: emp.isActive ? "#28a745" : "#dc3545",
                                                            width: "2.5rem",
                                                            height: "1.4rem",
                                                        }}
                                                    />
                                                </div>
                                            </td>
                                            <td>
                                                {/* Edit button */}
                                                <Link 
                                                    to={`/edit/${emp.empId}`} 
                                                    className="btn btn-primary btn-sm me-2"
                                                >
                                                    <i className="bi bi-pencil"></i> Edit
                                                </Link>
                                            </td>
                                        </tr>
                                    ))}
                                </tbody>
                            </table>
                        </div>

                        {/* Empty state message */}
                        {filteredEmployees.length === 0 && !isLoading && (
                            <div className="text-center my-4 text-muted">
                                No employees found {search && `matching "${search}"`}
                            </div>
                        )}

                        {/* Pagination component */}
                        <ReactPaginate
                            previousLabel={<i className="bi bi-chevron-left"></i>}
                            nextLabel={<i className="bi bi-chevron-right"></i>}
                            pageCount={pageCount}
                            onPageChange={changePage}
                            containerClassName="pagination justify-content-center mt-4"
                            activeClassName="active"
                            pageClassName="page-item"
                            pageLinkClassName="page-link"
                            previousClassName="page-item"
                            nextClassName="page-item"
                            previousLinkClassName="page-link"
                            nextLinkClassName="page-link"
                            breakLabel="..."
                            forcePage={pageNumber}
                        />
                    </>
                )}
            </div>
        </div>
    );
};

export default EmployeeList;

📦 5. Models (for strong typing)

📄 File: src/models/Employee.ts


import { Department } from "./Department";

export interface Employee {
    empId: number;
    empName: string;
    designation: string;
    dateOfJoining: string;
    salary: number;
    empPhoneNumber: string;
    deptId: number;
    isActive: boolean;
    department?: Department;
}

📄 File: src/models/Department.ts


export interface Department {
    deptId: number;
    deptName: string;
}


No comments:

Post a Comment