Files
LynkedUpPro_CRM/docs/integration/00_integration_overview.md
Satyam 7694788387 feat(ProCanvas): retro game overhaul with log actions, challenges & achievements modals
- Complete ProCanvas redesign with retro sports game aesthetic
- Card-game style photo frame with tilt, shimmer, corner star accents
- Daily Missions card with weekly challenge + individual quest progress bars
- Log Action card (Door Knocked, Lead Gained, Appointment Set, Client Meeting)
- Each log action opens themed modal with relevant input fields
- Challenges modal with 6 active challenges and progress tracking
- Achievements modal with all badges, unlock status, descriptions
- Nav bar tabs (Leaderboard, Challenges, Achievements) wired to modals
- Rewards & Checkpoints with named stages (Daily Grind to Legend Run)
- Smoother Hot Streak pulse animation (2.5s float + 3s pulse rings)
- Hit The Map button with smooth pulsing glow animation
- Grid pattern overlay in Leaderboard card
- Light mode support via dark: Tailwind variants throughout
- Top 3 badges displayed on profile card
- Fixed dropdown option visibility in dark mode
2026-02-26 01:36:36 +05:30

15 KiB
Raw Permalink Blame History

I0 — Integration Overview

Module: I0 — Integration Team Foundation Companion backend doc: docs/backend/00_project_overview.md (B0) Status: Integration Team reads this first, before touching any frontend code.


What This Module Covers

This document establishes the shared foundation every integration module (I1I8) will build on:

  1. API client setup — Axios instance, base URL, default headers
  2. Auth token strategy — httpOnly cookies, no localStorage
  3. Global 401 interceptor — automatic token refresh + request retry
  4. Environment variable setup
  5. Loading and error state conventions
  6. The mock-to-real migration pattern (how to replace mockStore piece by piece)
  7. Role-redirect logic (how to stay in sync with Login.jsx)

Read this document fully before working on any I1I8 module.


1. Environment Variables

The frontend uses Vite. All client-side environment variables must be prefixed with VITE_.

Add to .env.local (never commit this file):

# Backend API
VITE_API_URL=http://localhost:8000/api/v1

# External (already present — do not change)
VITE_GROQ_API_KEY=your_groq_key_here
VITE_OPENWEATHER_API_KEY=your_openweather_key_here

Add to .env.example (commit this as a template):

# Backend API base URL
VITE_API_URL=http://localhost:8000/api/v1

Access in code:

// src/config/env.js  (add alongside existing exports)
export const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:8000/api/v1';

The ?? fallback prevents crashes during local dev if .env.local is missing. In production on Vercel, VITE_API_URL must be set as an environment variable in the Vercel dashboard.


2. API Client Setup

Create one shared Axios instance. Every integration module imports from this file — never call axios.create() again elsewhere.

File to create: src/api/client.js

import axios from 'axios';
import { API_URL } from '../config/env';

const apiClient = axios.create({
  baseURL: API_URL,
  withCredentials: true,       // sends httpOnly cookies automatically
  headers: {
    'Content-Type': 'application/json',
  },
});

export default apiClient;

Why withCredentials: true?

The backend sets the access token and refresh token as httpOnly cookies (see B1). The browser will not include these cookies on cross-origin requests unless withCredentials: true is set. This is the only auth mechanism — there is no Authorization: Bearer header and no localStorage.

What NOT to do

// ❌ Never do this
localStorage.setItem('access_token', token);
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;

// ✅ Correct — the cookie is set by the backend, browser sends it automatically

3. Global 401 Interceptor — Token Refresh and Retry

When the access token expires, the backend returns 401. The client must silently refresh the token and retry the original request. This logic lives in one place: src/api/interceptors.js.

File to create: src/api/interceptors.js

import apiClient from './client';

let isRefreshing = false;
let failedQueue = [];   // requests that arrived while refresh was in-flight

function processQueue(error, token = null) {
  failedQueue.forEach(({ resolve, reject }) => {
    if (error) {
      reject(error);
    } else {
      resolve(token);
    }
  });
  failedQueue = [];
}

apiClient.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;

    // Only handle 401s that haven't already been retried
    if (error.response?.status !== 401 || originalRequest._retry) {
      return Promise.reject(error);
    }

    // If a refresh is already in progress, queue this request
    if (isRefreshing) {
      return new Promise((resolve, reject) => {
        failedQueue.push({ resolve, reject });
      }).then(() => apiClient(originalRequest));
    }

    originalRequest._retry = true;
    isRefreshing = true;

    try {
      // POST /auth/refresh — backend rotates the cookie automatically
      await apiClient.post('/auth/refresh');
      processQueue(null);
      return apiClient(originalRequest);     // retry original request
    } catch (refreshError) {
      processQueue(refreshError);
      // Refresh failed → force logout
      window.dispatchEvent(new CustomEvent('auth:logout-required'));
      return Promise.reject(refreshError);
    } finally {
      isRefreshing = false;
    }
  }
);

export default apiClient;

Register interceptors in your app entry point. Import this file once at the top of src/main.jsx (or src/App.jsx):

// src/main.jsx — add this import at the top
import './api/interceptors';

The failedQueue pattern prevents a thunderstorm of refresh requests when multiple concurrent API calls all get 401 at once. Only one refresh call is made; all others wait in the queue and retry together.

Listening for the forced-logout event

In AuthContext.jsx, add a listener for the custom event dispatched when refresh fails:

// Inside AuthProvider useEffect
useEffect(() => {
  const handleForceLogout = () => {
    logout();  // existing logout function
  };
  window.addEventListener('auth:logout-required', handleForceLogout);
  return () => window.removeEventListener('auth:logout-required', handleForceLogout);
}, []);

4. API Module Files

Each integration module (I1I8) will create one src/api/*.js file. These are thin wrappers around apiClient — no business logic, no state. Example structure:

src/api/
├── client.js          ← Axios instance (shared)
├── interceptors.js    ← 401 handler + refresh (imported once in main.jsx)
├── auth.js            ← I1: login, logout, refresh, getMe
├── properties.js      ← I3: getProperties, getProperty, assignAgent, updateStatus
├── users.js           ← I4: getUsers, getUser, updateUser
├── meetings.js        ← I5: getMeetings, createMeeting, updateMeeting, requestChange
├── vendors.js         ← I6: getVendors, getVendor, uploadComplianceDoc
├── invoices.js        ← I7: getInvoices, createInvoice, approveInvoice
└── chatbot.js         ← I8: sendMessage (streaming)

Convention for every API file:

// src/api/properties.js — example pattern
import apiClient from './client';

export async function getProperties(params = {}) {
  const { data } = await apiClient.get('/properties', { params });
  return data;   // { data: [...], meta: { total, page, page_size } }
}

export async function getProperty(id) {
  const { data } = await apiClient.get(`/properties/${id}`);
  return data;
}

export async function assignAgent(propertyId, agentId) {
  const { data } = await apiClient.patch(`/properties/${propertyId}`, {
    assigned_agent_id: agentId,
  });
  return data;
}

Always destructure { data } from the Axios response — the actual payload is in response.data, not response itself.


5. Loading and Error State Conventions

Every component that fetches data should follow this pattern. Use React's built-in useState + useEffect — no new libraries needed at this stage.

Standard fetch pattern

const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
  let cancelled = false;

  async function fetchData() {
    try {
      setLoading(true);
      setError(null);
      const result = await getProperties({ status: 'Hot Lead' });
      if (!cancelled) setData(result.data);
    } catch (err) {
      if (!cancelled) setError(err.response?.data?.detail ?? 'Failed to load data');
    } finally {
      if (!cancelled) setLoading(false);
    }
  }

  fetchData();
  return () => { cancelled = true; };
}, []);

The cancelled flag prevents state updates on unmounted components (React strict mode issue).

Error shape from the backend

// All backend errors follow this shape (from B0):
// { detail: "Human-readable message", code: "MACHINE_CODE" }

// Access in catch block:
const message = err.response?.data?.detail ?? 'An unexpected error occurred';
const code    = err.response?.data?.code;    // e.g. "PERMISSION_DENIED"

// HTTP status codes:
// 400 — Validation error (bad request body)
// 401 — Not authenticated / token expired
// 403 — Authenticated but forbidden (wrong role)
// 404 — Resource not found
// 422 — FastAPI schema validation failed
// 429 — Rate limited
// 500 — Server error

Loading UI

Use the existing LoadingSpinner component (or the role-appropriate skeleton) that is already present in the codebase. Do not add new spinner libraries.


6. Mock-to-Real Migration Pattern

The frontend currently runs entirely off src/data/mockStore.jsx through MockStoreProvider. The migration approach is module by module — never rip out the whole mock layer at once.

The three phases of each module swap

Phase 1 — Feature-flagged (default OFF)

Each integration module starts with a feature flag so the mock data remains the fallback during development:

// src/config/env.js — add this
export const FEATURE_REAL_API = import.meta.env.VITE_FEATURE_REAL_API === 'true';
# .env.local — set to true when you're ready to test a module
VITE_FEATURE_REAL_API=false

Phase 2 — Swap one data source at a time

For example, when doing I3 (Properties), update only the properties data hook:

// src/hooks/useProperties.js — new file created by I3
import { useState, useEffect } from 'react';
import { FEATURE_REAL_API } from '../config/env';
import { getProperties } from '../api/properties';
import { useMockStore } from '../context/MockStoreContext';

export function useProperties(filters = {}) {
  const mockStore = useMockStore();

  if (!FEATURE_REAL_API) {
    // Return mock data in the same shape as the real API
    return {
      data: mockStore.properties.filter(/* apply filters */),
      loading: false,
      error: null,
    };
  }

  // Real API path (only when flag is true)
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  // ... useEffect fetch ...
  return { data, loading, error };
}

Each integration module's spec (I1I8) will define the exact hook shape and what filters to support.

Phase 3 — Remove the mock branch

Once QA confirms the real API works correctly, delete the mock branch and the feature flag check. The MockStoreProvider can be removed only when all modules are migrated (I1 → I8 all complete).

Shape matching rule

The real API response shape must always match the mock data shape that components already consume. If the backend returns a field name differently (e.g., agent_id vs agentId), transform it in the API layer (src/api/*.js), not in the component.

// src/api/properties.js — transform snake_case to camelCase if needed
export async function getProperties(params = {}) {
  const { data } = await apiClient.get('/properties', { params });
  return {
    ...data,
    data: data.data.map((p) => ({
      ...p,
      agentId: p.assigned_agent_id,        // normalize for components
      marketValue: p.estimated_market_value,
    })),
  };
}

7. Role → Redirect Map

The backend returns a role field in the /auth/me and /auth/login responses. The frontend uses this to redirect users to their portal. This map must stay in sync with the switch statement in src/pages/Login.jsx.

Role Redirect path
OWNER /owner/snapshot
ADMIN /admin/dashboard
FIELD_AGENT /emp/fa/dashboard
CONTRACTOR /contractor/dashboard
SUBCONTRACTOR /subcontractor/dashboard
VENDOR /vendor/dashboard
CUSTOMER /portal/profile

These exact strings are used in src/context/AuthContext.jsx (the ROLE_ROUTES map) after I1 replaces the login flow.


8. Dependency Installation

Only one new package is needed: Axios. Everything else (React Query, SWR, etc.) is explicitly out of scope for the integration phase — keep the state management approach simple and consistent with the existing codebase.

npm install axios

Verify it was added to package.json dependencies (not devDependencies).


9. File Creation Checklist for I0

Before moving to I1, confirm all of the following are created or updated:

File Action
.env.local Add VITE_API_URL=http://localhost:8000/api/v1
.env.example Add VITE_API_URL= (no value)
src/config/env.js Export API_URL and FEATURE_REAL_API
src/api/client.js Create Axios instance with withCredentials: true
src/api/interceptors.js Create 401 interceptor with queue + retry logic
src/main.jsx Add import './api/interceptors' at top
src/context/AuthContext.jsx Add auth:logout-required event listener
package.json axios present in dependencies

10. What Each Integration Module Builds On Top of This

Module What it adds
I1 — Auth Integration Replaces AuthContext login/logout with real /auth/login, /auth/logout, /auth/me calls
I2 — Data Layer Migration Wraps MockStoreProvider in a feature-flagged ApiProvider; establishes hook conventions
I3 — Properties Hooks for map + property CRUD; geospatial filter params; lead status updates
I4 — Users & People Hooks for PeopleDirectory, VendorDirectory; masked field handling
I5 — Meetings Hooks for AdminSchedule, meeting status transitions, change request flow
I6 — Vendors & Compliance Hooks for vendor CRUD, COI upload, compliance badge logic
I7 — Financial Hooks for invoice tables, AR/AP views, payout approval flow
I8 — Chatbot Streaming response from /chatbot/message; replaces direct Groq SDK calls

11. Key Constraints — Do Not Violate

  1. Never store tokens in localStorage or sessionStorage. The httpOnly cookie is set by the backend. The frontend never touches it directly.
  2. Never call axios.create() outside of src/api/client.js. All API calls go through the shared instance so the interceptor works.
  3. Never modify src/data/mockStore.jsx during integration. Mock data is the reference — it must remain intact as a fallback.
  4. Never remove MockStoreProvider from main.jsx until I2 is fully complete and all feature flags have been validated.
  5. Money values arrive in cents from the API. Divide by 100 before display. Never send or store display-formatted dollar values back to the API.
  6. All timestamps are ISO 8601 UTC. Use new Date(isoString).toLocaleDateString() or the existing date util for display formatting — do not use raw strings.

Document maintained by Integration Team — mark complete in docs/README.md when all files in Section 9 checklist are created and verified.