swiftops-backend / docs /api /user-profile /FRONTEND_APP_MAPPING_GUIDE.md
kamau1's picture
feat: default theme is now darkmode, updated invitaion to refresh tokens for a reinvite, scoped apps by role better
eabe4ec

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)

  1. Create Config Files

    • src/config/appConfig.js - Map app codes to routes/components
    • src/config/iconMap.js - Map icon names to icon components
  2. Create Hook

    • src/hooks/useApps.js - Fetch and manage app data
  3. Create Components

    • src/components/Navigation.jsx - Top navigation bar
    • src/components/AppLauncher.jsx - 9-dot menu drawer
    • src/components/ProtectedRoute.jsx - Route protection
  4. Update Router

    • src/routes/AppRoutes.jsx - Dynamic route generation
  5. Fetch Apps on Login

    • Call /auth/apps after successful login
    • Store in global state (Redux/Zustand/Context)

Key Points

  1. Backend provides metadata - You don't hardcode app lists
  2. Frontend maps codes to implementation - You define routes and components
  3. Icons must match - Ensure icon names in backend match your icon library
  4. Routes can differ slightly - Backend says /map, you can use /map or /maps (just be consistent)
  5. Dynamic routing - Routes are generated based on user access
  6. 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

  1. Check console for unmapped apps:

    apps.forEach(app => {
      if (!APP_CONFIG[app.code]) {
        console.error(`Missing config for app: ${app.code}`);
      }
    });
    
  2. Check console for missing icons:

    apps.forEach(app => {
      if (!ICON_MAP[app.icon]) {
        console.warn(`Missing icon: ${app.icon} for app: ${app.code}`);
      }
    });
    
  3. 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)