Skip to content

PRS Frontend Architecture Overview

This document provides an overview of the PRS Frontend architecture, including its components, technologies, and design principles based on the actual implementation.

System Architecture

The PRS Frontend is built using React and follows a feature-based architecture with code splitting for optimized loading. It uses a combination of modern libraries and patterns to create a maintainable and performant application.

1. Core Application Structure

  • Built with React 18 and Vite for fast development and optimized builds
  • Uses React Router 6 for navigation with lazy-loaded routes
  • Implements code splitting for optimized loading
  • Centralized state management with Zustand
  • Error handling with React Error Boundary

Key Components: - src/app/: Contains the main application setup - src/app/provider.jsx: Sets up application providers including React Query, Error Boundary, and authentication - src/app/router.jsx: Defines routing structure with permission-based access control - src/app/routes/: Contains route components organized by feature

2. Feature-Based Organization

The codebase is organized by features rather than technical concerns, allowing for better separation of concerns and modularity. Each feature encapsulates its own components, API integration, and business logic.

Key Components: - src/features/: Contains feature-specific code - Each feature folder (e.g., dashboard, user, supplier, canvass, etc.) contains: - api/: API integration for the feature with React Query hooks - components/: React components specific to the feature

Actual Features Implemented: - dashboard: Requisition management and overview - user: User management and profiles - supplier: Supplier management - company: Company management - department: Department management - project: Project management - canvass: Canvassing process - purchase-order: Purchase order management - delivery-receipt: Delivery receipt management - invoice: Invoice management - payment-request: Payment request management - ofm: OFM (Official Form for Materials) management - non-ofm: Non-OFM items management - notification: System notifications - attachments: Document attachments - auth: Authentication and authorization

3. API Integration Layer

  • Uses Axios for HTTP requests with comprehensive error handling
  • Implements React Query (Tanstack Query) for data fetching, caching, and state management
  • Centralized API client with interceptors for authentication, token refresh, and error handling
  • Standardized error types and user-friendly error messages
  • Automatic retry logic for network errors

Key Components: - src/lib/apiClient.js: Configures Axios with interceptors and error handling - src/lib/reactQuery.js: Configures React Query defaults - Feature-specific API hooks in src/features/*/api/

Actual Implementation:

JavaScript
// Example from src/lib/apiClient.js
export const API_ERROR_TYPES = {
  UNAUTHORIZED: 'UNAUTHORIZED',
  FORBIDDEN: 'FORBIDDEN',
  NOT_FOUND: 'NOT_FOUND',
  VALIDATION: 'VALIDATION',
  SERVER_ERROR: 'SERVER_ERROR',
  NETWORK_ERROR: 'NETWORK_ERROR',
  TIMEOUT: 'TIMEOUT',
  UNKNOWN: 'UNKNOWN',
};

// API hooks pattern from features
export const getRequisitions = ({ page = 1, limit = 10, sortBy, filterBy, requestType }) => {
  return api.get('/v2/requisitions', {
    params: {
      page,
      limit,
      filterBy: convertObjectToString(removeEmptyStringFromObject(filterBy)),
      ...formatSortParams(sortBy),
      requestType,
    },
  });
};

export const useGetRequisitions = (
  { page, limit, sortBy, filterBy, requestType },
  config = {},
) => {
  return useQuery({
    queryKey: ['requisitions', { page, limit, sortBy, filterBy, requestType }],
    queryFn: () => getRequisitions({ page, limit, sortBy, filterBy, requestType }),
    keepPreviousData: true,
    ...config,
  });
};

4. UI Component Library

  • Custom UI components built with Tailwind CSS
  • Component composition pattern for reusability
  • Form handling with React Hook Form and Zod validation
  • Comprehensive set of UI components for consistent user experience

Key Components: - src/components/ui/: Contains reusable UI components - Button: Various button styles with icon support - Table: Data table with sorting, pagination, and custom cell rendering - Form: Form components with validation - Modal: Modal dialog components - Tabs: Tab navigation components - SearchBar: Search interface with filters - Dropdown: Dropdown menus and selects - Accordion: Expandable content sections - And many more specialized components - src/components/layouts/: Contains layout components - src/components/errors/: Contains error handling components

Actual Implementation:

JSX
// Example from Dashboard.jsx
<div className="flex flex-col gap-4">
  <SearchBar
    searchItems={searchItems}
    onSearch={val => setSearchQuery(val)}
    subClassName="lg:grid-cols-6"
    setPage={setPage}
  />

  <Tabs tabs={tabs} />
  <div
    className={cn('block sm:flex items-center justify-end', {
      'justify-between': createPermission,
    })}
  >
    {createPermission && (
      <Button
        onClick={() => {
          clearRequisitionItemsStore();
          navigate('create');
        }}
        variant="submit"
        className="w-auto px-8"
        icon={NewDocument}
      >
        New Request
      </Button>
    )}

    <ActionButtons />
  </div>
  <Pagination
    total={requisitions?.total}
    setPage={setPage}
    setLimit={setLimit}
    hasLimit={true}
  >
    <Table
      headers={headers}
      data={requisitions?.data}
      tdDesign={tdDesign}
      isLoading={isLoading}
      onSort={setSort}
      currentSort={currentSort}
      infoDescription="Create a new request to add content"
    />
  </Pagination>
</div>

5. State Management

  • Uses Zustand for global state management with persistence
  • React Query for server state management
  • Local component state for UI-specific state

Key Components: - src/store/: Contains Zustand store definitions - authStore.js: Authentication token management - userStore.js: User information and preferences - permissionStore.js: User permissions - requisitionItemsStore.js: Requisition items state - canvassItemsStore.js: Canvass items state - rsTabStore.js: Tab state for requisition slips - And other feature-specific stores

Actual Implementation:

JavaScript
// Example from authStore.js
const useTokenStore = create(
  persist(
    (set, get) => ({
      token: null,
      type: null,
      refreshToken: null,
      expiredAt: null,
      setToken: (token, type = null) => set({ token, type }),
      setRefreshToken: ({ accessToken, refreshToken, expiredAt }) => {
        const currentAccessToken = get().token;

        set({
          token: accessToken || currentAccessToken,
          refreshToken,
          expiredAt,
        });
      },
      setType: type => set({ type }),
      removeToken: () =>
        set({ token: null, type: null, refreshToken: null, expiredAt: null }),
    }),
    {
      name: 'auth-storage', // name of the item in the storage (must be unique)
    },
  ),
);

6. Authentication and Authorization

  • JWT-based authentication with token refresh
  • Role-based access control with fine-grained permissions
  • Protected routes with permission checks
  • OTP (One-Time Password) authentication flow

Key Components: - src/lib/auth.jsx: Authentication utilities and hooks - src/utils/permissions.js: Permission utilities - src/app/router.jsx: Permission-based route rendering

Actual Implementation:

JSX
// Example from router.jsx - Permission-based routing
const hasPermission = (route, permission) => {
  if (!permission) {
    return {};
  }
  return route;
};

// Usage in routes
hasPermission(
  {
    path: 'dashboard',
    children: [
      {
        path: '',
        lazy: async () => {
          const { DashboardRoute } = await import(
            './routes/app/dashboard'
          );
          return { Component: DashboardRoute };
        },
      },
      // More routes...
    ],
  },
  dashboardView, // Permission check
),

// Protected route wrapper
<ProtectedRoute>
  <AppRoot />
</ProtectedRoute>

Project Structure

The actual project structure of the PRS Frontend codebase:

Text Only
src/
├── app/                  # Application setup
│   ├── provider.jsx      # App providers
│   ├── router.jsx        # Routing configuration
│   └── routes/           # Route components
│       ├── app/          # Main application routes
│       │   ├── dashboard/
│       │   ├── user/
│       │   ├── supplier/
│       │   ├── canvass/
│       │   ├── purchase-order/
│       │   ├── delivery-receipt/
│       │   ├── invoice/
│       │   ├── payment-request/
│       │   ├── ofm/
│       │   ├── non-ofm/
│       │   ├── notification/
│       │   └── ...
│       ├── auth/         # Authentication routes
│       └── ...
├── assets/               # Static assets
│   ├── icons/            # SVG icons
│   ├── images/           # Image assets
│   └── markdown/         # Markdown content
├── components/           # Shared components
│   ├── errors/           # Error components
│   ├── layouts/          # Layout components
│   └── ui/               # UI components
│       ├── Accordion/
│       ├── Button/
│       ├── Dropdown/
│       ├── Form/
│       ├── Modal/
│       ├── SearchBar/
│       ├── Table/
│       ├── Tabs/
│       └── ...
├── config/               # Configuration files
├── features/             # Feature modules
│   ├── dashboard/        # Dashboard feature
│   │   ├── api/          # API integration
│   │   └── components/   # Feature components
│   ├── user/
│   ├── supplier/
│   ├── company/
│   ├── department/
│   ├── project/
│   ├── canvass/
│   ├── purchase-order/
│   ├── delivery-receipt/
│   ├── invoice/
│   ├── payment-request/
│   ├── ofm/
│   ├── non-ofm/
│   ├── notification/
│   ├── attachments/
│   └── auth/
├── hoc/                  # Higher-order components
├── hooks/                # Custom hooks
├── lib/                  # Core libraries
│   ├── apiClient.js      # API client
│   ├── auth.jsx          # Authentication
│   └── reactQuery.js     # React Query config
├── schema/               # Validation schemas
├── store/                # Zustand stores
│   ├── authStore.js
│   ├── userStore.js
│   ├── permissionStore.js
│   ├── requisitionItemsStore.js
│   ├── canvassItemsStore.js
│   ├── rsTabStore.js
│   └── ...
└── utils/                # Utility functions
    ├── cn.js             # Class name utility
    ├── permissions.js    # Permission utilities
    ├── query.js          # Query parameter utilities
    ├── stringCleanup.js  # String manipulation utilities
    └── ...

Key Technologies

Based on the actual package.json and codebase:

  • React 18: UI library
  • Vite: Build tool and development server
  • React Router 6: Routing with lazy loading
  • Tanstack React Query: Data fetching, caching, and server state management
  • Zustand: State management with persistence
  • Axios: HTTP client with interceptors
  • React Hook Form: Form handling and validation
  • Zod: Schema validation
  • Tailwind CSS: Utility-first CSS framework
  • React Error Boundary: Error handling and fallbacks
  • React Toastify: Toast notifications
  • Prop Types: Runtime type checking
  • XLSX: Excel file generation
  • QRCode.react: QR code generation
  • class-variance-authority: Component styling variants
  • tailwind-merge: Tailwind class merging utility

API Integration

The frontend integrates with the backend API through a centralized API client based on Axios with React Query for data management:

  • Standardized error handling with defined error types and user-friendly messages
  • Automatic token management for authentication with refresh token support
  • Request/response interceptors for consistent handling
  • Automatic retry logic for network errors with exponential backoff
  • Centralized error logging and formatting

The actual implementation from the codebase:

JavaScript
// Error types from apiClient.js
export const API_ERROR_TYPES = {
  UNAUTHORIZED: 'UNAUTHORIZED',
  FORBIDDEN: 'FORBIDDEN',
  NOT_FOUND: 'NOT_FOUND',
  VALIDATION: 'VALIDATION',
  SERVER_ERROR: 'SERVER_ERROR',
  NETWORK_ERROR: 'NETWORK_ERROR',
  TIMEOUT: 'TIMEOUT',
  UNKNOWN: 'UNKNOWN',
};

// Error handling from apiClient.js
api.interceptors.response.use(
  response => {
    return response.data;
  },
  async error => {
    const { setState } = useTokenStore;
    const { setState: setUserState } = useUserStore;
    const { setState: setPermissionState } = usePermissionStore;

    // Format the error for consistent handling
    const formattedError = formatApiError(error);

    // Handle unauthorized errors (401)
    if (formattedError.type === API_ERROR_TYPES.UNAUTHORIZED) {
      setState({
        token: null,
        type: null,
        refreshToken: null,
        expiredAt: null,
      });
      setUserState({ user: null, otp: null, secret: null, currentRoute: null });
      setPermissionState({ permissions: null });
      sessionStorage.removeItem('timeoutState');
    }
    // Handle network errors with retry logic
    else if (
      formattedError.type === API_ERROR_TYPES.NETWORK_ERROR ||
      formattedError.type === API_ERROR_TYPES.TIMEOUT
    ) {
      const config = error.config;

      // Add retry count to config if it doesn't exist
      if (!config.retryCount) {
        config.retryCount = 0;
      }

      // Retry the request if we haven't reached the maximum retries
      if (config.retryCount < MAX_RETRIES) {
        config.retryCount += 1;

        // Wait before retrying
        const delay = getRetryDelay(config.retryCount);
        await new Promise(resolve => setTimeout(resolve, delay));

        console.log(`Retrying request (${config.retryCount}/${MAX_RETRIES})...`);
        return api(config);
      }
    }

    return Promise.reject(error);
  },
);

// Example API hook from dashboard/api/get-requisitions.js
export const getRequisitions = ({ page = 1, limit = 10, sortBy, filterBy, requestType }) => {
  return api.get('/v2/requisitions', {
    params: {
      page,
      limit,
      filterBy: convertObjectToString(removeEmptyStringFromObject(filterBy)),
      ...formatSortParams(sortBy),
      requestType,
    },
  });
};

export const useGetRequisitions = (
  { page, limit, sortBy, filterBy, requestType },
  config = {},
) => {
  return useQuery({
    queryKey: ['requisitions', { page, limit, sortBy, filterBy, requestType }],
    queryFn: () => getRequisitions({ page, limit, sortBy, filterBy, requestType }),
    keepPreviousData: true,
    ...config,
  });
};

Routing and Code Splitting

The application uses React Router 6 with code splitting to optimize loading times. Routes are organized by feature and loaded lazily to improve initial load performance. The router also implements permission-based access control.

Actual implementation from the codebase:

JavaScript
// From router.jsx - Lazy loading pattern
{
  path: 'dashboard',
  lazy: async () => {
    const { DashboardRoute } = await import('./routes/app/dashboard');
    return { Component: DashboardRoute };
  },
}

// Permission-based routing
hasPermission(
  {
    path: 'purchase-order',
    children: [
      {
        path: ':id/requisition-slip/:rsId',
        lazy: async () => {
          const { PurchaseOrderLandingRoute } = await import(
            './routes/app/purchase-order'
          );
          return { Component: PurchaseOrderLandingRoute };
        },
      },
    ],
  },
  ordersView, // Permission check
),

// Main router creation
export const createAppRouter = (queryClient, availableRoutes) =>
  createBrowserRouter([
    {
      path: '/',
      lazy: async () => {
        const { LandingRoute } = await import('./routes/Landing');
        return { Component: LandingRoute };
      },
    },
    {
      path: 'login',
      lazy: async () => {
        const { LoginRoute } = await import('./routes/auth');
        return { Component: LoginRoute };
      },
    },
    {
      path: '/auth',
      element: (
        <AuthFlowWrapper>
          <AppRoot />
        </AuthFlowWrapper>
      ),
      children: [
        // Auth routes...
      ],
    },
    {
      path: '/app',
      element: (
        <ProtectedRoute>
          <AppRoot />
        </ProtectedRoute>
      ),
      children: [...availableRoutes],
    },
    {
      path: '*',
      lazy: async () => {
        const { NotFoundRoute } = await import('./routes/NotFound');
        return { Component: NotFoundRoute };
      },
    },
  ]);

Authentication Flow

The PRS Frontend implements a comprehensive authentication flow with JWT tokens and OTP (One-Time Password) verification:

  1. User logs in with email and password
  2. Backend returns JWT token and user information
  3. Token is stored in Zustand store with persistence
  4. For new users or password resets, the system requires OTP setup
  5. OTP verification is required for sensitive operations
  6. API requests include token in Authorization header via interceptors
  7. Token refresh is handled automatically when tokens expire
  8. Protected routes check for valid token and redirect unauthenticated users
  9. Role-based permissions control access to features and UI elements
  10. Unauthorized access attempts trigger automatic logout

The authentication state is managed through multiple Zustand stores: - authStore.js: Manages authentication tokens - userStore.js: Stores user information and preferences - permissionStore.js: Manages user permissions

The ProtectedRoute component ensures that only authenticated users can access protected routes:

JSX
1
2
3
4
// Protected route wrapper
<ProtectedRoute>
  <AppRoot />
</ProtectedRoute>

Error Handling

The PRS Frontend implements a comprehensive error handling strategy:

  • Global error boundary catches unhandled errors with fallback UI
  • API error handling with standardized error types and consistent formatting
  • User-friendly error messages through toast notifications
  • Automatic retry logic for network errors with exponential backoff
  • Centralized error logging for debugging
  • Form validation errors with clear user feedback
  • Authentication error handling with automatic logout
  • Consistent error handling patterns across features

The error handling is implemented through multiple layers:

JSX
// From provider.jsx - Global error boundary
<ErrorBoundary FallbackComponent={MainErrorFallback}>
  <HelmetProvider>
    <QueryClientProvider client={queryClient}>
      {import.meta.env.DEV && <ReactQueryDevtools />}
      <AuthLoader
        renderLoading={() => (
          <div className="flex h-screen w-screen items-center justify-center">
            <Spinner size="xl" />
          </div>
        )}
      >
        {children}
        <ToastContainer
          theme="colored"
          transition:Bounce
          className="w-96"
        />
      </AuthLoader>
    </QueryClientProvider>
  </HelmetProvider>
</ErrorBoundary>

// From apiClient.js - API error handling
export const formatApiError = (error) => {
  let errorType = API_ERROR_TYPES.UNKNOWN;
  let errorMessage = ERROR_MESSAGES[API_ERROR_TYPES.UNKNOWN];
  let statusCode = null;
  let data = null;

  // Network errors
  if (error.message === 'Network Error') {
    errorType = API_ERROR_TYPES.NETWORK_ERROR;
    errorMessage = ERROR_MESSAGES[API_ERROR_TYPES.NETWORK_ERROR];
  }
  // Timeout errors
  else if (error.code === 'ECONNABORTED') {
    errorType = API_ERROR_TYPES.TIMEOUT;
    errorMessage = ERROR_MESSAGES[API_ERROR_TYPES.TIMEOUT];
  }
  // Response errors
  else if (error.response) {
    statusCode = error.response.status;
    data = error.response.data;

    // Map status code to error type
    errorType = ERROR_STATUS_MAP[statusCode] || API_ERROR_TYPES.UNKNOWN;

    // Use server-provided message if available
    if (data && data.message) {
      errorMessage = data.message;
    } else {
      errorMessage = ERROR_MESSAGES[errorType];
    }
  }

  return {
    type: errorType,
    message: errorMessage,
    statusCode,
    data,
    originalError: error,
  };
};

Design Principles

Based on the actual codebase implementation:

  1. Feature-Based Architecture: Code organized by business domain rather than technical concerns
  2. Component Composition: Building complex UIs from reusable, composable components
  3. Separation of Concerns: Clear separation between UI components, state management, and API integration
  4. Permission-Based Access Control: Fine-grained control over feature access based on user roles
  5. Consistent Error Handling: Standardized approach to error handling across the application
  6. Lazy Loading: Code splitting to optimize initial load performance
  7. Responsive Design: Tailwind CSS utilities for responsive layouts
  8. Centralized Configuration: Environment-specific configuration in a central location

Workflow Implementation

The PRS Frontend implements several business workflows:

  1. Requisition Management:
  2. Creating and editing requisition slips
  3. Approval workflows with multiple approvers
  4. Status tracking and history

  5. Canvassing Process:

  6. Supplier quote management
  7. Comparison and selection

  8. Purchase Order Management:

  9. Creation from approved requisitions
  10. Approval workflow
  11. Document generation

  12. Delivery Receipt Management:

  13. Recording deliveries
  14. Partial and complete deliveries
  15. Returns handling

  16. Payment Processing:

  17. Invoice management
  18. Payment request creation
  19. Approval workflow

  20. Master Data Management:

  21. User management
  22. Supplier management
  23. Company and department management
  24. Project management