Merchants Navigation Refactor Implementation Plan โ
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Replace the cross-merchant tab strip with per-route headers, a merchant switcher dropdown, localStorage-backed "current merchant" persistence, and smart redirects โ so admins think in terms of a single working context, not drill-in/drill-out from a list.
Architecture: Two distinct chromes: (1) cross-merchant chrome for /merchants/all and /merchants/create with their own PageHeader; (2) per-merchant chrome for /merchants/:merchantId/* with breadcrumb, switcher, and the existing PageTabs. MerchantsListLayout (the shared tab strip shell) is deleted. A pure-JS helper module (currentMerchant.js) manages localStorage persistence. A MerchantsIndexRedirect component at the /merchants index handles role-aware + state-aware routing. PageHeader gains an optional breadcrumb prop.
Tech Stack: React 19, React Router v6 (layout routes, useParams, useNavigate, useLocation), lucide-react, Vitest + Testing Library React, localStorage.
Spec: docs/superpowers/specs/2026-04-25-merchants-nav-refactor-design.md
Before you start โ
- Verify auth hydration. The
userprop passed toAdminDashboardis a raw Firebase Auth user object (apps/admin/src/App.jsx:49โ88). It does not carrymerchantId. Task 5 adds this field. Until Task 5 is wired,MerchantsIndexRedirectwill misroute merchant-role users. Tasks 1โ4 can be built and tested in isolation; Tasks 5โ8 must be wired together. - Commit policy. Confirm commit permission at the first commit step; if denied, pause after each task and let the user commit manually.
File structure โ
New files โ
| Path | Responsibility |
|---|---|
apps/admin/src/lib/currentMerchant.js | localStorage helper: getCurrentMerchantId, getRecentMerchantIds, rememberMerchant, forgetMerchant, clearCurrentMerchant. Pure JS, no React, no Firebase. |
apps/admin/src/lib/__tests__/currentMerchant.test.js | Unit tests for the 5 exported functions. |
apps/admin/src/components/merchants/MerchantsIndexRedirect.jsx | Smart redirect at /merchants index: merchantโown detail, adminโstored or /all. |
apps/admin/src/components/merchants/__tests__/MerchantsIndexRedirect.test.jsx | 4 redirect cases + misconfigured state. |
apps/admin/src/components/merchants/MerchantSwitcher.jsx | Admin-only dropdown: "Currently viewing" + recent list + "View all" link. |
apps/admin/src/components/merchants/__tests__/MerchantSwitcher.test.jsx | Rendering states, navigation, empty state. |
Modified files โ
| Path | Change |
|---|---|
apps/admin/src/components/PageHeader.jsx | Add optional breadcrumb prop, rendered above the title. |
apps/admin/src/components/merchants/MerchantDetail.jsx | Add rememberMerchant effect. Augment 404 branch with forgetMerchant + redirect. Add breadcrumb (admin-only). Add <MerchantSwitcher /> in header actions. Accept isAdmin prop. |
apps/admin/src/components/merchants/MerchantsAll.jsx | Render own <PageHeader /> (title "Merchants", subtitle, [+ Create Merchant] button + <MerchantSwitcher />). Replace emoji in empty state. |
apps/admin/src/components/merchants/MerchantsCreate.jsx | Render own <PageHeader /> (title "Create Merchant", [Cancel] action). |
apps/admin/src/components/AdminDashboard.jsx | Replace <Route index> with <MerchantsIndexRedirect />. Remove /merchants/overview and /merchants/offers routes. Remove MerchantsListLayout wrapper. Pass isAdmin to <MerchantDetail />. Update merchantOnly redirect to point to /merchants instead of /merchants/overview. Remove MerchantsListLayout, MerchantsOverview, MerchantsOffers imports. |
apps/admin/src/App.jsx | Extend auth hydration to load users/{uid}.merchantId into user object for merchant-role users. Add clearCurrentMerchant() to sign-out flow. |
Deleted files โ
| Path | Reason |
|---|---|
apps/admin/src/components/merchants/MerchantsListLayout.jsx | Replaced by per-route headers. |
apps/admin/src/components/merchants/__tests__/MerchantsListLayout.test.jsx | Tests the deleted component. |
apps/admin/src/components/merchants/MerchantsOverview.jsx | Placeholder โ per-merchant overview is in MerchantDetail. |
apps/admin/src/components/merchants/MerchantsOffers.jsx | Placeholder โ per-merchant offers is Project C. |
Task 1: currentMerchant.js helper module (TDD) โ
Files:
Create:
apps/admin/src/lib/__tests__/currentMerchant.test.jsCreate:
apps/admin/src/lib/currentMerchant.js[ ] Step 1: Write the failing tests
Create apps/admin/src/lib/__tests__/currentMerchant.test.js:
import { describe, it, expect, beforeEach } from 'vitest'
import {
getCurrentMerchantId,
getRecentMerchantIds,
rememberMerchant,
forgetMerchant,
clearCurrentMerchant,
} from '../currentMerchant'
const KEY = 'lantern.admin.currentMerchant'
beforeEach(() => {
localStorage.clear()
})
describe('getCurrentMerchantId', () => {
it('returns null when nothing stored', () => {
expect(getCurrentMerchantId()).toBeNull()
})
it('returns the stored current merchant ID', () => {
localStorage.setItem(KEY, JSON.stringify({ v: 1, current: 'merch_1', recent: ['merch_1'] }))
expect(getCurrentMerchantId()).toBe('merch_1')
})
it('returns null on corrupted JSON', () => {
localStorage.setItem(KEY, '{not valid json')
expect(getCurrentMerchantId()).toBeNull()
})
it('returns null when current is missing from the object', () => {
localStorage.setItem(KEY, JSON.stringify({ v: 1, recent: [] }))
expect(getCurrentMerchantId()).toBeNull()
})
})
describe('getRecentMerchantIds', () => {
it('returns empty array when nothing stored', () => {
expect(getRecentMerchantIds()).toEqual([])
})
it('returns the stored recent array', () => {
localStorage.setItem(KEY, JSON.stringify({ v: 1, current: 'merch_1', recent: ['merch_1', 'merch_2'] }))
expect(getRecentMerchantIds()).toEqual(['merch_1', 'merch_2'])
})
it('returns empty array on corrupted JSON', () => {
localStorage.setItem(KEY, '{broken')
expect(getRecentMerchantIds()).toEqual([])
})
})
describe('rememberMerchant', () => {
it('sets current and adds to recent', () => {
rememberMerchant('merch_1')
expect(getCurrentMerchantId()).toBe('merch_1')
expect(getRecentMerchantIds()).toEqual(['merch_1'])
})
it('moves a repeated ID to the front of recent (dedupes)', () => {
rememberMerchant('merch_1')
rememberMerchant('merch_2')
rememberMerchant('merch_1')
expect(getRecentMerchantIds()).toEqual(['merch_1', 'merch_2'])
expect(getCurrentMerchantId()).toBe('merch_1')
})
it('caps recent at 8 entries', () => {
for (let i = 1; i <= 10; i++) {
rememberMerchant(`merch_${i}`)
}
const recent = getRecentMerchantIds()
expect(recent).toHaveLength(8)
expect(recent[0]).toBe('merch_10')
expect(recent[7]).toBe('merch_3')
})
it('does not crash on corrupted existing state', () => {
localStorage.setItem(KEY, '{broken')
rememberMerchant('merch_1')
expect(getCurrentMerchantId()).toBe('merch_1')
expect(getRecentMerchantIds()).toEqual(['merch_1'])
})
})
describe('forgetMerchant', () => {
it('removes from current and recent', () => {
rememberMerchant('merch_1')
rememberMerchant('merch_2')
forgetMerchant('merch_2')
expect(getCurrentMerchantId()).toBeNull()
expect(getRecentMerchantIds()).toEqual(['merch_1'])
})
it('clears current only if it matches', () => {
rememberMerchant('merch_1')
rememberMerchant('merch_2')
forgetMerchant('merch_1')
expect(getCurrentMerchantId()).toBe('merch_2')
expect(getRecentMerchantIds()).toEqual(['merch_2'])
})
it('is a no-op if ID is not present', () => {
rememberMerchant('merch_1')
forgetMerchant('merch_999')
expect(getCurrentMerchantId()).toBe('merch_1')
expect(getRecentMerchantIds()).toEqual(['merch_1'])
})
})
describe('clearCurrentMerchant', () => {
it('wipes the entire localStorage entry', () => {
rememberMerchant('merch_1')
rememberMerchant('merch_2')
clearCurrentMerchant()
expect(getCurrentMerchantId()).toBeNull()
expect(getRecentMerchantIds()).toEqual([])
expect(localStorage.getItem(KEY)).toBeNull()
})
})- [ ] Step 2: Run tests to verify they fail
Run: npx vitest run apps/admin/src/lib/__tests__/currentMerchant.test.js
Expected: FAIL โ module not found.
- [ ] Step 3: Implement
currentMerchant.js
Create apps/admin/src/lib/currentMerchant.js:
const KEY = 'lantern.admin.currentMerchant'
const MAX_RECENT = 8
function read() {
try {
const raw = localStorage.getItem(KEY)
if (!raw) return null
return JSON.parse(raw)
} catch {
return null
}
}
function write(data) {
localStorage.setItem(KEY, JSON.stringify(data))
}
export function getCurrentMerchantId() {
const data = read()
return data?.current ?? null
}
export function getRecentMerchantIds() {
const data = read()
return Array.isArray(data?.recent) ? data.recent : []
}
export function rememberMerchant(id) {
const data = read() || { v: 1, current: null, recent: [] }
data.current = id
data.recent = [id, ...data.recent.filter((r) => r !== id)].slice(0, MAX_RECENT)
write(data)
}
export function forgetMerchant(id) {
const data = read()
if (!data) return
if (data.current === id) data.current = null
data.recent = data.recent.filter((r) => r !== id)
write(data)
}
export function clearCurrentMerchant() {
localStorage.removeItem(KEY)
}- [ ] Step 4: Run tests to verify they pass
Run: npx vitest run apps/admin/src/lib/__tests__/currentMerchant.test.js
Expected: all 13 tests pass.
- [ ] Step 5: Commit
git add apps/admin/src/lib/currentMerchant.js apps/admin/src/lib/__tests__/currentMerchant.test.js
git commit -m "feat(admin): add currentMerchant localStorage helper
Pure-JS module for persisting admin's 'currently working on' merchant.
LRU recent list capped at 8, defensive against corrupted JSON, no React
or Firebase dependencies."Task 2: Add breadcrumb prop to PageHeader โ
Files:
Modify:
apps/admin/src/components/PageHeader.jsx[ ] Step 1: Add the
breadcrumbprop
Open apps/admin/src/components/PageHeader.jsx. The current file (14 lines):
// eslint-disable-next-line unused-imports/no-unused-imports
import React from 'react'
export default function PageHeader({ title, subtitle, actions }) {
return (
<div className="page-header-bar">
<div className="page-header-bar__titleblock">
<h1 className="page-header-bar__title">{title}</h1>
{subtitle && <p className="page-header-bar__subtitle">{subtitle}</p>}
</div>
{actions && <div className="page-header-bar__actions">{actions}</div>}
</div>
)
}Change to:
// eslint-disable-next-line unused-imports/no-unused-imports
import React from 'react'
export default function PageHeader({ breadcrumb, title, subtitle, actions }) {
return (
<div className="page-header-bar">
<div className="page-header-bar__titleblock">
{breadcrumb && <div className="page-header-bar__breadcrumb">{breadcrumb}</div>}
<h1 className="page-header-bar__title">{title}</h1>
{subtitle && <p className="page-header-bar__subtitle">{subtitle}</p>}
</div>
{actions && <div className="page-header-bar__actions">{actions}</div>}
</div>
)
}- [ ] Step 2: Add the breadcrumb CSS
Open apps/admin/src/styles.css. Find the .page-header-bar__subtitle rule. Add the breadcrumb rule immediately before it:
.page-header-bar__breadcrumb {
font-size: 0.75rem;
font-weight: 500;
margin-bottom: 2px;
}
.page-header-bar__breadcrumb a {
color: var(--muted);
text-decoration: none;
}
.page-header-bar__breadcrumb a:hover {
color: var(--text);
}- [ ] Step 3: Commit
git add apps/admin/src/components/PageHeader.jsx apps/admin/src/styles.css
git commit -m "feat(admin): add optional breadcrumb prop to PageHeader
Renders a small breadcrumb line above the title when provided. Backward-
compatible โ existing PageHeader consumers are unaffected."Task 3: MerchantsIndexRedirect (TDD) โ
Files:
Create:
apps/admin/src/components/merchants/__tests__/MerchantsIndexRedirect.test.jsxCreate:
apps/admin/src/components/merchants/MerchantsIndexRedirect.jsx[ ] Step 1: Write the failing tests
Create apps/admin/src/components/merchants/__tests__/MerchantsIndexRedirect.test.jsx:
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import { MemoryRouter, Routes, Route } from 'react-router-dom'
import MerchantsIndexRedirect from '../MerchantsIndexRedirect'
// Mock the localStorage helper
vi.mock('../../../lib/currentMerchant', () => ({
getCurrentMerchantId: vi.fn(() => null),
}))
import { getCurrentMerchantId } from '../../../lib/currentMerchant'
function renderAt(path, { user = {}, isAdmin = true } = {}) {
return render(
<MemoryRouter initialEntries={[path]}>
<Routes>
<Route path="/merchants" element={<MerchantsIndexRedirect user={user} isAdmin={isAdmin} />} />
<Route path="/merchants/all" element={<div data-testid="landed">all</div>} />
<Route path="/merchants/:id/overview" element={<div data-testid="landed">detail</div>} />
</Routes>
</MemoryRouter>
)
}
beforeEach(() => {
vi.clearAllMocks()
localStorage.clear()
})
describe('MerchantsIndexRedirect', () => {
it('admin with no stored merchant โ /merchants/all', () => {
getCurrentMerchantId.mockReturnValue(null)
renderAt('/merchants', { isAdmin: true })
expect(screen.getByTestId('landed')).toHaveTextContent('all')
})
it('admin with stored merchant โ /merchants/:id/overview', () => {
getCurrentMerchantId.mockReturnValue('merch_abc')
renderAt('/merchants', { isAdmin: true })
expect(screen.getByTestId('landed')).toHaveTextContent('detail')
})
it('merchant-role with merchantId โ /merchants/:id/overview', () => {
renderAt('/merchants', { user: { merchantId: 'merch_xyz' }, isAdmin: false })
expect(screen.getByTestId('landed')).toHaveTextContent('detail')
})
it('merchant-role without merchantId โ shows misconfigured message', () => {
renderAt('/merchants', { user: {}, isAdmin: false })
expect(screen.getByText(/merchant account is misconfigured/i)).toBeInTheDocument()
})
})- [ ] Step 2: Run tests to verify they fail
Run: npx vitest run apps/admin/src/components/merchants/__tests__/MerchantsIndexRedirect.test.jsx
Expected: FAIL โ module not found.
- [ ] Step 3: Implement
MerchantsIndexRedirect
Create apps/admin/src/components/merchants/MerchantsIndexRedirect.jsx:
// eslint-disable-next-line unused-imports/no-unused-imports
import React from 'react'
import { Navigate } from 'react-router-dom'
import { LogOut } from 'lucide-react'
import { getCurrentMerchantId } from '../../lib/currentMerchant'
import { signOut } from '../../firebase'
export default function MerchantsIndexRedirect({ user, isAdmin }) {
if (!isAdmin) {
if (user?.merchantId) {
return <Navigate to={`/merchants/${user.merchantId}/overview`} replace />
}
return <MerchantAccountMisconfigured />
}
const currentId = getCurrentMerchantId()
return currentId
? <Navigate to={`/merchants/${currentId}/overview`} replace />
: <Navigate to="/merchants/all" replace />
}
function MerchantAccountMisconfigured() {
return (
<div className="user-management-container">
<div className="user-management-main" style={{ paddingTop: 'var(--space-8)' }}>
<div className="feature-card" style={{ maxWidth: 480 }}>
<h3>Merchant account is misconfigured</h3>
<p className="text-muted" style={{ marginTop: 'var(--space-2)' }}>
Your account doesn't have a linked merchant profile. Please contact support to
resolve this.
</p>
<button
className="btn btn-secondary btn-sm"
style={{ marginTop: 'var(--space-4)' }}
onClick={() => signOut()}
>
<LogOut size={14} />
Sign out
</button>
</div>
</div>
</div>
)
}- [ ] Step 4: Run tests to verify they pass
Run: npx vitest run apps/admin/src/components/merchants/__tests__/MerchantsIndexRedirect.test.jsx
Expected: all 4 tests pass.
- [ ] Step 5: Commit
git add apps/admin/src/components/merchants/MerchantsIndexRedirect.jsx apps/admin/src/components/merchants/__tests__/MerchantsIndexRedirect.test.jsx
git commit -m "feat(admin): add MerchantsIndexRedirect for smart merchant routing
Role-aware redirect at /merchants index: merchant-role users go to their
own detail page, admins go to their last-viewed merchant or /merchants/all.
Includes MerchantAccountMisconfigured fallback for merchant-role users
without a linked merchantId."Task 4: MerchantSwitcher dropdown (TDD) โ
Files:
Create:
apps/admin/src/components/merchants/__tests__/MerchantSwitcher.test.jsxCreate:
apps/admin/src/components/merchants/MerchantSwitcher.jsx[ ] Step 1: Write the failing tests
Create apps/admin/src/components/merchants/__tests__/MerchantSwitcher.test.jsx:
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { MemoryRouter, Routes, Route } from 'react-router-dom'
import MerchantSwitcher from '../MerchantSwitcher'
// Mock the localStorage helper
vi.mock('../../../lib/currentMerchant', () => ({
getRecentMerchantIds: vi.fn(() => []),
}))
// Mock firebase
vi.mock('../../../firebase', () => ({
getMerchantData: vi.fn(),
}))
import { getRecentMerchantIds } from '../../../lib/currentMerchant'
import { getMerchantData } from '../../../firebase'
function renderSwitcher(path = '/merchants/all') {
return render(
<MemoryRouter initialEntries={[path]}>
<Routes>
<Route path="/merchants/all" element={<MerchantSwitcher />} />
<Route path="/merchants/create" element={<div data-testid="landed">create</div>} />
<Route path="/merchants/:id/overview" element={<MerchantSwitcher />} />
</Routes>
</MemoryRouter>
)
}
beforeEach(() => {
vi.clearAllMocks()
getRecentMerchantIds.mockReturnValue([])
})
describe('MerchantSwitcher', () => {
it('renders a trigger button', () => {
renderSwitcher()
expect(screen.getByRole('button', { name: /switch merchant/i })).toBeInTheDocument()
})
it('shows empty state when no recents and dropdown is open', async () => {
const user = userEvent.setup()
renderSwitcher()
await user.click(screen.getByRole('button', { name: /switch merchant/i }))
expect(screen.getByText(/no recent merchants/i)).toBeInTheDocument()
expect(screen.getByText(/view all merchants/i)).toBeInTheDocument()
})
it('shows recent merchants when available', async () => {
const user = userEvent.setup()
getRecentMerchantIds.mockReturnValue(['merch_1', 'merch_2'])
getMerchantData.mockImplementation((id) =>
Promise.resolve({ merchant: { businessName: `Business ${id}` } })
)
renderSwitcher('/merchants/merch_1/overview')
await user.click(screen.getByRole('button', { name: /switch merchant/i }))
await waitFor(() => {
expect(screen.getByText('Business merch_2')).toBeInTheDocument()
})
})
it('closes dropdown when clicking outside', async () => {
const user = userEvent.setup()
renderSwitcher()
await user.click(screen.getByRole('button', { name: /switch merchant/i }))
expect(screen.getByText(/no recent merchants/i)).toBeInTheDocument()
// Click outside
await user.click(document.body)
expect(screen.queryByText(/no recent merchants/i)).not.toBeInTheDocument()
})
})- [ ] Step 2: Run tests to verify they fail
Run: npx vitest run apps/admin/src/components/merchants/__tests__/MerchantSwitcher.test.jsx
Expected: FAIL โ module not found.
- [ ] Step 3: Implement
MerchantSwitcher
Create apps/admin/src/components/merchants/MerchantSwitcher.jsx:
// eslint-disable-next-line unused-imports/no-unused-imports
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { ChevronDown, Check, Plus } from 'lucide-react'
import { getRecentMerchantIds } from '../../lib/currentMerchant'
import { getMerchantData } from '../../firebase'
const MAX_DISPLAY = 5
export default function MerchantSwitcher() {
const navigate = useNavigate()
const { merchantId: currentId } = useParams()
const [open, setOpen] = useState(false)
const [names, setNames] = useState({})
const dropdownRef = useRef(null)
// Close on outside click
useEffect(() => {
if (!open) return
function handleClick(e) {
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
setOpen(false)
}
}
document.addEventListener('mousedown', handleClick)
return () => document.removeEventListener('mousedown', handleClick)
}, [open])
// Fetch names on open
const handleOpen = useCallback(() => {
setOpen((prev) => {
const willOpen = !prev
if (willOpen) {
const ids = getRecentMerchantIds()
const unknown = ids.filter((id) => !names[id])
if (unknown.length > 0) {
Promise.all(
unknown.map((id) =>
getMerchantData(id)
.then((d) => [id, d.merchant?.businessName || id])
.catch(() => [id, id])
)
).then((entries) => {
setNames((prev) => {
const next = { ...prev }
entries.forEach(([id, name]) => { next[id] = name })
return next
})
})
}
}
return willOpen
})
}, [names])
const recentIds = getRecentMerchantIds()
// Exclude the currently viewed merchant from the "recent" list
const otherRecent = recentIds.filter((id) => id !== currentId).slice(0, MAX_DISPLAY)
return (
<div ref={dropdownRef} style={{ position: 'relative', display: 'inline-block' }}>
<button
type="button"
className="btn btn-secondary btn-sm"
onClick={handleOpen}
aria-label="Switch merchant"
aria-expanded={open}
aria-haspopup="true"
>
<ChevronDown size={14} />
</button>
{open && (
<div className="merchant-switcher-dropdown">
{currentId && names[currentId] && (
<>
<div className="merchant-switcher-section-label">Currently viewing</div>
<div className="merchant-switcher-item merchant-switcher-item--current">
<Check size={12} />
<span>{names[currentId]}</span>
</div>
</>
)}
{otherRecent.length > 0 ? (
<>
<div className="merchant-switcher-section-label">Recent</div>
{otherRecent.map((id) => (
<button
key={id}
type="button"
className="merchant-switcher-item"
onClick={() => {
navigate(`/merchants/${id}/overview`)
setOpen(false)
}}
>
<span>{names[id] || id}</span>
</button>
))}
</>
) : (
<div className="merchant-switcher-empty">No recent merchants</div>
)}
<div className="merchant-switcher-divider" />
<button
type="button"
className="merchant-switcher-item"
onClick={() => {
navigate('/merchants/all')
setOpen(false)
}}
>
View all merchants โ
</button>
{otherRecent.length === 0 && (
<button
type="button"
className="merchant-switcher-item"
onClick={() => {
navigate('/merchants/create')
setOpen(false)
}}
>
<Plus size={12} />
<span>Create merchant</span>
</button>
)}
</div>
)}
</div>
)
}- [ ] Step 4: Add dropdown CSS
Add to apps/admin/src/styles.css, near the end of the merchant-related styles (or after .page-header-bar styles):
/* โโ Merchant Switcher dropdown โโ */
.merchant-switcher-dropdown {
position: absolute;
right: 0;
top: calc(100% + 4px);
min-width: 240px;
background: var(--surface);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
padding: var(--space-2) 0;
z-index: 50;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
}
.merchant-switcher-section-label {
font-size: 0.6875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--muted);
padding: var(--space-2) var(--space-3) var(--space-1);
}
.merchant-switcher-item {
display: flex;
align-items: center;
gap: var(--space-2);
width: 100%;
padding: var(--space-2) var(--space-3);
background: none;
border: none;
color: var(--text);
font-size: 0.8125rem;
cursor: pointer;
text-align: left;
}
.merchant-switcher-item:hover {
background: rgba(255, 255, 255, 0.05);
}
.merchant-switcher-item--current {
color: var(--accent-600);
cursor: default;
}
.merchant-switcher-empty {
padding: var(--space-3);
font-size: 0.8125rem;
color: var(--muted);
text-align: center;
}
.merchant-switcher-divider {
height: 1px;
background: rgba(255, 255, 255, 0.05);
margin: var(--space-1) 0;
}- [ ] Step 5: Run tests to verify they pass
Run: npx vitest run apps/admin/src/components/merchants/__tests__/MerchantSwitcher.test.jsx
Expected: all 4 tests pass.
- [ ] Step 6: Commit
git add apps/admin/src/components/merchants/MerchantSwitcher.jsx apps/admin/src/components/merchants/__tests__/MerchantSwitcher.test.jsx apps/admin/src/styles.css
git commit -m "feat(admin): add MerchantSwitcher dropdown for quick merchant switching
Admin-only dropdown showing currently viewed merchant, recent list (up
to 5), and 'View all merchants' link. Names resolved lazily on open.
Positioned in PageHeader actions slot."Task 5: Auth hydration โ load merchantId + clear on sign-out โ
Files:
- Modify:
apps/admin/src/App.jsx
This task adds merchantId to the user object for merchant-role users so MerchantsIndexRedirect can route them to their own merchant. Also wires clearCurrentMerchant() into sign-out.
- [ ] Step 1: Add
merchantIdto the auth hydration
Open apps/admin/src/App.jsx. Find the auth effect (lines 48โ91). After the checkMerchantRole result is set (line 63), add Firestore lookup for merchant-role users.
Change lines 58โ63 from:
const [adminStatus, merchantStatus] = await Promise.all([
checkAdminRole(firebaseUser),
checkMerchantRole(firebaseUser),
])
setIsAdmin(adminStatus)
setIsMerchant(merchantStatus)to:
const [adminStatus, merchantStatus] = await Promise.all([
checkAdminRole(firebaseUser),
checkMerchantRole(firebaseUser),
])
setIsAdmin(adminStatus)
setIsMerchant(merchantStatus)
// For merchant-role (non-admin) users, load their linked merchantId
// so MerchantsIndexRedirect can route to their detail page.
if (merchantStatus && !adminStatus) {
const { doc, getDoc } = await import('firebase/firestore')
const { db } = await import('./firebase')
const userDoc = await getDoc(doc(db, 'users', firebaseUser.uid))
if (userDoc.exists() && userDoc.data().merchantId) {
firebaseUser.merchantId = userDoc.data().merchantId
}
}Note: firebaseUser is a mutable object (Firebase Auth User instance). Attaching merchantId directly is simpler than creating a wrapper object that would need to be threaded through every consumer. This property is only read by MerchantsIndexRedirect.
Important: The db import uses a dynamic import to avoid circular dependency issues if firebase.js is already in the module graph at this point. Check if db is already imported at the top of App.jsx. If db is already available, use the static import instead:
import { db } from './firebase'and use it directly without dynamic import:
if (merchantStatus && !adminStatus) {
const { doc, getDoc } = await import('firebase/firestore')
const userDoc = await getDoc(doc(db, 'users', firebaseUser.uid))
if (userDoc.exists() && userDoc.data().merchantId) {
firebaseUser.merchantId = userDoc.data().merchantId
}
}Verify by checking the imports at the top of App.jsx โ the existing imports (line 6โ11) import from './firebase' but may not include db. If db is not imported, add it to the existing import line.
- [ ] Step 2: Add
clearCurrentMerchantto sign-out
Find the handleSignOut function (around line 138). Change from:
const handleSignOut = async () => {
try {
await signOut()
} catch (err) {
setError(err.message)
}
}to:
const handleSignOut = async () => {
try {
clearCurrentMerchant()
await signOut()
} catch (err) {
setError(err.message)
}
}Add the import at the top of App.jsx:
import { clearCurrentMerchant } from './lib/currentMerchant'- [ ] Step 3: Run admin tests
Run: npx vitest run --config apps/admin/vitest.config.js
Expected: all tests pass. The auth hydration change is an async side effect that existing tests don't exercise (they mock Firebase auth).
- [ ] Step 4: Commit
git add apps/admin/src/App.jsx
git commit -m "feat(admin): hydrate merchantId for merchant-role users, clear on sign-out
Loads users/{uid}.merchantId into the Firebase Auth user object during
auth hydration so MerchantsIndexRedirect can route merchant-role users
to their own detail page. Clears the currentMerchant localStorage entry
on sign-out to prevent stale state across sessions."Task 6: Update MerchantDetail โ breadcrumb, switcher, remember/forget โ
Files:
Modify:
apps/admin/src/components/merchants/MerchantDetail.jsx[ ] Step 1: Add new imports
Open apps/admin/src/components/merchants/MerchantDetail.jsx. Add to the existing imports:
import { ArrowLeft } from 'lucide-react'
import { Link } from 'react-router-dom'
import { rememberMerchant, forgetMerchant } from '../../lib/currentMerchant'
import MerchantSwitcher from './MerchantSwitcher'ArrowLeft is for the breadcrumb. Link is for the breadcrumb anchor (accessible, right-click โ open-in-new-tab friendly). rememberMerchant/forgetMerchant are for the localStorage side effects.
Also update the function signature to accept isAdmin:
Change:
export default function MerchantDetail() {to:
export default function MerchantDetail({ isAdmin }) {- [ ] Step 2: Add
rememberMerchanteffect
After the existing data-fetching useEffect (around line 67), add:
// Persist this merchant as "current" for admin's smart redirect + switcher.
useEffect(() => {
if (isAdmin && merchantId) {
rememberMerchant(merchantId)
}
}, [isAdmin, merchantId])- [ ] Step 3: Update the 404 branch with self-healing
Find the error/404 branch (around lines 142โ159). Change from:
if (error || !data.merchant) {
return (
<div className="user-management-container">
<div className="user-management-main">
<div className="page-header">
<div>
<h1>Merchant not found</h1>
<p className="text-muted">
{error || `No merchant account exists with ID ${merchantId}.`}
</p>
</div>
</div>
<button className="btn btn-secondary" onClick={() => navigate('/merchants/all')}>
Back to All Merchants
</button>
</div>
</div>
)
}to:
if (error || !data.merchant) {
// If this was a stale localStorage redirect, silently evict and redirect.
if (!error && !data.merchant && isAdmin) {
forgetMerchant(merchantId)
return <Navigate to="/merchants/all" replace />
}
return (
<div className="user-management-container">
<div className="user-management-main">
<PageHeader
title="Merchant not found"
subtitle={error || `No merchant account exists with ID ${merchantId}.`}
/>
{isAdmin ? (
<button className="btn btn-secondary" onClick={() => navigate('/merchants/all')}>
Back to All Merchants
</button>
) : (
<div className="feature-card" style={{ maxWidth: 480 }}>
<h3>Your merchant account references a business that no longer exists.</h3>
<p className="text-muted" style={{ marginTop: 'var(--space-2)' }}>
Please contact support to resolve this.
</p>
</div>
)}
</div>
</div>
)
}Also add the Navigate import โ it's already in the import line from react-router-dom. Check line 1: if Navigate is not already imported, add it:
import { useParams, useSearchParams, useLocation, useNavigate, Outlet, Navigate } from 'react-router-dom'- [ ] Step 4: Add breadcrumb and switcher to the main render
Find the main render block's <PageHeader> usage (around lines 221โ225). Change from:
<PageHeader
title={businessName}
subtitle="Merchant account details"
actions={headerActions}
/>to:
<PageHeader
breadcrumb={
isAdmin ? (
<Link to="/merchants/all">
<ArrowLeft size={12} style={{ verticalAlign: 'middle', marginRight: 4 }} />
Merchants
</Link>
) : null
}
title={businessName}
subtitle="Merchant account details"
actions={
<>
{headerActions}
{isAdmin && <MerchantSwitcher />}
</>
}
/>- [ ] Step 5: Run admin tests
Run: npx vitest run --config apps/admin/vitest.config.js
Expected: all tests pass.
- [ ] Step 6: Commit
git add apps/admin/src/components/merchants/MerchantDetail.jsx
git commit -m "feat(admin): add breadcrumb, switcher, and remember/forget to MerchantDetail
Admin-only breadcrumb 'โ Merchants' links to /merchants/all. MerchantSwitcher
renders in the header actions. rememberMerchant(id) is called on mount/
param change to persist the working context. 404 branch self-heals stale
localStorage by calling forgetMerchant and redirecting to /merchants/all."Task 7: Give MerchantsAll and MerchantsCreate their own headers โ
Files:
- Modify:
apps/admin/src/components/merchants/MerchantsAll.jsx - Modify:
apps/admin/src/components/merchants/MerchantsCreate.jsx
After MerchantsListLayout is deleted (Task 8), these pages need their own <PageHeader /> since no parent layout provides one.
- [ ] Step 1: Update
MerchantsAll.jsx
Open apps/admin/src/components/merchants/MerchantsAll.jsx.
Add imports at the top (after existing imports):
import { Plus, Store } from 'lucide-react'
import PageHeader from '../PageHeader'
import MerchantSwitcher from './MerchantSwitcher'Wrap the existing return JSX in the page shell and add PageHeader. Change the return block from:
return (
<>
<div className="form-group" style={{ marginBottom: 'var(--space-4)' }}>
<input ... />
</div>
{loading && ...}
{error && ...}
{!loading && !error && filtered.length === 0 && (
<div className="feature-card" style={{ maxWidth: 560 }}>
<div className="feature-card-icon">๐ช</div>
...to:
return (
<div className="user-management-container">
<div className="user-management-main">
<PageHeader
title="Merchants"
subtitle="Select a merchant to view details, or create a new one."
actions={
<>
<button
className="btn btn-primary btn-sm"
onClick={() => navigate('/merchants/create')}
>
<Plus size={14} />
Create Merchant
</button>
<MerchantSwitcher />
</>
}
/>
<div className="user-management-body">
<div className="form-group" style={{ marginBottom: 'var(--space-4)' }}>
<input ... />
</div>
{loading && ...}
{error && ...}
{!loading && !error && filtered.length === 0 && (
<div className="feature-card" style={{ maxWidth: 560 }}>
<div className="feature-card-icon"><Store size={32} /></div>
...Full replacement of the return block โ here is the complete new return:
return (
<div className="user-management-container">
<div className="user-management-main">
<PageHeader
title="Merchants"
subtitle="Select a merchant to view details, or create a new one."
actions={
<>
<button
className="btn btn-primary btn-sm"
onClick={() => navigate('/merchants/create')}
>
<Plus size={14} />
Create Merchant
</button>
<MerchantSwitcher />
</>
}
/>
<div className="user-management-body">
<div className="form-group" style={{ marginBottom: 'var(--space-4)' }}>
<input
type="search"
className="form-input"
placeholder="Search by business name, email, or contactโฆ"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
{loading && <p className="text-muted">Loading merchantsโฆ</p>}
{error && <div className="form-error">{error}</div>}
{!loading && !error && filtered.length === 0 && (
<div className="feature-card" style={{ maxWidth: 560 }}>
<div className="feature-card-icon"><Store size={32} /></div>
<h3>{search ? 'No merchants match your search' : 'No merchants yet'}</h3>
<p>
{search
? 'Try a different search term, or clear the filter to see all merchants.'
: 'Create your first merchant account to get started.'}
</p>
</div>
)}
{!loading && !error && filtered.length > 0 && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-3)' }}>
{filtered.map((m) => (
<MerchantRow
key={m.merchantId}
merchant={m}
onClick={() => navigate(`/merchants/${m.merchantId}`)}
/>
))}
</div>
)}
{hasMore && !search && (
<div style={{ textAlign: 'center', marginTop: 'var(--space-4)' }}>
<button
className="btn btn-secondary"
onClick={() => load({ append: true })}
disabled={loadingMore}
>
{loadingMore ? 'Loadingโฆ' : 'Load more'}
</button>
</div>
)}
</div>
</div>
</div>
)- [ ] Step 2: Update
MerchantsCreate.jsx
Open apps/admin/src/components/merchants/MerchantsCreate.jsx. Change from:
// eslint-disable-next-line unused-imports/no-unused-imports
import React from 'react'
import CreateMerchantForm from '../CreateMerchantForm'
/**
* Merchants > Create Merchant (admin-only).
*
* Renders the CreateMerchantForm inside the MerchantsListLayout shell. The layout
* swaps its title to "Create Merchant" and hides the tab strip for this route.
*
* Route-level role gating happens upstream โ the merchant redirect guard blocks
* direct URL access for non-admins.
*/
export default function MerchantsCreate() {
return <CreateMerchantForm />
}to:
// eslint-disable-next-line unused-imports/no-unused-imports
import React from 'react'
import { useNavigate } from 'react-router-dom'
import PageHeader from '../PageHeader'
import CreateMerchantForm from '../CreateMerchantForm'
/**
* Merchants > Create Merchant (admin-only).
*
* Renders the CreateMerchantForm with its own PageHeader. Route-level role
* gating happens upstream โ the merchant redirect guard blocks direct URL
* access for non-admins.
*/
export default function MerchantsCreate() {
const navigate = useNavigate()
return (
<div className="user-management-container">
<div className="user-management-main">
<PageHeader
title="Create Merchant"
actions={
<button
className="btn btn-secondary btn-sm"
onClick={() => navigate('/merchants/all')}
>
Cancel
</button>
}
/>
<div className="user-management-body">
<CreateMerchantForm />
</div>
</div>
</div>
)
}- [ ] Step 3: Run admin tests
Run: npx vitest run --config apps/admin/vitest.config.js
Expected: all tests pass (these pages have no unit tests; this is a render-only change).
- [ ] Step 4: Commit
git add apps/admin/src/components/merchants/MerchantsAll.jsx apps/admin/src/components/merchants/MerchantsCreate.jsx
git commit -m "feat(admin): give MerchantsAll and MerchantsCreate their own PageHeaders
Each page renders its own header since the shared MerchantsListLayout is
being removed. MerchantsAll shows title, subtitle, Create CTA, and the
MerchantSwitcher. MerchantsCreate shows title and Cancel button. Also
replaces the empty-state emoji with a Lucide Store icon."Task 8: Rewire routing and delete dead files โ
Files:
Modify:
apps/admin/src/components/AdminDashboard.jsxDelete:
apps/admin/src/components/merchants/MerchantsListLayout.jsxDelete:
apps/admin/src/components/merchants/__tests__/MerchantsListLayout.test.jsxDelete:
apps/admin/src/components/merchants/MerchantsOverview.jsxDelete:
apps/admin/src/components/merchants/MerchantsOffers.jsx[ ] Step 1: Update imports in
AdminDashboard.jsx
Open apps/admin/src/components/AdminDashboard.jsx. Find the merchant-related imports at the top.
Remove these imports:
import MerchantsListLayout from './merchants/MerchantsListLayout'
import MerchantsOverview from './merchants/MerchantsOverview'
import MerchantsOffers from './merchants/MerchantsOffers'Add this import:
import MerchantsIndexRedirect from './merchants/MerchantsIndexRedirect'- [ ] Step 2: Update the merchants routing block
Find the merchants routing block (around lines 599โ615). Change from:
<Route path="/merchants">
<Route element={<MerchantsListLayout isAdmin={isAdmin} />}>
<Route index element={<Navigate to="/merchants/overview" replace />} />
<Route path="overview" element={<MerchantsOverview />} />
<Route path="offers" element={<MerchantsOffers isAdmin={isAdmin} />} />
<Route path="all" element={<MerchantsAll />} />
<Route path="create" element={<MerchantsCreate />} />
</Route>
<Route path=":merchantId" element={<MerchantDetail />}>
<Route index element={<Navigate to="overview" replace />} />
<Route path="overview" element={<MerchantOverviewTab />} />
<Route path="venues" element={<MerchantVenuesTab />} />
<Route path="notes" element={<MerchantNotesTab />} />
<Route path="photos" element={<MerchantPhotosTab />} />
<Route path="address" element={<MerchantAddressTab />} />
</Route>
</Route>to:
<Route path="/merchants">
<Route index element={<MerchantsIndexRedirect user={user} isAdmin={isAdmin} />} />
<Route path="all" element={<MerchantsAll />} />
<Route path="create" element={<MerchantsCreate />} />
<Route path=":merchantId" element={<MerchantDetail isAdmin={isAdmin} />}>
<Route index element={<Navigate to="overview" replace />} />
<Route path="overview" element={<MerchantOverviewTab />} />
<Route path="venues" element={<MerchantVenuesTab />} />
<Route path="notes" element={<MerchantNotesTab />} />
<Route path="photos" element={<MerchantPhotosTab />} />
<Route path="address" element={<MerchantAddressTab />} />
</Route>
</Route>Key changes:
Index route uses
MerchantsIndexRedirectinstead ofNavigate to="/merchants/overview".No
MerchantsListLayoutwrapper โ each page owns its own shell.overviewandoffersroutes removed.MerchantDetailreceivesisAdminprop.[ ] Step 3: Update the
merchantOnlyredirect
Find the merchantOnly effect (around lines 159โ167). Change from:
useEffect(() => {
if (!merchantOnly) return
const allowed = ['/merchants/overview', '/merchants/offers']
if (!allowed.includes(location.pathname)) {
navigate('/merchants/overview', { replace: true })
}
}, [merchantOnly, location.pathname, navigate])to:
useEffect(() => {
if (!merchantOnly) return
// Merchant-role users can only access /merchants (index redirect handles
// routing to their own detail page) and /merchants/:theirMerchantId/*.
if (
location.pathname !== '/merchants' &&
!location.pathname.startsWith(`/merchants/${user?.merchantId}`)
) {
navigate('/merchants', { replace: true })
}
}, [merchantOnly, location.pathname, navigate, user?.merchantId])This allows merchant-role users to access:
/merchants(redirects to their detail viaMerchantsIndexRedirect)/merchants/:theirMerchantId/*(their own detail page and all tabs)
Everything else (including /merchants/all, /merchants/create, other merchants' detail pages) redirects to /merchants.
- [ ] Step 4: Delete the dead files
Run:
rm apps/admin/src/components/merchants/MerchantsListLayout.jsx
rm apps/admin/src/components/merchants/__tests__/MerchantsListLayout.test.jsx
rm apps/admin/src/components/merchants/MerchantsOverview.jsx
rm apps/admin/src/components/merchants/MerchantsOffers.jsx- [ ] Step 5: Verify no dangling references
Run: grep -rn 'MerchantsListLayout\|MerchantsOverview\|MerchantsOffers' apps/admin/src/ --include='*.jsx' --include='*.js'
Expected: no output (no matches). If there are matches, remove or update them.
- [ ] Step 6: Run admin tests
Run: npx vitest run --config apps/admin/vitest.config.js
Expected: all tests pass. The deleted MerchantsListLayout.test.jsx no longer runs. The MerchantsIndexRedirect tests from Task 3 should still pass.
- [ ] Step 7: Commit
git add apps/admin/src/components/AdminDashboard.jsx
git rm apps/admin/src/components/merchants/MerchantsListLayout.jsx apps/admin/src/components/merchants/__tests__/MerchantsListLayout.test.jsx apps/admin/src/components/merchants/MerchantsOverview.jsx apps/admin/src/components/merchants/MerchantsOffers.jsx
git commit -m "refactor(admin): replace shared merchants layout with per-route headers
Deletes MerchantsListLayout (the 3-tab shell) and the two placeholder
pages (MerchantsOverview, MerchantsOffers). Routes now use a smart index
redirect and each page owns its own PageHeader. Merchant-role redirect
updated from the deleted /merchants/overview to /merchants (which the
index redirect resolves to the user's own merchant detail page)."Task 9: Full validation sweep โ
No code changes โ validation and visual verification.
- [ ] Step 1: Run the project-wide validator
Run: npm run validate -- --workspace apps/admin
Expected: all checks pass (lint, format, tests, build, audit).
If anything fails, fix inline and commit the fix separately.
- [ ] Step 2: Visual smoke test (admin user)
Run: npm run dev -w apps/admin
Manual checks:
/merchantswith no localStorage โ lands on/merchants/all. Header: "Merchants" / "Select a merchant..." /[+ Create Merchant]+ switcher button. Switcher shows "No recent merchants" + "View all merchants" + "Create merchant".- Click a merchant row โ
/merchants/:id/overview. Header: breadcrumbโ Merchants, merchant name,[Back to All][Edit]+ switcher.PageTabsshows 5 tabs. - Click switcher โ "Currently viewing: (name)" + "View all merchants โ".
- Navigate to
/merchantsagain โ smart-redirects to the merchant you just viewed (localStoragecurrent). - Click
[+ Create Merchant]on/merchants/allโ/merchants/create. Header: "Create Merchant" /[Cancel]. Cancel โ back to/merchants/all. - Browser back/forward preserves correct state at each stop.
- Hard-reload on
/merchants/:id/venuesโ breadcrumb, switcher, and Venues tab all correct.
- [ ] Step 3: Visual smoke test (merchant-role user)
If a merchant-role test account is available:
- Sign in โ automatically lands on
/merchants/:theirMerchantId/overview. - No breadcrumb. No switcher. No
[+ Create Merchant]. - PageTabs visible with all 5 tabs.
- Direct nav to
/merchants/allโ redirected to/merchantsโ redirected to own detail. - Direct nav to
/merchants/other-id/overviewโ redirected to/merchantsโ own detail.
If no merchant-role account is available, flag for PR reviewer.
- [ ] Step 4: Cross-page regression
Confirm non-merchants pages unaffected:
/usersโ UserManagement loads, sidebar active./billingโ Billing loads./systemโ SystemHealth loads./โ DashboardHome renders. "Merchant Management" card navigates to/merchants.
- [ ] Step 5: Sign-out verification
- Visit several merchants to populate localStorage.
- Sign out.
- Open devtools โ Application โ localStorage โ
lantern.admin.currentMerchantshould be absent. - Sign back in โ
/merchantsโ/merchants/all(no stored merchant).
Self-review โ
Spec coverage check:
| Spec section | Task |
|---|---|
| ยง1 Two distinct chromes | Tasks 6 (per-merchant) + 7 (cross-merchant) |
| ยง2 URL/routing changes | Task 8 step 2 |
ยง2 Routes removed (/merchants/overview, /merchants/offers) | Task 8 steps 2, 4 |
| ยง3 Current-merchant persistence (localStorage) | Task 1 |
ยง3 getCurrentMerchantId / getRecentMerchantIds / rememberMerchant / forgetMerchant / clearCurrentMerchant | Task 1 step 3 |
| ยง3 Storage cap 8, display 5 | Task 1 (cap) + Task 4 (MAX_DISPLAY = 5) |
ยง4 Smart redirect (MerchantsIndexRedirect) | Task 3 |
| ยง4 Optimistic redirect, self-healing 404 | Task 6 step 3 |
ยง4 Auth-state prerequisite (merchantId on user) | Task 5 step 1 |
ยง4 MerchantAccountMisconfigured | Task 3 step 3 |
| ยง5 Switcher dropdown | Task 4 |
ยง5 "Currently viewing" from useParams, not localStorage | Task 4 step 3 (uses useParams().merchantId) |
ยง5 Name resolution via getMerchantData | Task 4 step 3 |
| ยง5 Empty state (no recent) | Task 4 step 3 + tests |
ยง6 Breadcrumb (โ Merchants โ /merchants/all) | Task 2 (prop) + Task 6 step 4 (usage) |
| ยง6 Breadcrumb hidden for merchant-role | Task 6 step 4 (isAdmin gate) |
ยง7 Per-route headers for MerchantsAll / MerchantsCreate | Task 7 |
ยง7 MerchantDetail gains breadcrumb + switcher | Task 6 step 4 |
ยงโ clearCurrentMerchant on sign-out | Task 5 step 2 |
ยงโ merchantOnly redirect updated | Task 8 step 3 |
ยงโ Delete MerchantsListLayout, MerchantsOverview, MerchantsOffers | Task 8 steps 1, 4 |
ยงโ Delete MerchantsListLayout.test.jsx | Task 8 step 4 |
ยงโ Automated tests: currentMerchant.test.js | Task 1 |
ยงโ Automated tests: MerchantsIndexRedirect.test.jsx | Task 3 |
ยงโ Automated tests: MerchantSwitcher.test.jsx | Task 4 |
No gaps.
Placeholder scan: No TBDs, TODOs, or vague instructions. Every code step has complete code.
Type consistency:
MerchantsIndexRedirecttakes{ user, isAdmin }โ matches Task 8 wiring (user={user} isAdmin={isAdmin}).MerchantDetailtakes{ isAdmin }โ matches Task 8 wiring (isAdmin={isAdmin}).MerchantSwitchertakes no props โ usesuseParams().merchantIdinternally. Consistent across Tasks 4, 6, 7.currentMerchant.jsexports 5 functions โ all consumed by name in Tasks 3, 4, 5, 6.PageHeadergainsbreadcrumbprop โ backward-compatible, used only byMerchantDetail(Task 6).forgetMerchant/rememberMerchant/clearCurrentMerchantnames match between Task 1 definition and Tasks 3, 5, 6 consumption.- Lucide icons:
ArrowLeft,ChevronDown,Check,Plus,LogOut,Storeโ all exist inlucide-react.
No inconsistencies found.