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

441 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (`I1``I8`) 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 `I1``I8` 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):**
```env
# 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):**
```env
# Backend API base URL
VITE_API_URL=http://localhost:8000/api/v1
```
**Access in code:**
```js
// 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`
```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
```js
// ❌ 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`
```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`):
```js
// 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:
```js
// 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 (`I1``I8`) 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:**
```js
// 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
```js
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
```js
// 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:
```js
// src/config/env.js — add this
export const FEATURE_REAL_API = import.meta.env.VITE_FEATURE_REAL_API === 'true';
```
```env
# .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:
```js
// 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.
```js
// 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.
```bash
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.*