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:
- User logs in with email and password
- Backend returns JWT token and user information
- Token is stored in Zustand store with persistence
- For new users or password resets, the system requires OTP setup
- OTP verification is required for sensitive operations
- API requests include token in Authorization header via interceptors
- Token refresh is handled automatically when tokens expire
- Protected routes check for valid token and redirect unauthenticated users
- Role-based permissions control access to features and UI elements
- 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 |
|---|
| // 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:
- Feature-Based Architecture: Code organized by business domain rather than technical concerns
- Component Composition: Building complex UIs from reusable, composable components
- Separation of Concerns: Clear separation between UI components, state management, and API integration
- Permission-Based Access Control: Fine-grained control over feature access based on user roles
- Consistent Error Handling: Standardized approach to error handling across the application
- Lazy Loading: Code splitting to optimize initial load performance
- Responsive Design: Tailwind CSS utilities for responsive layouts
- Centralized Configuration: Environment-specific configuration in a central location
Workflow Implementation
The PRS Frontend implements several business workflows:
- Requisition Management:
- Creating and editing requisition slips
- Approval workflows with multiple approvers
-
Status tracking and history
-
Canvassing Process:
- Supplier quote management
-
Comparison and selection
-
Purchase Order Management:
- Creation from approved requisitions
- Approval workflow
-
Document generation
-
Delivery Receipt Management:
- Recording deliveries
- Partial and complete deliveries
-
Returns handling
-
Payment Processing:
- Invoice management
- Payment request creation
-
Approval workflow
-
Master Data Management:
- User management
- Supplier management
- Company and department management
- Project management