fix: wire up maintenance flow creation, editing, and navigation in frontend

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-17 18:47:26 -05:00
parent dbe0e9e5b3
commit c8c5fc84db
4 changed files with 61 additions and 21 deletions

View File

@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useNavigate, Link } from 'react-router-dom' import { useNavigate, Link } from 'react-router-dom'
import { Play, Pencil, Share2, Trash2, GitBranch, Clock, TrendingUp, FolderTree, Plus, ListOrdered, ChevronDown } from 'lucide-react' import { Play, Pencil, Share2, Trash2, GitBranch, Clock, TrendingUp, FolderTree, Plus, ListOrdered, ChevronDown, Wrench } from 'lucide-react'
import { treesApi } from '@/api/trees' import { treesApi } from '@/api/trees'
import { sessionsApi } from '@/api/sessions' import { sessionsApi } from '@/api/sessions'
import type { TreeListItem } from '@/types' import type { TreeListItem } from '@/types'
@@ -71,7 +71,9 @@ export function MyTreesPage() {
} }
const handleStartSession = (tree: TreeWithStats) => { const handleStartSession = (tree: TreeWithStats) => {
if (tree.tree_type === 'procedural') { if (tree.tree_type === 'maintenance') {
navigate(`/flows/${tree.id}/maintenance`)
} else if (tree.tree_type === 'procedural') {
navigate(`/flows/${tree.id}/navigate`) navigate(`/flows/${tree.id}/navigate`)
} else { } else {
navigate(`/trees/${tree.id}/navigate`) navigate(`/trees/${tree.id}/navigate`)
@@ -79,7 +81,8 @@ export function MyTreesPage() {
} }
const getEditPath = (tree: TreeWithStats) => { const getEditPath = (tree: TreeWithStats) => {
return tree.tree_type === 'procedural' ? `/flows/${tree.id}/edit` : `/trees/${tree.id}/edit` return tree.tree_type === 'procedural' || tree.tree_type === 'maintenance'
? `/flows/${tree.id}/edit` : `/trees/${tree.id}/edit`
} }
const handleDeleteTree = async () => { const handleDeleteTree = async () => {
@@ -153,6 +156,17 @@ export function MyTreesPage() {
<div className="text-xs text-muted-foreground">Step-by-step procedure</div> <div className="text-xs text-muted-foreground">Step-by-step procedure</div>
</div> </div>
</Link> </Link>
<Link
to="/flows/new?type=maintenance"
onClick={() => setShowCreateMenu(false)}
className="flex items-center gap-3 rounded-md px-3 py-2.5 text-sm text-foreground hover:bg-accent"
>
<Wrench className="h-4 w-4 text-amber-400" />
<div>
<div className="font-medium">Maintenance Flow</div>
<div className="text-xs text-muted-foreground">Scheduled multi-target tasks</div>
</div>
</Link>
</div> </div>
</> </>
)} )}
@@ -209,6 +223,9 @@ export function MyTreesPage() {
{tree.tree_type === 'procedural' && ( {tree.tree_type === 'procedural' && (
<ListOrdered className="h-4 w-4 shrink-0 text-muted-foreground" /> <ListOrdered className="h-4 w-4 shrink-0 text-muted-foreground" />
)} )}
{tree.tree_type === 'maintenance' && (
<Wrench className="h-4 w-4 shrink-0 text-amber-400" />
)}
<h3 className="font-semibold text-foreground">{tree.name}</h3> <h3 className="font-semibold text-foreground">{tree.name}</h3>
</div> </div>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
@@ -217,6 +234,11 @@ export function MyTreesPage() {
Procedure Procedure
</span> </span>
)} )}
{tree.tree_type === 'maintenance' && (
<span className="rounded-full bg-amber-400/10 px-2 py-0.5 text-[10px] font-medium text-amber-400">
Maintenance
</span>
)}
{tree.category_info && ( {tree.category_info && (
<span className="rounded-full bg-accent px-2 py-0.5 text-xs text-muted-foreground"> <span className="rounded-full bg-accent px-2 py-0.5 text-xs text-muted-foreground">
{tree.category_info.name} {tree.category_info.name}

View File

@@ -1,20 +1,23 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import { useParams, useNavigate } from 'react-router-dom' import { useParams, useNavigate, useSearchParams } from 'react-router-dom'
import { Save, ArrowLeft, ListOrdered } from 'lucide-react' import { Save, ArrowLeft, ListOrdered, Wrench } from 'lucide-react'
import { treesApi } from '@/api/trees' import { treesApi } from '@/api/trees'
import { useProceduralEditorStore } from '@/store/proceduralEditorStore' import { useProceduralEditorStore } from '@/store/proceduralEditorStore'
import { IntakeFormBuilder } from '@/components/procedural-editor/IntakeFormBuilder' import { IntakeFormBuilder } from '@/components/procedural-editor/IntakeFormBuilder'
import { StepList } from '@/components/procedural-editor/StepList' import { StepList } from '@/components/procedural-editor/StepList'
import { TagInput } from '@/components/common/TagInput' import { TagInput } from '@/components/common/TagInput'
import { toast } from '@/lib/toast' import { toast } from '@/lib/toast'
import type { TreeType } from '@/types'
export function ProceduralEditorPage() { export function ProceduralEditorPage() {
const { id } = useParams<{ id: string }>() const { id } = useParams<{ id: string }>()
const [searchParams] = useSearchParams()
const navigate = useNavigate() const navigate = useNavigate()
const isEditMode = !!id const isEditMode = !!id
const { const {
treeId, treeId,
treeType,
name, name,
description, description,
tags, tags,
@@ -34,12 +37,16 @@ export function ProceduralEditorPage() {
getTreeForSave, getTreeForSave,
} = useProceduralEditorStore() } = useProceduralEditorStore()
const isMaintenance = treeType === 'maintenance'
const flowLabel = isMaintenance ? 'Maintenance Flow' : 'Procedure'
// Load tree or init new // Load tree or init new
useEffect(() => { useEffect(() => {
if (isEditMode && id) { if (isEditMode && id) {
loadExistingTree(id) loadExistingTree(id)
} else { } else {
initNew() const urlType = searchParams.get('type')
initNew((urlType === 'maintenance' ? 'maintenance' : 'procedural') as TreeType)
} }
return () => { reset() } return () => { reset() }
@@ -48,21 +55,21 @@ export function ProceduralEditorPage() {
const loadExistingTree = async (treeId: string) => { const loadExistingTree = async (treeId: string) => {
try { try {
const tree = await treesApi.get(treeId) const tree = await treesApi.get(treeId)
if (tree.tree_type !== 'procedural') { if (tree.tree_type !== 'procedural' && tree.tree_type !== 'maintenance') {
toast.error('This tree is not a procedural flow') toast.error('This flow is not a procedural or maintenance flow')
navigate('/my-trees') navigate('/my-trees')
return return
} }
loadTree(tree) loadTree(tree)
} catch { } catch {
toast.error('Failed to load procedure') toast.error('Failed to load flow')
navigate('/my-trees') navigate('/my-trees')
} }
} }
const handleSave = async (saveStatus?: 'draft' | 'published') => { const handleSave = async (saveStatus?: 'draft' | 'published') => {
if (!name.trim()) { if (!name.trim()) {
toast.error('Please enter a name for the procedure') toast.error(`Please enter a name for the ${flowLabel.toLowerCase()}`)
return return
} }
@@ -76,18 +83,18 @@ export function ProceduralEditorPage() {
if (isEditMode && treeId) { if (isEditMode && treeId) {
await treesApi.update(treeId, payload) await treesApi.update(treeId, payload)
markSaved() markSaved()
toast.success('Procedure saved') toast.success(`${flowLabel} saved`)
} else { } else {
const created = await treesApi.create(payload) const created = await treesApi.create(payload)
markSaved() markSaved()
toast.success('Procedure created') toast.success(`${flowLabel} created`)
navigate(`/flows/${created.id}/edit`, { replace: true }) navigate(`/flows/${created.id}/edit`, { replace: true })
} }
} catch (err: unknown) { } catch (err: unknown) {
const message = err && typeof err === 'object' && 'response' in err const message = err && typeof err === 'object' && 'response' in err
? (err as { response?: { data?: { detail?: string | { message?: string } } } }).response?.data?.detail ? (err as { response?: { data?: { detail?: string | { message?: string } } } }).response?.data?.detail
: null : null
const errorText = typeof message === 'string' ? message : typeof message === 'object' && message?.message ? message.message : 'Failed to save procedure' const errorText = typeof message === 'string' ? message : typeof message === 'object' && message?.message ? message.message : `Failed to save ${flowLabel.toLowerCase()}`
toast.error(errorText) toast.error(errorText)
} finally { } finally {
setIsSaving(false) setIsSaving(false)
@@ -114,9 +121,11 @@ export function ProceduralEditorPage() {
<ArrowLeft className="h-5 w-5" /> <ArrowLeft className="h-5 w-5" />
</button> </button>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ListOrdered className="h-5 w-5 text-muted-foreground" /> {isMaintenance
? <Wrench className="h-5 w-5 text-amber-400" />
: <ListOrdered className="h-5 w-5 text-muted-foreground" />}
<h1 className="text-xl font-bold text-foreground sm:text-2xl"> <h1 className="text-xl font-bold text-foreground sm:text-2xl">
{isEditMode ? 'Edit Procedure' : 'New Procedure'} {isEditMode ? `Edit ${flowLabel}` : `New ${flowLabel}`}
</h1> </h1>
</div> </div>
</div> </div>

View File

@@ -196,7 +196,9 @@ export function TreeLibraryPage() {
} }
const handleStartSession = (treeId: string, treeType?: string) => { const handleStartSession = (treeId: string, treeType?: string) => {
if (treeType === 'procedural') { if (treeType === 'maintenance') {
navigate(`/flows/${treeId}/maintenance`)
} else if (treeType === 'procedural') {
navigate(`/flows/${treeId}/navigate`) navigate(`/flows/${treeId}/navigate`)
} else { } else {
navigate(`/trees/${treeId}/navigate`) navigate(`/trees/${treeId}/navigate`)
@@ -267,14 +269,14 @@ export function TreeLibraryPage() {
</div> </div>
{canCreateTrees && ( {canCreateTrees && (
<Link <Link
to={typeFilter === 'procedural' ? '/flows/new' : '/trees/new'} to={typeFilter === 'procedural' ? '/flows/new' : typeFilter === 'maintenance' ? '/flows/new?type=maintenance' : '/trees/new'}
className={cn( className={cn(
'flex items-center gap-2 rounded-md bg-gradient-brand px-4 py-2 text-sm font-medium text-white shadow-lg shadow-primary/20', 'flex items-center gap-2 rounded-md bg-gradient-brand px-4 py-2 text-sm font-medium text-white shadow-lg shadow-primary/20',
'hover:opacity-90' 'hover:opacity-90'
)} )}
> >
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
{typeFilter === 'procedural' ? 'New Project' : 'Create Flow'} {typeFilter === 'procedural' ? 'New Project' : typeFilter === 'maintenance' ? 'New Maintenance Flow' : 'Create Flow'}
</Link> </Link>
)} )}
</div> </div>

View File

@@ -64,6 +64,7 @@ function createDefaultField(index: number, existingFields: IntakeFormField[]): I
interface ProceduralEditorState { interface ProceduralEditorState {
// Tree metadata // Tree metadata
treeId: string | null treeId: string | null
treeType: TreeType
name: string name: string
description: string description: string
categoryId: string | null categoryId: string | null
@@ -83,11 +84,12 @@ interface ProceduralEditorState {
isSaving: boolean isSaving: boolean
// Actions - Init // Actions - Init
initNew: () => void initNew: (type?: TreeType) => void
loadTree: (tree: Tree) => void loadTree: (tree: Tree) => void
reset: () => void reset: () => void
// Actions - Metadata // Actions - Metadata
setTreeType: (treeType: TreeType) => void
setName: (name: string) => void setName: (name: string) => void
setDescription: (description: string) => void setDescription: (description: string) => void
setCategoryId: (categoryId: string | null) => void setCategoryId: (categoryId: string | null) => void
@@ -131,6 +133,7 @@ export const useProceduralEditorStore = create<ProceduralEditorState>()(
immer((set, get) => ({ immer((set, get) => ({
// Initial state // Initial state
treeId: null, treeId: null,
treeType: 'procedural' as TreeType,
name: '', name: '',
description: '', description: '',
categoryId: null, categoryId: null,
@@ -146,9 +149,10 @@ export const useProceduralEditorStore = create<ProceduralEditorState>()(
isSaving: false, isSaving: false,
// Init // Init
initNew: () => { initNew: (type?: TreeType) => {
set((state) => { set((state) => {
state.treeId = null state.treeId = null
state.treeType = type || 'procedural'
state.name = '' state.name = ''
state.description = '' state.description = ''
state.categoryId = null state.categoryId = null
@@ -169,6 +173,7 @@ export const useProceduralEditorStore = create<ProceduralEditorState>()(
const structure = tree.tree_structure as unknown as ProceduralTreeStructure const structure = tree.tree_structure as unknown as ProceduralTreeStructure
set((state) => { set((state) => {
state.treeId = tree.id state.treeId = tree.id
state.treeType = tree.tree_type
state.name = tree.name state.name = tree.name
state.description = tree.description || '' state.description = tree.description || ''
state.categoryId = tree.category_id state.categoryId = tree.category_id
@@ -188,6 +193,7 @@ export const useProceduralEditorStore = create<ProceduralEditorState>()(
reset: () => { reset: () => {
set((state) => { set((state) => {
state.treeId = null state.treeId = null
state.treeType = 'procedural'
state.name = '' state.name = ''
state.description = '' state.description = ''
state.categoryId = null state.categoryId = null
@@ -205,6 +211,7 @@ export const useProceduralEditorStore = create<ProceduralEditorState>()(
}, },
// Metadata // Metadata
setTreeType: (treeType) => set((state) => { state.treeType = treeType }),
setName: (name) => set((state) => { state.name = name; state.isDirty = true }), setName: (name) => set((state) => { state.name = name; state.isDirty = true }),
setDescription: (description) => set((state) => { state.description = description; state.isDirty = true }), setDescription: (description) => set((state) => { state.description = description; state.isDirty = true }),
setCategoryId: (categoryId) => set((state) => { state.categoryId = categoryId; state.isDirty = true }), setCategoryId: (categoryId) => set((state) => { state.categoryId = categoryId; state.isDirty = true }),
@@ -343,7 +350,7 @@ export const useProceduralEditorStore = create<ProceduralEditorState>()(
return { return {
name: state.name, name: state.name,
description: state.description, description: state.description,
tree_type: 'procedural' as TreeType, tree_type: state.treeType,
tree_structure: { steps: state.steps }, tree_structure: { steps: state.steps },
intake_form: state.intakeForm.length > 0 ? state.intakeForm : undefined, intake_form: state.intakeForm.length > 0 ? state.intakeForm : undefined,
category_id: state.categoryId, category_id: state.categoryId,