fix: navigation correctness - back buttons, exit dialog, dedup nav, redirects

- Standardize all procedural back/exit paths to /trees (not /my-trees)
- Add exit button with ConfirmDialog to procedural session top bar
- Consolidate duplicate account links in sidebar and topbar
- Auto-redirect non-owners to personal analytics
- Add toast feedback before silent permission redirects in tree editor
- Delete orphaned AdminCategoriesPage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-19 14:26:49 -05:00
parent 78b4242a76
commit 779dff5b5e
9 changed files with 482 additions and 283 deletions

View File

@@ -0,0 +1,441 @@
# ResolutionFlow UX Deep Dive — Final Merged Implementation Plan
**Date:** 2026-02-19
**Author:** Michael Chihlas
**Purpose:** Comprehensive frontend UX sweep merging the original 35-issue audit with Codex-revised plan. Ordered by user impact, covering broken functionality, navigation, shared components, visual consistency, and backend alignment.
---
## Locked Decisions
These decisions are finalized and should not be revisited:
1. **Canonical post-editor/post-session destination:** `/trees` (the full flow library). `/my-trees` remains available for creator-specific management but is not a navigation target from editors or sessions.
2. **Step Library handling:** Add a placeholder route now. Not hidden, not fully built.
3. **Cleanup strategy:** Immediate removal of dead code and unused types. No staged deprecation. One final `grep` sweep before each deletion to confirm zero references.
---
## Phase 0 — Tree Editor Authoring Blockers (Immediate)
**Goal:** Remove "can't reach bottom / too busy form" friction before the broader UX sweep. This phase was absent from the original audit and added by the Codex review.
### 0.1 Fix node editor scroll trap and bottom clipping
**File:** `NodeEditorPanel.tsx`
- Replace fixed viewport math (`h-[calc(100vh-105px)]`) with `h-full min-h-0`
- Keep body as the only scroll container (`min-h-0 flex-1 overflow-y-auto`)
- Make footer sticky (`sticky bottom-0`) so Save/Cancel are always reachable
- Add `scroll-pb-24` on form body to prevent bottom fields hiding behind footer
### 0.2 Reduce instruction density in decision/action/resolution forms
**Files:** `NodeFormDecision.tsx`, `NodeFormAction.tsx`, `NodeFormResolution.tsx`
- Convert long instructional copy to compact labels + InfoTip tooltips
- Keep one short contextual hint per form section
- Remove large always-visible prose blocks
### 0.3 Keep answer-first branching flow explicit
**File:** `NodeEditorPanel.tsx`
- Preserve existing behavior: saving decision options without `next_node_id` auto-creates answer stubs
- Add UI hint in decision form: "Options become answer placeholders you type later."
### Phase 0 Verification
- Open a long decision form → verify full vertical scroll to footer fields/buttons
- Focus bottom fields → confirm they are visible (not clipped behind footer)
- Decision instructions are compact with info tooltips (no walls of text)
- Save a decision with two new options → two answer placeholders are auto-created
---
## Phase 1 — Broken Functionality (Critical)
**Goal:** Fix features that are actively broken or silently wrong right now.
### 1.1 Register /step-library route
Sidebar links to `/step-library` but no route exists — users hit a blank page.
**Changes:**
- `router.tsx` — add route with lazy-loaded `StepLibraryPage.tsx` placeholder shell
- `StepLibraryPage.tsx` — create placeholder page (e.g., "Step Library — Coming Soon" with consistent layout)
- `Sidebar.tsx` — remove `badge="dot"` while placeholder is live (no false "new feature" signal)
### 1.2 Preserve backend auth error detail in login/register flows
Login failures show "Request failed with status code 401" instead of "Invalid credentials."
**File:** `authStore.ts` (lines ~50-54, 63-67)
- Extract `error.response?.data?.detail` before falling back to generic message
- Apply to both login and register error paths
### 1.3 Fix inverted 4xx toast logic and add 429 handling
**File:** `client.ts` (lines ~36-43)
4xx errors with a `detail` message currently suppress the toast entirely (inverted condition). No 429 handler exists.
**Behavior after fix:**
| Status | Action |
|--------|--------|
| 401 | Suppressed (handled by token refresh flow) |
| 429 | Always toast: backend detail or "Rate limit exceeded, please retry shortly." |
| Other 4xx | Toast `detail` if present, else "Invalid request." |
| 5xx | Existing generic server error toast (unchanged) |
### 1.4 Fix role update payload contract mismatch
**File:** `accounts.ts` (line ~28)
Sends `{ role }` but backend schema expects `{ account_role }`. Role changes silently fail with 422.
- Change to `{ account_role: role }`
### 1.5 Fix "Repeat Last Session" broken for non-troubleshooting flows
**File:** `TreeLibraryPage.tsx` (line ~453)
Currently hardcodes `/trees/:id/navigate` — loses prefill state via safety redirect for procedural/maintenance flows.
- Use `getSessionResumePath()` instead of hardcoded path
### Phase 1 Verification
- Step Library nav opens placeholder page from Sidebar, AppLayout mobile nav, and Quick Launch
- Login with bad credentials shows backend detail string (e.g., "Invalid credentials"), not axios error
- Force a 429 response and verify toast appears
- Change a team member's role in Account Settings → confirm it actually saves (requires `team_admin` user)
- Repeat Last Session works correctly for troubleshooting, procedural, and maintenance flow types
---
## Phase 2 — Navigation Correctness (Medium Scope)
**Goal:** Fix cases where users end up at unexpected pages or lack navigation affordances.
### 2.1 Standardize editor/session back and exit targets to /trees
**Files:**
- `ProceduralEditorPage.tsx` (lines ~86, 92, 157) — change `/my-trees``/trees`
- `ProceduralNavigationPage.tsx` (lines ~120, 180, 304, 330) — error, cancel, completion, and intake cancel paths all → `/trees`
### 2.2 Add explicit exit affordance during procedural session execution
Currently there is no way to leave a procedural session mid-execution except the browser back button.
**File:** `ProceduralNavigationPage.tsx` (top bar, ~line 345)
- Add an Exit button to the top bar
- If session has progress, exit opens a `ConfirmDialog` before navigating to `/trees`
- If no progress, navigate directly
### 2.3 Remove duplicate account links in global nav
"Team" and "Settings" both point to `/account`. Same duplication in TopBar dropdown.
**Files:**
- `Sidebar.tsx` (lines ~206-207) — consolidate footer to single "Account" item
- `TopBar.tsx` — consolidate dropdown to single "Account" entry
### 2.4 Improve analytics routing for non-owners
Non-owners click Analytics, hit an access-denied wall, then must click through to personal stats.
**File:** `TeamAnalyticsPage.tsx` (lines ~48-65)
- Auto-redirect non-owner/non-admin users to `/analytics/me`
- Show an informational toast explaining the redirect (e.g., "Viewing your personal analytics")
### 2.5 Add feedback before permission redirects in tree editor
`TreeEditorPage` silently redirects when users lack permission — no feedback shown.
**File:** `TreeEditorPage.tsx` (lines ~143-146, 157-159)
- Add `toast.error("You don't have permission to edit this flow")` before each `navigate()` call
### 2.6 Delete orphaned AdminCategoriesPage
Not connected to any route, superseded by `admin/GlobalCategoriesPage.tsx`.
- Delete `AdminCategoriesPage.tsx`
- Remove its export from `index.ts` if present
### Phase 2 Verification
- Procedural editor Back button → `/trees` (not `/my-trees`)
- Procedural session cancel, exit, error, and completion → all route to `/trees`
- Exit button is visible during procedural execution and prompts confirmation if session has progress
- Sidebar footer shows one "Account" item (not "Team" + "Settings")
- Non-owner clicks Analytics → auto-redirects to `/analytics/me` with toast
- Unauthorized tree edit attempt → toast shown, then redirect
---
## Phase 3 — Shared Components & Quick Consistency Wins (Medium Scope)
**Goal:** Build reusable infrastructure and fix small but important consistency issues.
### 3.1 Create shared Spinner component
Currently 4 different spinner implementations across 20+ files.
**Create:** `Spinner.tsx` with `sm | md | lg` sizes, default `border-t-primary`
**Migrate page-level loading states in:**
- `ProceduralNavigationPage.tsx`
- `ProceduralEditorPage.tsx`
- `TreeEditorPage.tsx`
- `TreeNavigationPage.tsx`
- `SessionHistoryPage.tsx`
- `SessionDetailPage.tsx`
- `MySharesPage.tsx`
- `MyTreesPage.tsx`
- `AccountSettingsPage.tsx`
- `SharedSessionPage.tsx`
- `PageLoader.tsx`
**Deferred:** Leave tiny inline button spinners for later to avoid churn.
### 3.2 Promote EmptyState to shared component
Admin has a well-designed EmptyState; main app uses 3+ ad hoc patterns.
- Move/create `common/EmptyState.tsx`
- Re-export from admin's `EmptyState.tsx` for backward compatibility
- Adopt in: `MySharesPage.tsx`, `SessionHistoryPage.tsx`, `TreeLibraryPage.tsx`
### 3.3 Replace native window.confirm() with design-system ConfirmDialog
3 places use native browser dialogs that break the design system.
**Files:**
- `MySharesPage.tsx` (line ~81)
- `NodeEditorPanel.tsx` (line ~87)
- `FolderSidebar.tsx` (line ~286)
### 3.4 Fix sidebar optimistic unpin bug
State is removed immediately even if API call fails — the flow disappears permanently until page refresh.
**File:** `Sidebar.tsx` (lines ~105-113)
- Move `setPinnedFlows` update to after successful `await` (not before)
### 3.5 Fix PinnedFlow.tree_type missing 'maintenance'
Maintenance flows can be pinned but navigation will use the wrong path.
**Files:**
- `pinnedFlows.ts` (line ~7) — add `'maintenance'` to the `PinnedFlow.tree_type` union type
- `PinnedFlowsSection.tsx` — validate icon/path logic via `getTreeNavigatePath` handles maintenance correctly
### Phase 3 Verification
- All page-level loading states use the shared `Spinner` component (visual consistency)
- Empty states in MyShares, SessionHistory, TreeLibrary use the shared `EmptyState` component
- Deleting a share, removing a node, removing a folder → all show styled dialog (not browser native)
- Unpin a flow while network is down → flow should NOT disappear (reverts on failure)
- Pin a maintenance flow → clicking it navigates to the correct path
---
## Phase 4 — Visual Consistency Sweep (Large Scope — Many Small Changes)
**Goal:** Design system compliance fixes. Each change is small but there are many files.
### 4.1 Fix Sonner toast styling
The `richColors` prop overrides custom CSS with garish built-in green/red backgrounds. Existing custom CSS in `index.css` (lines ~201-251) already defines themed toasts (`bg-card`, `border-border`, colored border accents) but `richColors` overrides them.
**File:** `main.tsx`
- Remove `richColors` prop
- Add `visibleToasts={3}` and `gap={8}`
- Keep existing custom toast CSS in `index.css`; patch only if needed to ensure themed styles fully apply
### 4.2 Typography: Add font-heading to all page H1s
Missing from approximately half the pages.
**Files to update:**
- `MyTreesPage.tsx`
- `TeamAnalyticsPage.tsx`
- `MyAnalyticsPage.tsx`
- `FeedbackPage.tsx`
- `AccountSettingsPage.tsx`
- `TeamCategoriesPage.tsx`
- `admin/PageHeader.tsx`
### 4.3 Typography: Add font-label to TagBadges component
Design system requires Outfit font for tags/badges.
**File:** `TagBadges.tsx`
### 4.4 Fix hardcoded light-mode button in TeamAnalyticsPage
Only hardcoded `bg-white text-black` button in the app — breaks in dark mode.
**File:** `TeamAnalyticsPage.tsx` (line ~59)
- Replace with `bg-gradient-brand text-white`
### 4.5 Fix non-standard focus ring tokens
Analytics selects use `focus:ring-ring` instead of the standard token.
**Files:** `TeamAnalyticsPage.tsx`, `MyAnalyticsPage.tsx`
- Change `focus:ring-ring``focus:ring-primary/20`
### 4.6 Replace deprecated glass-stat style
**File:** `AccountSettingsPage.tsx` (line ~588)
- Replace `glass-stat` with `bg-card border border-border`
### 4.7 Standardize container/padding on analytics pages
Missing responsive padding.
**Files:** `TeamAnalyticsPage.tsx`, `MyAnalyticsPage.tsx`
- Add `container mx-auto px-4 py-6 sm:px-6 sm:py-8`
### Phase 4 Verification
- Toast colors/borders follow custom theme (not Sonner rich presets) — check success, error, and info toasts
- All page H1s use `font-heading` (Plus Jakarta Sans)
- Tag badges use `font-label` (Outfit)
- TeamAnalytics CTA button renders correctly in both light and dark mode
- Focus rings on analytics selects use subtle `primary/20` glow
- No `glass-stat` class remains in the codebase
- Analytics pages have consistent container spacing matching rest of app
---
## Phase 5 — Backend Alignment & Cleanup (Immediate Removals)
**Goal:** API contract fixes and dead code removal. All removals are immediate (per locked decision #3), with a final `grep` sweep before each deletion.
### 5.1 Remove non-functional drafts toggle from library UI
Backend has no `include_drafts` parameter — the toggle does nothing.
**Files:**
- `TreeLibraryPage.tsx` — remove `showDrafts` state and drafts filter UI
- `tree.ts` (`TreeFilters` type) — remove `include_drafts` field
### 5.2 Align invite types with backend schema
`invited_by_id` and `accepted_by_id` not in backend response — always `undefined`.
**File:** `account.ts` — remove `invited_by_id` and `accepted_by_id` from `AccountInvite` type
### 5.3 Remove dead/unused client code
Before deleting each item, run a global `grep` to confirm zero consumers:
| Item | File | What to Remove |
|------|------|----------------|
| `pinnedFlowsApi.pin()` | `pinnedFlows.ts` | Dead method (never called) |
| `pinnedFlowsApi.reorder()` | `pinnedFlows.ts` | Dead method (never called) |
| `treesApi.getSharedTree()` | `trees.ts` | Dead method (no route/consumer) |
| `SessionListResponse` | `sessions.ts` | Unused type |
| `RatingCreate.is_verified_use` | `step.ts` | Field ignored by backend |
| `AdminCategoriesPage.tsx` | — | Orphaned file + export (if not already deleted in Phase 2) |
### 5.4 Add session list truncation indicator
Session history silently truncates at the backend limit with no indication to the user.
**File:** `SessionHistoryPage.tsx`
- Request `size=51` from backend
- If result length is 51: show "Showing first 50 sessions" indicator, render only first 50
- If result length ≤ 50: show "Showing X sessions"
- No backend API contract changes required (frontend lookahead strategy)
### Phase 5 Verification
- No drafts toggle visible in TreeLibrary UI
- `grep -r "include_drafts" frontend/src/` returns zero results
- `grep -r "invited_by_id\|accepted_by_id" frontend/src/` returns zero results for `AccountInvite` usage
- `grep` for each removed method/type confirms zero references
- Session history shows truncation indicator at 50+ results
- Session history shows "Showing X sessions" at fewer than 50 results
- `cd frontend && npm run build` passes with zero errors
---
## Items Explicitly Deferred
| Item | Reason |
|------|--------|
| Migrate 14+ custom modals to shared Modal component | Very high effort, low breakage risk. Migrate incrementally on new work. |
| Standardize input border radius app-wide | Cosmetic, low user impact |
| Standardize icon sizing method (`className` vs `size` prop) | No visual difference |
| Session pagination (full load-more) | Larger feature, beyond UX sweep scope |
| Inline button spinners | Leave for later to avoid churn (Phase 3 note) |
| Full Step Library feature build | Intentionally placeholder-only this cycle |
---
## Public APIs / Interfaces / Types Changed
**Frontend routing:**
- Add new route `/step-library` in `router.tsx`
**Type/interface updates:**
- `TreeFilters` in `tree.ts`: remove `include_drafts`
- `AccountInvite` in `account.ts`: remove `invited_by_id`, `accepted_by_id`
- `PinnedFlow.tree_type` in `pinnedFlows.ts`: add `'maintenance'`
- `RatingCreate` in `step.ts`: remove `is_verified_use`
- Remove unused `SessionListResponse` in `sessions.ts`
**Shared component surface:**
- Add `Spinner` component in `Spinner.tsx`
- Add common `EmptyState` component in `EmptyState.tsx`
---
## Cross-Phase Verification Checklist
Run after each phase is complete:
1. **Build:** `cd frontend && npm run build` — must pass with zero errors
2. **Navigation:** Test all sidebar items, back buttons, editor → library flow, procedural navigation exit
3. **Auth:** Intentional wrong password → shows backend error detail
4. **Role change:** Test in Account Settings (requires `team_admin` user)
5. **Visual spot-check:** H1 fonts, spinner consistency, empty states, toast styling, dark mode
6. **Grep sweep:** Before merging each phase, confirm no dead references remain for removed items
---
## Assumptions
- `/trees` is the canonical destination for broad flow browsing and all post-task returns
- `/my-trees` remains available but is not a default navigation target
- Step Library is placeholder-only this cycle — no full feature work
- No backend API contract changes are required for any item in this plan
- Session truncation uses the frontend lookahead strategy (`size=51`)
- All cleanup is immediate with pre-deletion reference verification

View File

@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
import { LayoutGrid, Box, PenLine, Clock, FileText, Bookmark, BarChart3, Users, Settings, PanelLeftClose, PanelLeftOpen, MessageSquareText } from 'lucide-react'
import { LayoutGrid, Box, PenLine, Clock, FileText, Bookmark, BarChart3, Settings, PanelLeftClose, PanelLeftOpen, MessageSquareText } from 'lucide-react'
import { cn } from '@/lib/utils'
import { useUserPreferencesStore } from '@/store/userPreferencesStore'
import { CategoryList } from '@/components/sidebar/CategoryList'
@@ -203,8 +203,7 @@ export function Sidebar() {
{!sidebarCollapsed && (
<>
<NavItem href="/feedback" icon={MessageSquareText} label="Feedback" />
<NavItem href="/account" icon={Users} label="Team" />
<NavItem href="/account" icon={Settings} label="Settings" />
<NavItem href="/account" icon={Settings} label="Account" />
</>
)}
<button

View File

@@ -1,6 +1,6 @@
import { useState, useRef, useEffect, useCallback } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { Search, Zap, LogOut, User, Shield, Settings } from 'lucide-react'
import { Search, Zap, LogOut, Shield, Settings } from 'lucide-react'
import { useAuthStore } from '@/store/authStore'
import { usePermissions } from '@/hooks/usePermissions'
import { BrandLogo } from '@/components/common/BrandLogo'
@@ -122,21 +122,13 @@ export function TopBar() {
</span>
)}
</div>
<Link
to="/account"
onClick={() => setUserMenuOpen(false)}
className="flex items-center gap-2 rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
>
<User size={14} />
Account
</Link>
<Link
to="/account"
onClick={() => setUserMenuOpen(false)}
className="flex items-center gap-2 rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
>
<Settings size={14} />
Settings
Account
</Link>
{isSuperAdmin && (
<Link

View File

@@ -1,242 +0,0 @@
import { useState, useEffect } from 'react'
import { Plus } from 'lucide-react'
import { DndContext, closestCenter } from '@dnd-kit/core'
import type { DragEndEvent } from '@dnd-kit/core'
import { SortableContext, verticalListSortingStrategy, arrayMove } from '@dnd-kit/sortable'
import { stepCategoriesApi } from '@/api/stepCategories'
import { stepsApi } from '@/api/steps'
import { CategoryRow } from '@/components/admin/CategoryRow'
import { CreateCategoryModal } from '@/components/admin/CreateCategoryModal'
import { EditCategoryModal } from '@/components/admin/EditCategoryModal'
import type { StepCategoryListItem } from '@/types'
import { cn } from '@/lib/utils'
import { toast } from '@/lib/toast'
export function AdminCategoriesPage() {
const [categories, setCategories] = useState<StepCategoryListItem[]>([])
const [allSteps, setAllSteps] = useState<{ category_id?: string }[]>([])
const [isLoading, setIsLoading] = useState(true)
const [showCreateModal, setShowCreateModal] = useState(false)
const [showEditModal, setShowEditModal] = useState(false)
const [editingCategory, setEditingCategory] = useState<StepCategoryListItem | null>(null)
const [isSaving, setIsSaving] = useState(false)
const [includeArchived, setIncludeArchived] = useState(false)
useEffect(() => {
loadData()
}, [includeArchived])
const loadData = async () => {
setIsLoading(true)
try {
const [categoriesData, stepsData] = await Promise.all([
stepCategoriesApi.list({ include_inactive: includeArchived }),
stepsApi.list({})
])
setCategories(categoriesData)
setAllSteps(stepsData)
} catch (err) {
console.error('Failed to load categories:', err)
toast.error('Failed to load categories')
} finally {
setIsLoading(false)
}
}
const getStepCount = (categoryId: string) => {
return allSteps?.filter(s => s.category_id === categoryId).length || 0
}
const handleCreate = async (data: { name: string; description: string }) => {
setIsSaving(true)
try {
await stepCategoriesApi.create({
name: data.name,
description: data.description || undefined
})
toast.success('Category created successfully')
setShowCreateModal(false)
await loadData()
} catch (err) {
console.error('Failed to create category:', err)
toast.error('Failed to create category')
throw err
} finally {
setIsSaving(false)
}
}
const handleEdit = async (data: { name: string; description: string }) => {
if (!editingCategory) return
setIsSaving(true)
try {
await stepCategoriesApi.update(editingCategory.id, {
name: data.name,
description: data.description || undefined
})
toast.success('Category updated successfully')
setShowEditModal(false)
setEditingCategory(null)
await loadData()
} catch (err) {
console.error('Failed to update category:', err)
toast.error('Failed to update category')
throw err
} finally {
setIsSaving(false)
}
}
const handleArchive = async (id: string) => {
try {
await stepCategoriesApi.archive(id)
toast.success('Category archived')
await loadData()
} catch (err) {
console.error('Failed to archive category:', err)
toast.error('Failed to archive category')
}
}
const handleRestore = async (id: string) => {
try {
await stepCategoriesApi.restore(id)
toast.success('Category restored')
await loadData()
} catch (err) {
console.error('Failed to restore category:', err)
toast.error('Failed to restore category')
}
}
const handleDragEnd = async (event: DragEndEvent) => {
const { active, over } = event
if (!over || active.id === over.id) return
const oldIndex = categories.findIndex(c => c.id === active.id)
const newIndex = categories.findIndex(c => c.id === over.id)
const reordered = arrayMove(categories, oldIndex, newIndex)
// Optimistic update
setCategories(reordered)
try {
// Update display_order for all affected categories
const updates = reordered.map((cat, index) => ({
id: cat.id,
display_order: index
}))
await stepCategoriesApi.updateOrder(updates)
toast.success('Categories reordered')
} catch (err) {
console.error('Failed to reorder categories:', err)
toast.error('Failed to save order')
// Revert on error
await loadData()
}
}
const openEditModal = (category: StepCategoryListItem) => {
setEditingCategory(category)
setShowEditModal(true)
}
if (isLoading) {
return (
<div className="flex h-64 items-center justify-center">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-border border-t-foreground" />
</div>
)
}
return (
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
{/* Header */}
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div>
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">
Step Categories
</h1>
<p className="mt-2 text-muted-foreground">
Manage categories for organizing step library
</p>
</div>
<button
onClick={() => setShowCreateModal(true)}
className={cn(
'flex items-center gap-2 rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium',
'hover:opacity-90'
)}
>
<Plus className="h-4 w-4" />
Create Category
</button>
</div>
{/* Filter Toggle */}
<div className="mb-4">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={includeArchived}
onChange={(e) => setIncludeArchived(e.target.checked)}
className="h-4 w-4 rounded border-border text-foreground focus:ring-2 focus:ring-primary/20 focus:ring-offset-0"
/>
<span className="text-sm text-muted-foreground">Show archived categories</span>
</label>
</div>
{/* Categories List */}
{categories.length === 0 ? (
<div className="bg-card border border-border rounded-xl p-12 text-center">
<p className="text-muted-foreground">
No categories found. Create your first category to get started.
</p>
</div>
) : (
<DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext
items={categories.map(c => c.id)}
strategy={verticalListSortingStrategy}
>
<div className="space-y-3">
{categories.map(category => (
<CategoryRow
key={category.id}
category={category}
stepCount={getStepCount(category.id)}
onEdit={openEditModal}
onArchive={handleArchive}
onRestore={handleRestore}
/>
))}
</div>
</SortableContext>
</DndContext>
)}
{/* Create Modal */}
<CreateCategoryModal
isOpen={showCreateModal}
onClose={() => setShowCreateModal(false)}
onSubmit={handleCreate}
isSaving={isSaving}
/>
{/* Edit Modal */}
<EditCategoryModal
isOpen={showEditModal}
onClose={() => {
setShowEditModal(false)
setEditingCategory(null)
}}
onSubmit={handleEdit}
category={editingCategory}
isSaving={isSaving}
/>
</div>
)
}
export default AdminCategoriesPage

View File

@@ -83,13 +83,13 @@ export function ProceduralEditorPage() {
const tree = await treesApi.get(treeId)
if (tree.tree_type !== 'procedural' && tree.tree_type !== 'maintenance') {
toast.error('This flow is not a procedural or maintenance flow')
navigate('/my-trees')
navigate('/trees')
return
}
loadTree(tree)
} catch {
toast.error('Failed to load flow')
navigate('/my-trees')
navigate('/trees')
}
}
@@ -154,7 +154,7 @@ export function ProceduralEditorPage() {
<div className="flex shrink-0 items-center justify-between border-b border-border bg-card px-4 py-2">
<div className="flex items-center gap-3">
<button
onClick={() => navigate('/my-trees')}
onClick={() => navigate('/trees')}
className="rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-foreground"
>
<ArrowLeft className="h-5 w-5" />

View File

@@ -9,6 +9,7 @@ import { StepChecklist } from '@/components/procedural/StepChecklist'
import { StepDetail } from '@/components/procedural/StepDetail'
import { ProgressBar } from '@/components/procedural/ProgressBar'
import { CompletionSummary } from '@/components/procedural/CompletionSummary'
import { ConfirmDialog } from '@/components/common/ConfirmDialog'
import { cn } from '@/lib/utils'
import { toast } from '@/lib/toast'
import { StepFeedback } from '@/components/session/StepFeedback'
@@ -35,6 +36,7 @@ export function ProceduralNavigationPage() {
const [stepStates, setStepStates] = useState<Map<string, StepState>>(new Map())
const [isComplete, setIsComplete] = useState(false)
const [completedAt, setCompletedAt] = useState<string>('')
const [showExitConfirm, setShowExitConfirm] = useState(false)
const [sidebarOpen, setSidebarOpen] = useState(true)
const [paramsOpen, setParamsOpen] = useState(false)
const [showCsatModal, setShowCsatModal] = useState(false)
@@ -117,7 +119,7 @@ export function ProceduralNavigationPage() {
}
} catch {
toast.error('Failed to load flow')
navigate('/my-trees')
navigate('/trees')
} finally {
setIsLoading(false)
}
@@ -177,7 +179,7 @@ export function ProceduralNavigationPage() {
setCurrentStepIndex(firstIncomplete >= 0 ? firstIncomplete : pSteps.length - 1)
} catch {
toast.error('Failed to resume session')
navigate('/my-trees')
navigate('/trees')
}
}
@@ -301,7 +303,7 @@ export function ProceduralNavigationPage() {
fields={tree.intake_form || []}
treeName={tree.name}
onSubmit={handleIntakeSubmit}
onCancel={() => navigate('/my-trees')}
onCancel={() => navigate('/trees')}
/>
)
}
@@ -327,7 +329,7 @@ export function ProceduralNavigationPage() {
startedAt={session.started_at}
completedAt={completedAt}
onExport={() => navigate(`/sessions/${session.id}`)}
onClose={() => navigate('/my-trees')}
onClose={() => navigate('/trees')}
/>
</div>
)
@@ -354,6 +356,19 @@ export function ProceduralNavigationPage() {
<ListOrdered className="h-5 w-5 text-muted-foreground" />
<h1 className="text-sm font-semibold text-foreground sm:text-base">{tree.name}</h1>
</div>
<button
onClick={() => {
if (currentStepIndex > 0) {
setShowExitConfirm(true)
} else {
navigate('/trees')
}
}}
className="rounded-md border border-border px-3 py-1.5 text-xs font-medium text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
>
<X className="mr-1 inline h-3.5 w-3.5" />
Exit
</button>
</div>
<div className="mt-2">
<ProgressBar
@@ -433,6 +448,15 @@ export function ProceduralNavigationPage() {
/>
)}
<ConfirmDialog
isOpen={showExitConfirm}
onClose={() => setShowExitConfirm(false)}
onConfirm={() => navigate('/trees')}
title="Exit Session"
message="You have progress in this session. Are you sure you want to exit? Your progress will not be saved."
confirmLabel="Exit"
/>
{/* Parameters popover */}
{paramsOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center">

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { BarChart3, Loader2, Users, Target, Clock, TrendingUp, ShieldX } from 'lucide-react'
import { Link, Navigate } from 'react-router-dom'
import { BarChart3, Loader2, Users, Target, Clock, TrendingUp } from 'lucide-react'
import {
AreaChart,
Area,
@@ -44,25 +44,8 @@ export default function TeamAnalyticsPage() {
.finally(() => setLoading(false))
}, [period, isAccountOwner, isSuperAdmin])
// Permission guard
if (!isAccountOwner && !isSuperAdmin) {
return (
<div className="flex flex-col items-center justify-center min-h-[60vh] gap-4 p-6">
<ShieldX size={48} className="text-muted-foreground" />
<h2 className="text-xl font-semibold text-foreground">Access Denied</h2>
<p className="text-muted-foreground text-center max-w-md">
Team Analytics is only available to account owners and administrators.
You can view your personal stats instead.
</p>
<Link
to="/analytics/me"
className="mt-2 inline-flex items-center gap-2 rounded-lg bg-white text-black px-4 py-2 text-sm font-medium hover:bg-white/90 transition-colors"
>
<TrendingUp size={16} />
View My Stats
</Link>
</div>
)
return <Navigate to="/analytics/me" replace />
}
if (loading) {

View File

@@ -141,6 +141,7 @@ export function TreeEditorPage() {
// Permission guard: redirect viewers away from editor
useEffect(() => {
if (!canCreateTrees) {
toast.error("You don't have permission to edit flows")
navigate('/trees')
}
}, [canCreateTrees, navigate])
@@ -155,6 +156,7 @@ export function TreeEditorPage() {
try {
const tree = await treesApi.get(id)
if (!canEditTree({ author_id: tree.author_id, account_id: tree.account_id })) {
toast.error("You don't have permission to edit this flow")
navigate('/trees')
return
}
@@ -162,6 +164,7 @@ export function TreeEditorPage() {
setTreeStatus(tree.status) // Load status from existing tree
} catch (err) {
console.error('Failed to load tree:', err)
toast.error('Failed to load flow')
navigate('/trees')
}
} else {

View File

@@ -7,4 +7,3 @@ export { default as TreeEditorPage } from './TreeEditorPage'
export { default as SessionHistoryPage } from './SessionHistoryPage'
export { default as SessionDetailPage } from './SessionDetailPage'
export { default as AccountSettingsPage } from './AccountSettingsPage'
export { default as AdminCategoriesPage } from './AdminCategoriesPage'