Getting Started with PRS Frontend Development
This guide will help you set up your development environment and understand the basics of working with the PRS Frontend codebase based on the actual implementation.
Prerequisites
Before you begin, make sure you have the following installed:
- Node.js (v16 or later)
- npm (v8 or later)
- Git
- A modern code editor (VS Code recommended)
- ESLint and Prettier extensions for your editor
Setting Up the Development Environment
1. Clone the Repository
| Bash |
|---|
| git clone <repository-url>
cd prs-frontend
|
2. Install Dependencies
Using npm:
Or using yarn:
3. Set Up Environment Variables
Copy the example environment file and update it with your local configuration:
| Bash |
|---|
| cp .env.example .env.local
|
Edit the .env.local file with your API endpoint and other configuration options:
| Text Only |
|---|
| VITE_API_BASE_URL=http://localhost:3000/api
VITE_APP_ENV=development
|
4. Start the Development Server
Using npm:
Or using yarn:
The development server will start on http://localhost:5173 by default.
Project Structure
The PRS Frontend follows a feature-based architecture. Here's the actual structure of the 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/
│ │ └── ...
│ ├── 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 Concepts
1. Feature-Based Organization
The codebase is organized by features rather than technical concerns. Each feature folder contains everything related to that feature, including components, API integration, and utilities.
Here's an actual example from the codebase:
| JavaScript |
|---|
| // src/features/dashboard/api/get-requisitions.js
import { useQuery } from '@tanstack/react-query';
import { api } from '@lib/apiClient';
import { formatSortParams } from '@utils/query';
import {
convertObjectToString,
removeEmptyStringFromObject,
} from '@utils/stringCleanup';
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,
});
};
|
2. Component Composition
UI components are built using composition, with smaller components combined to create more complex interfaces.
Here's an actual example from the codebase:
| JSX |
|---|
| // 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>
|
The Table component is a good example of composition, with custom cell rendering:
| JSX |
|---|
| // Table cell rendering configuration from Dashboard.jsx
const tdDesign = {
rsNumber: {
render: ({ combine_rs_number, id, status }) =>
status === 'draft' ? (
<Link
to={`/app/dashboard/create/${id}`}
className="text-blue-500 underline underline-offset-4"
>
{combine_rs_number}
</Link>
) : (
<Link
to={`/app/requisition-slip/${id}`}
className="text-blue-500 underline underline-offset-4"
>
{combine_rs_number}
</Link>
),
},
status: {
css: 'min-w-48',
render: Status, // Status component for rendering status pills
},
// Other cell renderers...
};
|
3. Data Fetching with React Query
React Query (Tanstack Query) is used for data fetching, caching, and state management. The application uses a consistent pattern for API integration.
Here's an actual example from the codebase:
| JSX |
|---|
| // From Dashboard.jsx - Using React Query hooks
const { data: requisitions, isLoading } = useGetRequisitions(
{
page: currentPage,
limit: currentLimit,
sortBy,
filterBy: searchQuery,
requestType: requestType,
},
{ enabled: getPermission },
);
const { data: users } = useGetUsers({
page: 1,
paginate: false,
includeCurrentUser: true,
sortBy: { firstName: 'ASC' },
});
const { data: companies } = useGetCompanies({
page: 1,
paginate: false,
sortBy: { name: 'ASC' },
});
// Using the data in the component
return (
<div className="flex flex-col gap-4">
{/* ... */}
<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>
);
|
The React Query configuration is centralized in src/lib/reactQuery.js to ensure consistent behavior across the application.
4. State Management with Zustand
Zustand is used for global state management with persistence. The application uses multiple stores for different concerns.
Here's an actual example from the codebase:
| JavaScript |
|---|
| // 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)
},
),
);
// From requisitionItemsStore.js - Store for managing requisition items
const useRequisitionItemsStore = create(set => ({
items: [],
setItems: items => set({ items }),
addItem: item => set(state => ({ items: [...state.items, item] })),
updateItem: (index, item) =>
set(state => ({
items: state.items.map((i, idx) => (idx === index ? item : i)),
})),
removeItem: index =>
set(state => ({
items: state.items.filter((_, idx) => idx !== index),
})),
clearItems: () => set({ items: [] }),
}));
|
Forms are built using React Hook Form with validation. The application uses custom form components that integrate with React Hook Form.
Here's an example of form handling from the codebase:
| JSX |
|---|
| // From a form component in the codebase
const {
register,
handleSubmit,
formState: { errors },
control,
setValue,
watch,
} = useForm({
defaultValues: {
requestType: 'GOODS',
requestDate: new Date(),
requestedBy: user?.id,
companyId: user?.company?.id,
departmentId: user?.department?.id,
projectId: '',
purpose: '',
remarks: '',
},
});
// Form submission handler
const onSubmit = data => {
const payload = {
...data,
requestDate: format(new Date(data.requestDate), 'yyyy-MM-dd'),
items: requisitionItems,
};
createRequisition(payload, {
onSuccess: data => {
toast.success('Requisition created successfully');
navigate(`/app/requisition-slip/${data.id}`);
},
});
};
// Form rendering
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
<FormInput
label="Request Type"
name="requestType"
register={register}
errors={errors}
type="select"
options={[
{ value: 'GOODS', label: 'Goods' },
{ value: 'SERVICES', label: 'Services' },
]}
required
/>
<FormInput
label="Request Date"
name="requestDate"
register={register}
errors={errors}
type="date"
required
/>
<FormInput
label="Requested By"
name="requestedBy"
register={register}
errors={errors}
type="select"
options={userOptions}
required
/>
{/* More form fields... */}
</div>
<div className="mt-4 flex justify-end gap-2">
<Button
type="button"
variant="secondary"
onClick={() => navigate('/app/dashboard')}
>
Cancel
</Button>
<Button type="submit" variant="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</Button>
</div>
</form>
);
|
Common Development Tasks
1. Creating a New Feature
Based on the actual codebase patterns, here's how to create a new feature:
- Create a new folder in
src/features
- Add API integration files in the
api subfolder
- Add components in the
components subfolder
- Create a route component in
src/app/routes/app/
- Add the feature to the router with permission checks
Example based on the actual codebase patterns:
| JSX |
|---|
| // 1. Create API integration
// src/features/supplier/api/get-suppliers.js
import { useQuery } from '@tanstack/react-query';
import { api } from '@lib/apiClient';
import { formatSortParams } from '@utils/query';
import {
convertObjectToString,
removeEmptyStringFromObject,
} from '@utils/stringCleanup';
export const getSuppliers = ({ page = 1, limit = 10, sortBy, filterBy, paginate = true }) => {
return api.get('/v2/suppliers', {
params: {
page,
limit,
paginate,
filterBy: convertObjectToString(removeEmptyStringFromObject(filterBy)),
...formatSortParams(sortBy),
},
});
};
export const useGetSuppliers = (
{ page, limit, sortBy, filterBy, paginate },
config = {},
) => {
return useQuery({
queryKey: ['suppliers', { page, limit, sortBy, filterBy, paginate }],
queryFn: () => getSuppliers({ page, limit, sortBy, filterBy, paginate }),
keepPreviousData: true,
...config,
});
};
// 2. Create components
// src/features/supplier/components/SupplierList.jsx
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useGetSuppliers } from '../api/get-suppliers';
import { Table, SearchBar, Pagination, Button } from '@components/ui';
import { usePermission } from '@hooks/usePermission';
import { supplierCreate } from '@utils/permissions';
export const SupplierList = () => {
const navigate = useNavigate();
const [currentPage, setPage] = useState(1);
const [currentLimit, setLimit] = useState(10);
const [sortBy, setSort] = useState({ name: 'ASC' });
const [searchQuery, setSearchQuery] = useState({});
const createPermission = usePermission(supplierCreate);
const { data: suppliers, isLoading } = useGetSuppliers({
page: currentPage,
limit: currentLimit,
sortBy,
filterBy: searchQuery,
});
const headers = [
{ key: 'name', label: 'Name' },
{ key: 'contactPerson', label: 'Contact Person' },
{ key: 'contactNumber', label: 'Contact Number' },
{ key: 'email', label: 'Email' },
{ key: 'status', label: 'Status' },
];
const tdDesign = {
name: {
render: ({ name, id }) => (
<a
href={`/app/supplier/${id}`}
className="text-blue-500 underline underline-offset-4"
>
{name}
</a>
),
},
status: {
render: ({ status }) => (
<span className={`badge ${status === 'ACTIVE' ? 'bg-green-500' : 'bg-red-500'}`}>
{status}
</span>
),
},
};
const searchItems = [
{ key: 'name', label: 'Name', type: 'text' },
{ key: 'contactPerson', label: 'Contact Person', type: 'text' },
{ key: 'email', label: 'Email', type: 'text' },
{ key: 'status', label: 'Status', type: 'select', options: [
{ value: 'ACTIVE', label: 'Active' },
{ value: 'INACTIVE', label: 'Inactive' },
]},
];
return (
<div className="flex flex-col gap-4">
<SearchBar
searchItems={searchItems}
onSearch={val => setSearchQuery(val)}
setPage={setPage}
/>
<div className="flex justify-between">
<h1 className="text-2xl font-bold">Suppliers</h1>
{createPermission && (
<Button
onClick={() => navigate('create')}
variant="submit"
>
Add Supplier
</Button>
)}
</div>
<Pagination
total={suppliers?.total}
setPage={setPage}
setLimit={setLimit}
hasLimit={true}
>
<Table
headers={headers}
data={suppliers?.data}
tdDesign={tdDesign}
isLoading={isLoading}
onSort={setSort}
currentSort={sortBy}
/>
</Pagination>
</div>
);
};
// 3. Create route component
// src/app/routes/app/supplier.jsx
import { SupplierList } from '@features/supplier/components/SupplierList';
export const SupplierRoute = () => {
return <SupplierList />;
};
// 4. Add to router with permission check
// src/app/router.jsx
hasPermission(
{
path: 'supplier',
children: [
{
path: '',
lazy: async () => {
const { SupplierRoute } = await import('./routes/app/supplier');
return { Component: SupplierRoute };
},
},
{
path: 'create',
lazy: async () => {
const { SupplierCreateRoute } = await import('./routes/app/supplier');
return { Component: SupplierCreateRoute };
},
},
{
path: ':id',
lazy: async () => {
const { SupplierDetailRoute } = await import('./routes/app/supplier');
return { Component: SupplierDetailRoute };
},
},
],
},
supplierView, // Permission check
);
|
2. Adding a New API Integration
Based on the actual codebase patterns, here's how to add a new API integration:
- Create a new file in the feature's
api folder
- Define the API function for the HTTP request
- Create a React Query hook (useQuery for GET, useMutation for POST/PUT/DELETE)
- Include proper error handling and cache invalidation
Example based on the actual codebase patterns:
| JavaScript |
|---|
| // src/features/requisition/api/approve-requisition.js
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@lib/apiClient';
import { toast } from 'react-toastify';
export const approveRequisition = ({ id, remarks }) => {
return api.post(`/v2/requisitions/${id}/approve`, { remarks });
};
export const useApproveRequisition = (config = {}) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: approveRequisition,
onSuccess: (data, variables) => {
// Invalidate relevant queries to refresh data
queryClient.invalidateQueries(['requisitions']);
queryClient.invalidateQueries(['requisition', variables.id]);
// Show success message
toast.success('Requisition approved successfully');
},
onError: (error) => {
// Show error message
toast.error(error.message || 'Failed to approve requisition');
},
...config,
});
};
// Usage example
const { mutate: approveRequisition, isLoading: isApproving } = useApproveRequisition();
const handleApprove = () => {
approveRequisition(
{ id: requisitionId, remarks: 'Approved' },
{
onSuccess: () => {
// Additional success handling if needed
navigate('/app/dashboard');
},
}
);
};
|
3. Creating a New UI Component
Based on the actual codebase patterns, here's how to create a new UI component:
- Create a new folder in
src/components/ui for the component
- Create an index.jsx file for the main component
- Use Tailwind CSS for styling with the
cn utility for class merging
- Use composition pattern for complex components
- Include proper prop validation
Example based on the actual codebase patterns:
| JSX |
|---|
| // src/components/ui/Button/index.jsx
import { forwardRef } from 'react';
import PropTypes from 'prop-types';
import { cva } from 'class-variance-authority';
import { cn } from '@utils/cn';
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none',
{
variants: {
variant: {
default: 'bg-gray-900 text-white hover:bg-gray-700 focus:ring-gray-900',
submit: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-600',
destructive: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-600',
outline: 'bg-transparent border border-gray-200 hover:bg-gray-100 focus:ring-gray-900',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200 focus:ring-gray-900',
ghost: 'bg-transparent hover:bg-gray-100 focus:ring-gray-900',
link: 'bg-transparent underline-offset-4 hover:underline text-blue-600 hover:bg-transparent focus:ring-0',
},
size: {
default: 'h-10 py-2 px-4',
sm: 'h-8 px-2 rounded-md',
lg: 'h-12 px-8 rounded-md',
icon: 'h-10 w-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);
const Button = forwardRef(
({ className, variant, size, icon: Icon, children, ...props }, ref) => {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
>
{Icon && <Icon className="mr-2 h-4 w-4" />}
{children}
</button>
);
},
);
Button.displayName = 'Button';
Button.propTypes = {
className: PropTypes.string,
variant: PropTypes.oneOf([
'default',
'submit',
'destructive',
'outline',
'secondary',
'ghost',
'link',
]),
size: PropTypes.oneOf(['default', 'sm', 'lg', 'icon']),
icon: PropTypes.elementType,
children: PropTypes.node,
};
export { Button, buttonVariants };
// Usage example
import { Button } from '@components/ui/Button';
import { PlusIcon } from '@assets/icons';
<Button variant="submit" size="lg" icon={PlusIcon}>
Create New
</Button>
|
For composite components like Table, the pattern is to create subcomponents:
| JSX |
|---|
| // src/components/ui/Table/index.jsx
import { forwardRef } from 'react';
import PropTypes from 'prop-types';
import { cn } from '@utils/cn';
import { Spinner } from '@components/ui/Spinner';
import { EmptyState } from '@components/ui/EmptyState';
const Table = forwardRef(
(
{
className,
headers,
data,
tdDesign,
isLoading,
onSort,
currentSort,
infoDescription,
...props
},
ref,
) => {
// Sort handling logic
const handleSort = key => {
if (!onSort) return;
const newSort = { ...currentSort };
if (newSort[key]) {
if (newSort[key] === 'ASC') {
newSort[key] = 'DESC';
} else {
delete newSort[key];
}
} else {
Object.keys(newSort).forEach(k => delete newSort[k]);
newSort[key] = 'ASC';
}
onSort(newSort);
};
// Render loading state
if (isLoading) {
return (
<div className="flex h-60 items-center justify-center">
<Spinner size="lg" />
</div>
);
}
// Render empty state
if (!data || data.length === 0) {
return (
<EmptyState
title="No data available"
description={infoDescription || 'No data to display'}
/>
);
}
return (
<div className="w-full overflow-auto">
<table
className={cn('w-full border-collapse text-sm', className)}
ref={ref}
{...props}
>
<thead>
<tr className="border-b bg-gray-50">
{headers.map(header => (
<th
key={header.key}
className={cn(
'px-4 py-3 text-left font-medium text-gray-500',
header.sortable !== false && 'cursor-pointer',
)}
onClick={() => header.sortable !== false && handleSort(header.key)}
>
<div className="flex items-center">
{header.label}
{currentSort && currentSort[header.key] && (
<span className="ml-1">
{currentSort[header.key] === 'ASC' ? '↑' : '↓'}
</span>
)}
</div>
</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, rowIndex) => (
<tr
key={row.id || rowIndex}
className="border-b hover:bg-gray-50"
>
{headers.map(header => {
const design = tdDesign?.[header.key];
return (
<td
key={`${row.id || rowIndex}-${header.key}`}
className={cn('px-4 py-3', design?.css)}
>
{design?.render
? design.render(row)
: row[header.key] || '-'}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
);
},
);
Table.displayName = 'Table';
Table.propTypes = {
className: PropTypes.string,
headers: PropTypes.arrayOf(
PropTypes.shape({
key: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
sortable: PropTypes.bool,
}),
).isRequired,
data: PropTypes.array,
tdDesign: PropTypes.object,
isLoading: PropTypes.bool,
onSort: PropTypes.func,
currentSort: PropTypes.object,
infoDescription: PropTypes.string,
};
export { Table };
|
Debugging
Based on the actual codebase patterns, here are the debugging techniques used:
Use the React Developer Tools browser extension to inspect component props, state, and the component tree. This is especially useful for debugging complex component hierarchies and state management issues.
The application includes React Query DevTools in development mode to inspect queries, mutations, and cache state:
| JSX |
|---|
| // From provider.jsx
<QueryClientProvider client={queryClient}>
{import.meta.env.DEV && <ReactQueryDevtools />}
{/* Application components */}
</QueryClientProvider>
|
3. Network Tab
Use the browser's Network tab to inspect API requests and responses. The application uses Axios interceptors that log detailed information about requests and responses.
4. Toast Notifications
The application uses React Toastify for displaying error messages and success notifications:
| JSX |
|---|
| // Error handling example
toast.error(error.message || 'An error occurred');
// Success notification example
toast.success('Operation completed successfully');
|
5. Console Logging
Use console.log for debugging, but remember to remove them before committing.
| JSX |
|---|
| useEffect(() => {
console.log('Component mounted with data:', data);
return () => {
console.log('Component unmounted');
};
}, [data]);
|
6. Error Boundaries
The application uses Error Boundaries to catch and display errors in a user-friendly way:
| JSX |
|---|
| // From provider.jsx
<ErrorBoundary FallbackComponent={MainErrorFallback}>
{/* Application components */}
</ErrorBoundary>
|
Additional Resources