Spaces:
Sleeping
Frontend App Mapping Guide
Overview
The backend provides app metadata via API, but the frontend needs to map this data to actual routes, icons, and components. This guide shows you how to implement this mapping.
Architecture
Backend API Frontend Mapping UI Rendering
βββββββββββββ βββββββββββββββββββββ βββββββββββββ
GET /auth/apps β appConfig.js/ts β Navigation.jsx
- Route mapping - App icons
{ - Icon mapping - App names
"code": "tickets", - Component mapping - Click handlers
"name": "Tickets",
"icon": "ticket",
"route": "/tickets"
}
Step 1: App Configuration File
Create a centralized config file that maps app codes to your frontend routes and components.
src/config/appConfig.js (React Example)
import React from 'react';
// Import your page components
import DashboardPage from '@/pages/Dashboard';
import TicketsPage from '@/pages/Tickets';
import ProjectsPage from '@/pages/Projects';
import MapsPage from '@/pages/Maps';
import OrganizationsPage from '@/pages/Organizations';
import UsersPage from '@/pages/Users';
import ActivityPage from '@/pages/ActivityLog';
import SalesOrdersPage from '@/pages/SalesOrders';
import CustomersPage from '@/pages/Customers';
import TeamPage from '@/pages/Team';
import TimesheetsPage from '@/pages/Timesheets';
import PayrollPage from '@/pages/Payroll';
import ExpensesPage from '@/pages/Expenses';
import DocumentsPage from '@/pages/Documents';
import SettingsPage from '@/pages/Settings';
import BillingPage from '@/pages/Billing';
import ProfilePage from '@/pages/Profile';
import NotificationsPage from '@/pages/Notifications';
import HelpPage from '@/pages/Help';
import ContractorsPage from '@/pages/Contractors';
import IncidentsPage from '@/pages/Incidents';
import TasksPage from '@/pages/Tasks';
import SubscriptionsPage from '@/pages/Subscriptions';
import InventoryPage from '@/pages/Inventory';
import ReportsPage from '@/pages/Reports';
/**
* App Configuration Mapping
* Maps backend app codes to frontend routes and components
*/
export const APP_CONFIG = {
// Core Apps
dashboard: {
component: DashboardPage,
path: '/dashboard',
exact: true
},
organizations: {
component: OrganizationsPage,
path: '/organizations',
exact: false // Allows sub-routes like /organizations/:id
},
users: {
component: UsersPage,
path: '/users',
exact: false
},
activity: {
component: ActivityPage,
path: '/activity',
exact: true
},
// Operations Apps
tickets: {
component: TicketsPage,
path: '/tickets',
exact: false // Sub-routes: /tickets/:id, /tickets/create
},
projects: {
component: ProjectsPage,
path: '/projects',
exact: false
},
maps: {
component: MapsPage,
path: '/map', // Note: Backend returns /map (singular)
exact: true
},
contractors: {
component: ContractorsPage,
path: '/contractors',
exact: false
},
incidents: {
component: IncidentsPage,
path: '/incidents',
exact: false
},
tasks: {
component: TasksPage,
path: '/tasks',
exact: false
},
documents: {
component: DocumentsPage,
path: '/documents',
exact: false
},
inventory: {
component: InventoryPage,
path: '/inventory',
exact: false
},
// Sales Apps
sales_orders: {
component: SalesOrdersPage,
path: '/sales-orders',
exact: false
},
customers: {
component: CustomersPage,
path: '/customers',
exact: false
},
reports: {
component: ReportsPage,
path: '/reports',
exact: false
},
// Team Apps
team: {
component: TeamPage,
path: '/team',
exact: false
},
timesheets: {
component: TimesheetsPage,
path: '/timesheets',
exact: false
},
// Finance Apps
payroll: {
component: PayrollPage,
path: '/payroll',
exact: false
},
expenses: {
component: ExpensesPage,
path: '/expenses',
exact: false
},
subscriptions: {
component: SubscriptionsPage,
path: '/subscriptions',
exact: false
},
billing: {
component: BillingPage,
path: '/billing',
exact: false
},
// Settings Apps
profile: {
component: ProfilePage,
path: '/profile',
exact: true
},
settings: {
component: SettingsPage,
path: '/settings',
exact: false // Sub-routes: /settings/account, /settings/team
},
notifications: {
component: NotificationsPage,
path: '/notifications',
exact: true
},
help: {
component: HelpPage,
path: '/help',
exact: false
}
};
/**
* Get component for app code
*/
export const getAppComponent = (appCode) => {
return APP_CONFIG[appCode]?.component;
};
/**
* Get route path for app code
*/
export const getAppPath = (appCode) => {
return APP_CONFIG[appCode]?.path;
};
/**
* Check if app code exists in config
*/
export const hasAppConfig = (appCode) => {
return appCode in APP_CONFIG;
};
Step 2: Icon Mapping
Map icon names from backend to your icon library components.
src/config/iconMap.js
import {
LayoutDashboard,
Building2,
Users,
Activity,
Ticket,
FolderKanban,
Map,
HardHat,
AlertTriangle,
CheckSquare,
ShoppingCart,
UserCircle2,
BarChart3,
UsersRound,
Clock,
DollarSign,
Receipt,
FileText,
Settings,
CreditCard,
User,
Bell,
HelpCircle,
RefreshCw,
Package
} from 'lucide-react'; // Using lucide-react icons
/**
* Icon Mapping
* Maps backend icon names to actual icon components
*/
export const ICON_MAP = {
// Core
dashboard: LayoutDashboard,
building: Building2,
users: Users,
activity: Activity,
// Operations
ticket: Ticket,
folder: FolderKanban,
map: Map,
'hard-hat': HardHat,
'alert-triangle': AlertTriangle,
'check-square': CheckSquare,
'file-text': FileText,
package: Package,
// Sales
'shopping-cart': ShoppingCart,
'chart-bar': BarChart3,
// Team
clock: Clock,
// Finance
'dollar-sign': DollarSign,
receipt: Receipt,
'credit-card': CreditCard,
'refresh-cw': RefreshCw,
// Settings
user: User,
settings: Settings,
bell: Bell,
'help-circle': HelpCircle
};
/**
* Get icon component by name
*/
export const getIcon = (iconName, fallback = HelpCircle) => {
return ICON_MAP[iconName] || fallback;
};
/**
* Render icon with props
*/
export const renderIcon = (iconName, props = {}) => {
const IconComponent = getIcon(iconName);
return <IconComponent {...props} />;
};
Using Different Icon Libraries
Font Awesome:
import {
faDashboard,
faTicket,
faMap
} from '@fortawesome/free-solid-svg-icons';
export const ICON_MAP = {
dashboard: faDashboard,
ticket: faTicket,
map: faMap,
// ...
};
Material Icons:
import DashboardIcon from '@mui/icons-material/Dashboard';
import ConfirmationNumberIcon from '@mui/icons-material/ConfirmationNumber';
import MapIcon from '@mui/icons-material/Map';
export const ICON_MAP = {
dashboard: DashboardIcon,
ticket: ConfirmationNumberIcon,
map: MapIcon,
// ...
};
Step 3: Router Setup
Generate routes dynamically based on accessible apps.
src/routes/AppRoutes.jsx
import React from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import { useAuth } from '@/hooks/useAuth';
import { useApps } from '@/hooks/useApps';
import { APP_CONFIG } from '@/config/appConfig';
import ProtectedRoute from '@/components/ProtectedRoute';
import NotFoundPage from '@/pages/NotFound';
import UnauthorizedPage from '@/pages/Unauthorized';
export const AppRoutes = () => {
const { user } = useAuth();
const { accessibleApps, loading } = useApps();
if (loading) {
return <div>Loading...</div>;
}
return (
<Routes>
{/* Public routes */}
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
{/* Dynamic app routes based on user access */}
{accessibleApps.map((app) => {
const config = APP_CONFIG[app.code];
if (!config) {
console.warn(`No config found for app: ${app.code}`);
return null;
}
return (
<Route
key={app.code}
path={config.path}
element={
<ProtectedRoute
appCode={app.code}
hasAccess={app.has_access}
>
<config.component />
</ProtectedRoute>
}
/>
);
})}
{/* Default redirect */}
<Route
path="/"
element={<Navigate to="/dashboard" replace />}
/>
{/* Error routes */}
<Route path="/unauthorized" element={<UnauthorizedPage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
);
};
Step 4: Navigation Component
Render navigation items using app data from backend.
src/components/Navigation.jsx
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
import { useAuth } from '@/hooks/useAuth';
import { getIcon } from '@/config/iconMap';
import { getAppPath } from '@/config/appConfig';
export const Navigation = () => {
const { preferences } = useAuth();
const { apps } = useApps();
const location = useLocation();
// Get favorite apps with full metadata
const favoriteApps = preferences.favorite_apps
.map(code => apps.find(app => app.code === code))
.filter(app => app && app.has_access); // Only show accessible apps
return (
<nav className="flex items-center gap-2">
{favoriteApps.map((app) => {
const IconComponent = getIcon(app.icon);
const path = getAppPath(app.code);
const isActive = location.pathname.startsWith(path);
return (
<Link
key={app.code}
to={path}
className={`
flex items-center gap-2 px-4 py-2 rounded-lg
${isActive ? 'bg-primary text-white' : 'text-gray-700 hover:bg-gray-100'}
`}
title={app.description}
>
<IconComponent size={20} />
<span>{app.name}</span>
</Link>
);
})}
<AppLauncherButton />
</nav>
);
};
Step 5: App Launcher (9-Dot Menu)
Show all accessible apps in a drawer/modal.
src/components/AppLauncher.jsx
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Grid3x3 } from 'lucide-react';
import { useApps } from '@/hooks/useApps';
import { getIcon } from '@/config/iconMap';
import { getAppPath } from '@/config/appConfig';
export const AppLauncher = () => {
const [isOpen, setIsOpen] = useState(false);
const { apps, appsByCategory } = useApps();
const navigate = useNavigate();
const handleAppClick = (app) => {
const path = getAppPath(app.code);
navigate(path);
setIsOpen(false);
};
return (
<>
{/* Trigger Button */}
<button
onClick={() => setIsOpen(true)}
className="p-2 rounded-lg hover:bg-gray-100"
title="All Apps"
>
<Grid3x3 size={20} />
</button>
{/* Drawer/Modal */}
{isOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 z-50">
<div className="absolute right-0 top-0 h-full w-96 bg-white shadow-lg overflow-y-auto">
<div className="p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold">All Apps</h2>
<button onClick={() => setIsOpen(false)}>Γ</button>
</div>
{/* Apps by Category */}
{Object.entries(appsByCategory).map(([category, categoryApps]) => {
// Filter only accessible apps
const accessibleApps = categoryApps.filter(app => app.has_access);
if (accessibleApps.length === 0) return null;
return (
<div key={category} className="mb-6">
<h3 className="text-sm font-semibold text-gray-500 uppercase mb-3">
{category}
</h3>
<div className="grid grid-cols-3 gap-4">
{accessibleApps.map((app) => {
const IconComponent = getIcon(app.icon);
return (
<button
key={app.code}
onClick={() => handleAppClick(app)}
className="flex flex-col items-center p-4 rounded-lg hover:bg-gray-100 transition"
>
<IconComponent size={32} className="mb-2" />
<span className="text-sm text-center">{app.name}</span>
</button>
);
})}
</div>
</div>
);
})}
</div>
</div>
</div>
)}
</>
);
};
Step 6: Custom Hook for Apps
Create a hook to manage app state.
src/hooks/useApps.js
import { useEffect, useState } from 'react';
import { useAuth } from './useAuth';
import api from '@/services/api';
export const useApps = () => {
const { isAuthenticated } = useAuth();
const [apps, setApps] = useState([]);
const [appsByCategory, setAppsByCategory] = useState({});
const [accessibleApps, setAccessibleApps] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!isAuthenticated) return;
const fetchApps = async () => {
try {
const response = await api.get('/auth/apps');
const data = response.data;
setApps(data.apps);
setAppsByCategory(data.apps_by_category);
setAccessibleApps(data.apps.filter(app => app.has_access));
} catch (error) {
console.error('Failed to fetch apps:', error);
} finally {
setLoading(false);
}
};
fetchApps();
}, [isAuthenticated]);
return {
apps,
appsByCategory,
accessibleApps,
loading,
// Helper functions
getAppByCode: (code) => apps.find(app => app.code === code),
hasAccess: (code) => accessibleApps.some(app => app.code === code)
};
};
Step 7: Protected Route Component
Prevent access to routes user doesn't have permission for.
src/components/ProtectedRoute.jsx
import React from 'react';
import { Navigate } from 'react-router-dom';
import { useAuth } from '@/hooks/useAuth';
export const ProtectedRoute = ({ children, appCode, hasAccess }) => {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
if (hasAccess === false) {
return <Navigate to="/unauthorized" replace />;
}
return children;
};
export default ProtectedRoute;
Quick Setup Checklist
β Backend (Already Done)
- Apps defined in
app.config.apps.py - Role access configured
- API endpoints created (
/auth/apps,/me/preferences/available-apps)
π Frontend (Your Tasks)
Create Config Files
-
src/config/appConfig.js- Map app codes to routes/components -
src/config/iconMap.js- Map icon names to icon components
-
Create Hook
-
src/hooks/useApps.js- Fetch and manage app data
-
Create Components
-
src/components/Navigation.jsx- Top navigation bar -
src/components/AppLauncher.jsx- 9-dot menu drawer -
src/components/ProtectedRoute.jsx- Route protection
-
Update Router
-
src/routes/AppRoutes.jsx- Dynamic route generation
-
Fetch Apps on Login
- Call
/auth/appsafter successful login - Store in global state (Redux/Zustand/Context)
- Call
Key Points
- Backend provides metadata - You don't hardcode app lists
- Frontend maps codes to implementation - You define routes and components
- Icons must match - Ensure icon names in backend match your icon library
- Routes can differ slightly - Backend says
/map, you can use/mapor/maps(just be consistent) - Dynamic routing - Routes are generated based on user access
- Type safety - Consider using TypeScript for app config
TypeScript Version
If using TypeScript, define types:
// types/app.ts
export interface App {
code: string;
name: string;
description: string;
icon: string;
route: string;
category: string;
requires_permission: string | null;
is_active: boolean;
has_access: boolean;
}
export interface AppConfig {
component: React.ComponentType;
path: string;
exact: boolean;
}
export type AppConfigMap = Record<string, AppConfig>;
Testing
Check console for unmapped apps:
apps.forEach(app => { if (!APP_CONFIG[app.code]) { console.error(`Missing config for app: ${app.code}`); } });Check console for missing icons:
apps.forEach(app => { if (!ICON_MAP[app.icon]) { console.warn(`Missing icon: ${app.icon} for app: ${app.code}`); } });Test route protection:
- Try accessing a route for an app you don't have access to
- Should redirect to
/unauthorized
Last Updated: November 18, 2025
Framework Examples: React (easily adaptable to Vue, Angular, Svelte)