Skip to content

Phase 06: Frontend & UI

Duration: 1 week (parallel with backend phases) Goal: Build user interfaces for Legacy ERP WMS operations using Refine.dev

Update (Jan 2026): Developers have chosen Refine.dev as it maps 1:1 with Framework M’s auto-generated APIs. Only ~100 lines of code needed for data provider mapping.


Advantage Benefit for Legacy ERP
1:1 API mapping Framework M generates OpenAPI, Refine consumes it
Headless Choose any UI library (Ant Design, Material UI, etc.)
CRUD out of box List, Create, Edit, Show pages auto-generated
Auth integration Works with JWT/Session auth
Real-time WebSocket support for live updates
src/providers/frameworkMDataProvider.ts
import { DataProvider } from "@refinedev/core";
const API_URL = import.meta.env.VITE_API_URL;
export const dataProvider: DataProvider = {
getList: async ({ resource, pagination, filters, sorters }) => {
const { current = 1, pageSize = 10 } = pagination ?? {};
const params = new URLSearchParams({
page: String(current),
page_size: String(pageSize),
});
// Add filters
filters?.forEach(filter => {
params.append(`filter[${filter.field}]`, String(filter.value));
});
const response = await fetch(`${API_URL}/api/v1/${resource}?${params}`, {
headers: { Authorization: `Bearer ${localStorage.getItem("token")}` },
});
const data = await response.json();
return {
data: data.items,
total: data.total,
};
},
getOne: async ({ resource, id }) => {
const response = await fetch(`${API_URL}/api/v1/${resource}/${id}`, {
headers: { Authorization: `Bearer ${localStorage.getItem("token")}` },
});
return { data: await response.json() };
},
create: async ({ resource, variables }) => {
const response = await fetch(`${API_URL}/api/v1/${resource}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
body: JSON.stringify(variables),
});
return { data: await response.json() };
},
update: async ({ resource, id, variables }) => {
const response = await fetch(`${API_URL}/api/v1/${resource}/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
body: JSON.stringify(variables),
});
return { data: await response.json() };
},
deleteOne: async ({ resource, id }) => {
await fetch(`${API_URL}/api/v1/${resource}/${id}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${localStorage.getItem("token")}` },
});
return { data: { id } };
},
getApiUrl: () => API_URL,
};

Terminal window
npm create refine-app@latest legacy_erp-wms-ui -- \
--ui antd \
--router react-router \
--auth custom
legacy_erp-wms-ui/
├── src/
│ ├── providers/
│ │ ├── frameworkMDataProvider.ts # ~100 lines
│ │ └── authProvider.ts
│ ├── pages/
│ │ ├── items/
│ │ ├── depots/
│ │ ├── urcs/
│ │ └── stock-entries/
│ ├── components/
│ └── App.tsx
├── package.json
└── vite.config.ts
  • Create Refine project with Ant Design
  • Implement Framework M data provider
  • Implement auth provider (JWT)
  • Configure resources

src/App.tsx
import { Refine } from "@refinedev/core";
import { dataProvider } from "./providers/frameworkMDataProvider";
import { authProvider } from "./providers/authProvider";
function App() {
return (
<Refine
dataProvider={dataProvider}
authProvider={authProvider}
resources={[
{
name: "item",
list: "/items",
create: "/items/create",
edit: "/items/:id/edit",
show: "/items/:id",
meta: { label: "Items", icon: <ShoppingOutlined /> },
},
{
name: "depot",
list: "/depots",
create: "/depots/create",
edit: "/depots/:id/edit",
meta: { label: "Depots", icon: <HomeOutlined /> },
},
{
name: "urc",
list: "/urcs",
create: "/urcs/create",
edit: "/urcs/:id/edit",
meta: { label: "URCs", icon: <ShopOutlined /> },
},
{
name: "stock_entry",
list: "/stock-entries",
create: "/stock-entries/create",
show: "/stock-entries/:id",
meta: { label: "Stock Entries", icon: <SwapOutlined /> },
},
{
name: "material_request",
list: "/material-requests",
create: "/material-requests/create",
show: "/material-requests/:id",
meta: { label: "Material Requests", icon: <FileTextOutlined /> },
},
]}
/>
);
}

Portal Users Resources
HQ Admin Legacy ERP HO Staff All resources, reports
Depot Depot Staff Inventory, GRN, Delivery
URC URC Managers Orders, Stock view
// Filter resources based on user role
const getResourcesForRole = (role: string) => {
const allResources = [...];
if (role === "depot_staff") {
return allResources.filter(r =>
["stock_entry", "material_request", "item"].includes(r.name)
);
}
// ...
};

5.1 Item List (Auto-generated with customization)

Section titled “5.1 Item List (Auto-generated with customization)”
src/pages/items/list.tsx
import { List, Table, useTable } from "@refinedev/antd";
export const ItemList = () => {
const { tableProps } = useTable();
return (
<List>
<Table {...tableProps} rowKey="id">
<Table.Column dataIndex="item_code" title="Code" />
<Table.Column dataIndex="item_name" title="Name" />
<Table.Column dataIndex="item_group" title="Group" />
<Table.Column dataIndex="legacy_erp_price" title="Legacy ERP Price" />
<Table.Column dataIndex="is_active" title="Active" />
</Table>
</List>
);
};
src/pages/stock-entries/create.tsx
import { Create, useForm, useSelect } from "@refinedev/antd";
import { Form, Input, Select, Table } from "antd";
export const StockEntryCreate = () => {
const { formProps, saveButtonProps } = useForm();
const { selectProps: warehouseProps } = useSelect({ resource: "warehouse" });
return (
<Create saveButtonProps={saveButtonProps}>
<Form {...formProps} layout="vertical">
<Form.Item label="Entry Type" name="entry_type" rules={[{ required: true }]}>
<Select options={[
{ value: "Receipt", label: "Receipt" },
{ value: "Issue", label: "Issue" },
{ value: "Transfer", label: "Transfer" },
]} />
</Form.Item>
<Form.Item label="Warehouse" name="warehouse">
<Select {...warehouseProps} />
</Form.Item>
{/* Items table */}
</Form>
</Create>
);
};

src/providers/authProvider.ts
import { AuthProvider } from "@refinedev/core";
export const authProvider: AuthProvider = {
login: async ({ email, password }) => {
const response = await fetch(`${API_URL}/api/v1/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
if (response.ok) {
const { access_token, user } = await response.json();
localStorage.setItem("token", access_token);
localStorage.setItem("user", JSON.stringify(user));
return { success: true, redirectTo: "/" };
}
return { success: false, error: { message: "Invalid credentials" } };
},
logout: async () => {
localStorage.removeItem("token");
localStorage.removeItem("user");
return { success: true, redirectTo: "/login" };
},
check: async () => {
const token = localStorage.getItem("token");
return { authenticated: !!token };
},
getIdentity: async () => {
const user = localStorage.getItem("user");
return user ? JSON.parse(user) : null;
},
};

Dockerfile.frontend
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80

  • Login/logout works
  • Item CRUD functional
  • Depot CRUD functional
  • URC CRUD functional
  • Stock Entry creation works
  • Material Request workflow works
  • Role-based access working
  • Mobile responsive
  • Deployed to MeghRaj

Task Duration
Setup + Data Provider 4 hours
Auth Provider 2 hours
Core resources 2 days
Custom pages 2 days
Testing & polish 1 day
Total ~5 days