feat: flexible intake — deferred variables + prepared sessions (#103)

* feat: flexible intake — deferred variables + prepared sessions

Remove blocking intake form modal. Variables are now filled inline during
flow execution or pre-filled via prepared sessions. Adds PATCH /sessions/{id}/variables
endpoint, POST /sessions/prepare for session pre-staging, inline variable prompts
in StepDetail, editable Session Variables panel, and "Prepared for You" dashboard section.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: pass treeData directly to startSession to avoid stale state

setTree(treeData) hasn't committed when startSession runs immediately
after, so tree is still null and getStepsFromTree returns []. This
caused the step detail area to render empty on new session start.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: wire PrepareSessionModal entry point in Flow Library

Add "Prepare session" button (clipboard icon) to grid, list, and table
views for procedural/maintenance flows. Clicking fetches tree intake
fields and account members, then opens PrepareSessionModal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit was merged in pull request #103.
This commit is contained in:
chihlasm
2026-03-10 09:49:51 -04:00
committed by GitHub
parent 4727106141
commit ccd06c9ed4
25 changed files with 1214 additions and 102 deletions

View File

@@ -56,7 +56,7 @@ export function MyTreesPage() {
const lastUsedMap = new Map<string, string>()
for (const session of recentSessions) {
const existing = lastUsedMap.get(session.tree_id)
if (!existing || new Date(session.started_at) > new Date(existing)) {
if (session.started_at && (!existing || new Date(session.started_at) > new Date(existing))) {
lastUsedMap.set(session.tree_id, session.started_at)
}
}

View File

@@ -1,13 +1,12 @@
import { useEffect, useState, useRef } from 'react'
import { useParams, useNavigate, useLocation } from 'react-router-dom'
import { ChevronLeft, ChevronRight, ListOrdered, Settings2, X, Plus } from 'lucide-react'
import { ChevronLeft, ChevronRight, ListOrdered, Settings2, X, Plus, Check, AlertCircle } from 'lucide-react'
import { treesApi } from '@/api/trees'
import { sessionsApi } from '@/api/sessions'
import { stepsApi } from '@/api/steps'
import type { Tree, Session, ProceduralStep, DecisionRecord, RuntimeStep, CustomProceduralStep } from '@/types'
import type { Tree, Session, ProceduralStep, DecisionRecord, RuntimeStep, CustomProceduralStep, IntakeFormField } from '@/types'
import type { CustomStep } from '@/types/session'
import type { Step } from '@/types/step'
import { IntakeFormModal } from '@/components/procedural/IntakeFormModal'
import { StepChecklist } from '@/components/procedural/StepChecklist'
import { StepDetail } from '@/components/procedural/StepDetail'
import { ProgressBar } from '@/components/procedural/ProgressBar'
@@ -64,7 +63,6 @@ export function ProceduralNavigationPage() {
const [tree, setTree] = useState<Tree | null>(null)
const [session, setSession] = useState<Session | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [showIntakeForm, setShowIntakeForm] = useState(false)
const [sessionVariables, setSessionVariables] = useState<Record<string, string>>({})
const [currentStepIndex, setCurrentStepIndex] = useState(0)
const [stepStates, setStepStates] = useState<Map<string, StepState>>(new Map())
@@ -88,6 +86,22 @@ export function ProceduralNavigationPage() {
const [isSavingStep, setIsSavingStep] = useState(false)
const [copilotOpen, setCopilotOpen] = useState(false)
// Editable variables panel state
const [editingVarName, setEditingVarName] = useState<string | null>(null)
const [editingVarValue, setEditingVarValue] = useState('')
const [showUnfilledWarning, setShowUnfilledWarning] = useState(false)
// Get intake form fields from tree snapshot or tree
const intakeFields: IntakeFormField[] = (() => {
if (tree?.intake_form && tree.intake_form.length > 0) return tree.intake_form
// Fallback: check tree snapshot on session
const snapshot = session?.tree_snapshot as Record<string, unknown> | undefined
if (snapshot?.intake_form && Array.isArray(snapshot.intake_form)) {
return snapshot.intake_form as IntakeFormField[]
}
return []
})()
// Get procedural steps from tree
const getSteps = (): ProceduralStep[] => {
if (!tree) return []
@@ -127,9 +141,9 @@ export function ProceduralNavigationPage() {
// Elapsed time timer
useEffect(() => {
if (session && !isComplete) {
if (session && session.started_at && !isComplete) {
const calcElapsed = () => {
const start = parseTimestamp(session.started_at).getTime()
const start = parseTimestamp(session.started_at!).getTime()
setElapsedMinutes(Math.max(0, Math.floor((Date.now() - start) / 60000)))
}
calcElapsed()
@@ -169,12 +183,9 @@ export function ProceduralNavigationPage() {
return
}
// Check if intake form exists
if (treeData.intake_form && treeData.intake_form.length > 0) {
setShowIntakeForm(true)
} else {
await startSession(id, {})
}
// Start session immediately — no intake form modal
// Variables will be filled inline during execution
await startSession(id, {}, treeData)
} catch {
toast.error('Failed to load flow')
navigate('/trees')
@@ -183,7 +194,7 @@ export function ProceduralNavigationPage() {
}
}
const startSession = async (id: string, variables: Record<string, string>) => {
const startSession = async (id: string, variables: Record<string, string>, treeData?: Tree) => {
try {
const newSession = await sessionsApi.create({
tree_id: id,
@@ -191,11 +202,10 @@ export function ProceduralNavigationPage() {
})
setSession(newSession)
setSessionVariables(variables)
setShowIntakeForm(false)
// Initialize step states
// Initialize step states — use passed treeData since `tree` state may not have committed yet
const initialStates = new Map<string, StepState>()
const allSteps = getStepsFromTree(tree!)
const allSteps = getStepsFromTree(treeData || tree!)
for (const step of allSteps) {
initialStates.set(step.id, { notes: '', verificationValue: '', completedAt: null })
}
@@ -212,7 +222,6 @@ export function ProceduralNavigationPage() {
const sessionData = await sessionsApi.get(sessionId)
setSession(sessionData)
setSessionVariables(sessionData.session_variables || {})
setShowIntakeForm(false)
// Initialize step states from session decisions
const allSteps = getStepsFromTree(treeData)
@@ -257,9 +266,21 @@ export function ProceduralNavigationPage() {
return structure.steps || []
}
const handleIntakeSubmit = async (variables: Record<string, string>) => {
if (!treeId) return
await startSession(treeId, variables)
// Handle inline variable submission (from StepDetail or variables panel)
const handleVariableSubmit = async (variableName: string, value: string) => {
if (!session) return
// Optimistic update
const newVars = { ...sessionVariables, [variableName]: value }
setSessionVariables(newVars)
try {
await sessionsApi.updateVariables(session.id, { [variableName]: value })
} catch {
// Revert on failure
setSessionVariables(sessionVariables)
toast.error('Failed to save variable')
}
}
const handleMarkComplete = async () => {
@@ -310,7 +331,18 @@ export function ProceduralNavigationPage() {
// Move to next step or complete
if (currentStepIndex >= procedureSteps.length - 1) {
// Last step — complete the procedure
// Last step — check for unfilled required variables
const unfilledReqVars = intakeFields.filter(
f => f.required && !sessionVariables[f.variable_name]?.trim()
)
if (unfilledReqVars.length > 0 && !showUnfilledWarning) {
// Show warning but don't block
setShowUnfilledWarning(true)
return
}
setShowUnfilledWarning(false)
// Complete the procedure
const completedTime = new Date().toISOString()
await sessionsApi.complete(session.id, {
outcome: 'resolved',
@@ -461,6 +493,21 @@ export function ProceduralNavigationPage() {
setPendingCustomStep(null)
}
// Variables panel: start editing
const startEditingVar = (varName: string) => {
setEditingVarName(varName)
setEditingVarValue(sessionVariables[varName] || '')
}
// Variables panel: save edit
const saveEditingVar = () => {
if (editingVarName && editingVarValue.trim()) {
handleVariableSubmit(editingVarName, editingVarValue.trim())
}
setEditingVarName(null)
setEditingVarValue('')
}
// Loading state
if (isLoading) {
return (
@@ -470,19 +517,6 @@ export function ProceduralNavigationPage() {
)
}
// Intake form modal
if (showIntakeForm && tree) {
return (
<IntakeFormModal
isOpen={true}
fields={tree.intake_form || []}
treeName={tree.name}
onSubmit={handleIntakeSubmit}
onCancel={() => navigate('/trees')}
/>
)
}
// Completion summary
if (isComplete && tree && session) {
return (
@@ -501,7 +535,7 @@ export function ProceduralNavigationPage() {
}])
)}
variables={sessionVariables}
startedAt={session.started_at}
startedAt={session.started_at || ''}
completedAt={completedAt}
onExport={() => navigate(`/sessions/${session.id}`)}
onClose={() => navigate('/trees')}
@@ -516,6 +550,11 @@ export function ProceduralNavigationPage() {
const currentStep = procedureSteps[currentStepIndex]
const currentStepState = currentStep ? stepStates.get(currentStep.id) : undefined
// Count unfilled required variables
const unfilledRequired = intakeFields.filter(
f => f.required && !sessionVariables[f.variable_name]?.trim()
).length
return (
<div className="flex h-full min-h-0 flex-col overflow-hidden">
{/* Top bar */}
@@ -583,15 +622,20 @@ export function ProceduralNavigationPage() {
onStepClick={setCurrentStepIndex}
/>
{/* View Parameters button */}
{Object.keys(sessionVariables).length > 0 && (
{/* Session Variables button */}
{intakeFields.length > 0 && (
<div className="mt-3 border-t border-border pt-3">
<button
onClick={() => setParamsOpen(true)}
className="flex w-full items-center gap-2 rounded-lg border border-border px-3 py-2 text-xs text-muted-foreground hover:bg-accent hover:text-muted-foreground"
>
<Settings2 className="h-3.5 w-3.5" />
View Parameters ({Object.keys(sessionVariables).length})
Session Variables
{unfilledRequired > 0 && (
<span className="ml-auto flex h-4 w-4 items-center justify-center rounded-full bg-amber-400/20 text-[0.6rem] font-bold text-amber-400">
{unfilledRequired}
</span>
)}
</button>
</div>
)}
@@ -614,6 +658,8 @@ export function ProceduralNavigationPage() {
isCompleted={completedStepIds.has(currentStep.id)}
onMarkComplete={handleMarkComplete}
isLast={currentStepIndex === procedureSteps.length - 1}
intakeFields={intakeFields}
onVariableSubmit={handleVariableSubmit}
/>
)}
@@ -667,7 +713,16 @@ export function ProceduralNavigationPage() {
confirmLabel="Exit"
/>
{/* Parameters popover */}
<ConfirmDialog
isOpen={showUnfilledWarning}
onClose={() => setShowUnfilledWarning(false)}
onConfirm={handleMarkComplete}
title="Unfilled Variables"
message={`${intakeFields.filter(f => f.required && !sessionVariables[f.variable_name]?.trim()).length} required variable(s) are still empty. You can fill them now via the Session Variables panel, or complete anyway. Unfilled variables will appear as blank in exports.`}
confirmLabel="Complete Anyway"
/>
{/* Session Variables Panel (editable) */}
{paramsOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div
@@ -676,7 +731,7 @@ export function ProceduralNavigationPage() {
/>
<div className="relative w-full max-w-md rounded-2xl border border-border bg-card shadow-2xl backdrop-blur-xs">
<div className="flex items-center justify-between border-b border-border px-5 py-4">
<h3 className="text-sm font-semibold text-foreground">Project Parameters</h3>
<h3 className="text-sm font-semibold text-foreground">Session Variables</h3>
<button
onClick={() => setParamsOpen(false)}
className="rounded-lg p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
@@ -686,12 +741,111 @@ export function ProceduralNavigationPage() {
</div>
<div className="max-h-[60vh] overflow-y-auto p-5">
<div className="space-y-2">
{Object.entries(sessionVariables).map(([key, value]) => (
<div key={key} className="flex items-baseline justify-between gap-4 rounded-lg bg-accent px-3 py-2">
<span className="text-xs font-medium text-muted-foreground">{key.replace(/_/g, ' ')}</span>
<span className="text-right text-sm text-muted-foreground">{value || 'N/A'}</span>
</div>
))}
{intakeFields
.sort((a, b) => a.display_order - b.display_order)
.map((field) => {
const value = sessionVariables[field.variable_name] || ''
const isFilled = !!value.trim()
const isEditing = editingVarName === field.variable_name
return (
<div
key={field.variable_name}
className={cn(
'rounded-lg border px-3 py-2.5',
isFilled ? 'border-border bg-accent' : 'border-cyan-500/20 bg-cyan-500/5'
)}
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 min-w-0">
{isFilled ? (
<Check className="h-3.5 w-3.5 shrink-0 text-emerald-400" />
) : (
<AlertCircle className="h-3.5 w-3.5 shrink-0 text-cyan-400" />
)}
<span className="text-xs font-medium text-muted-foreground truncate">
{field.label}
{field.required && <span className="ml-1 text-amber-400">*</span>}
</span>
</div>
{!isEditing && (
<button
onClick={() => startEditingVar(field.variable_name)}
className="shrink-0 rounded px-2 py-0.5 text-xs text-muted-foreground hover:bg-card hover:text-foreground"
>
{isFilled ? 'Edit' : 'Fill'}
</button>
)}
</div>
{isEditing ? (
<div className="mt-2">
{field.field_type === 'select' && field.options?.length ? (
<select
value={editingVarValue}
onChange={(e) => setEditingVarValue(e.target.value)}
autoFocus
className="w-full rounded-md border border-cyan-500/30 bg-card px-2.5 py-1.5 text-sm text-foreground focus:border-cyan-400 focus:outline-hidden focus:ring-1 focus:ring-cyan-400/30"
>
<option value="">{field.placeholder || 'Select...'}</option>
{field.options.map((opt) => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
) : field.field_type === 'textarea' ? (
<textarea
value={editingVarValue}
onChange={(e) => setEditingVarValue(e.target.value)}
autoFocus
rows={3}
placeholder={field.placeholder}
className="w-full rounded-md border border-cyan-500/30 bg-card px-2.5 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-cyan-400 focus:outline-hidden focus:ring-1 focus:ring-cyan-400/30"
/>
) : (
<input
type={field.field_type === 'number' ? 'number' : field.field_type === 'email' ? 'email' : 'text'}
value={editingVarValue}
onChange={(e) => setEditingVarValue(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') saveEditingVar() }}
autoFocus
placeholder={field.placeholder}
className="w-full rounded-md border border-cyan-500/30 bg-card px-2.5 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-cyan-400 focus:outline-hidden focus:ring-1 focus:ring-cyan-400/30"
/>
)}
{field.help_text && (
<p className="mt-1 text-[0.625rem] text-muted-foreground">{field.help_text}</p>
)}
<div className="mt-2 flex justify-end gap-2">
<button
onClick={() => { setEditingVarName(null); setEditingVarValue('') }}
className="rounded px-2.5 py-1 text-xs text-muted-foreground hover:text-foreground"
>
Cancel
</button>
<button
onClick={saveEditingVar}
disabled={!editingVarValue.trim()}
className="rounded bg-gradient-brand px-2.5 py-1 text-xs font-medium text-[#101114] disabled:opacity-40"
>
Save
</button>
</div>
</div>
) : isFilled ? (
<p className="mt-1 text-sm text-foreground truncate">{value}</p>
) : null}
</div>
)
})}
{/* Show any extra variables not in intake form */}
{Object.entries(sessionVariables)
.filter(([key]) => !intakeFields.some(f => f.variable_name === key))
.map(([key, value]) => (
<div key={key} className="flex items-baseline justify-between gap-4 rounded-lg bg-accent px-3 py-2">
<span className="text-xs font-medium text-muted-foreground">{key.replace(/_/g, ' ')}</span>
<span className="text-right text-sm text-muted-foreground">{value || 'N/A'}</span>
</div>
))}
</div>
</div>
</div>

View File

@@ -26,6 +26,7 @@ import { WeeklyCalendar } from '@/components/dashboard/WeeklyCalendar'
import { QuickActions } from '@/components/dashboard/QuickActions'
import { OpenSessions } from '@/components/dashboard/OpenSessions'
import { RecentActivity } from '@/components/dashboard/RecentActivity'
import { PreparedSessions } from '@/components/dashboard/PreparedSessions'
function timeAgo(dateStr: string): string {
const now = Date.now()
@@ -218,6 +219,7 @@ export function QuickStartPage() {
// Stats
const openSessions = activeSessions.length
const todaySessions = allSessions.filter(s => {
if (!s.started_at) return false
const d = new Date(s.started_at)
const now = new Date()
return d.toDateString() === now.toDateString()
@@ -226,14 +228,15 @@ export function QuickStartPage() {
// Open sessions for the new panel (3 oldest)
const openSessionItems = activeSessions
.sort((a, b) => new Date(a.started_at).getTime() - new Date(b.started_at).getTime())
.filter(s => s.started_at) // Exclude prepared sessions (started_at is null)
.sort((a, b) => new Date(a.started_at!).getTime() - new Date(b.started_at!).getTime())
.slice(0, 3)
.map(s => ({
id: s.id,
treeName: s.tree_snapshot?.name || 'Unknown',
treeId: s.tree_id,
treeType: (s.tree_snapshot as unknown as Record<string, unknown>)?.tree_type as string | undefined,
timeAgo: timeAgo(s.started_at),
timeAgo: timeAgo(s.started_at!),
}))
// recentSessionItems removed — replaced by RecentActivity component
@@ -335,7 +338,10 @@ export function QuickStartPage() {
</div>
</div>
{/* Row 3: Recent Activity */}
{/* Row 3: Prepared Sessions (only visible when sessions exist) */}
<PreparedSessions />
{/* Row 4: Recent Activity */}
<RecentActivity />
{/* ── Existing content below ── */}

View File

@@ -293,7 +293,7 @@ export function SessionDetailPage() {
const getTotalDuration = () => {
if (!session?.completed_at) return 'In progress'
const startedAtMs = new Date(session.started_at).getTime()
const startedAtMs = new Date(session.started_at || Date.now()).getTime()
const completedAtMs = new Date(session.completed_at).getTime()
if (Number.isNaN(startedAtMs) || Number.isNaN(completedAtMs)) return 'Unknown'
const seconds = Math.max(0, Math.floor((completedAtMs - startedAtMs) / 1000))
@@ -358,7 +358,7 @@ export function SessionDetailPage() {
<p className="mt-1 text-sm text-muted-foreground">
{session.tree_snapshot?.name}
{session.client_name && <> · Client: {session.client_name}</>}
{' · '}{new Date(session.started_at).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}
{session.started_at && <>{' · '}{new Date(session.started_at).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}</>}
</p>
</div>
<ActionMenu
@@ -482,7 +482,7 @@ export function SessionDetailPage() {
<SessionTimeline
decisions={session.decisions}
treeType={(session.tree_snapshot as unknown as Record<string, unknown>).tree_type as string}
startedAt={session.started_at}
startedAt={session.started_at || ''}
completedAt={session.completed_at}
/>

View File

@@ -22,7 +22,7 @@ export function SessionHistoryPage() {
const [hasMore, setHasMore] = useState(false)
const [trees, setTrees] = useState<TreeListItem[]>([])
const [isLoading, setIsLoading] = useState(true)
const [filter, setFilter] = useState<'all' | 'completed' | 'active'>('all')
const [filter, setFilter] = useState<'all' | 'completed' | 'active' | 'prepared'>('all')
// Initialize filters from URL params
const [filters, setFilters] = useState<SessionFilterState>(() => {
@@ -67,8 +67,10 @@ export function SessionHistoryPage() {
try {
const params: Record<string, string | boolean> = {}
// Tab filter (all/active/completed)
if (filter !== 'all') {
// Tab filter (all/active/completed/prepared)
if (filter === 'prepared') {
params.status = 'prepared'
} else if (filter !== 'all') {
params.completed = filter === 'completed'
}
@@ -173,7 +175,7 @@ export function SessionHistoryPage() {
{/* Filter Tabs */}
<div className="mb-6 flex gap-2 border-b border-border">
{(['all', 'active', 'completed'] as const).map((tab) => (
{(['all', 'active', 'completed', 'prepared'] as const).map((tab) => (
<button
key={tab}
onClick={() => setFilter(tab)}
@@ -267,7 +269,7 @@ export function SessionHistoryPage() {
{/* Timestamps */}
<p className="mt-1 text-sm text-muted-foreground">
Started: {formatDate(session.started_at)}
Started: {session.started_at ? formatDate(session.started_at) : 'Not started'}
{session.completed_at && (
<> · Completed: {formatDate(session.completed_at)}</>
)}

View File

@@ -7,11 +7,13 @@ import { treesApi } from '@/api/trees'
import { categoriesApi } from '@/api/categories'
import { foldersApi } from '@/api/folders'
import { sessionsApi } from '@/api/sessions'
import type { TreeListItem, CategoryListItem, FolderListItem, Session } from '@/types'
import type { TreeListItem, CategoryListItem, FolderListItem, Session, IntakeFormField } from '@/types'
import { FolderEditModal } from '@/components/library/FolderEditModal'
import { ForkModal } from '@/components/library/ForkModal'
import { ExportFlowModal } from '@/components/library/ExportFlowModal'
import { ImportFlowModal } from '@/components/library/ImportFlowModal'
import { PrepareSessionModal } from '@/components/procedural/PrepareSessionModal'
import { accountsApi } from '@/api/accounts'
import { ConfirmDialog } from '@/components/common/ConfirmDialog'
import { TreeGridView } from '@/components/library/TreeGridView'
import { TreeListView } from '@/components/library/TreeListView'
@@ -83,6 +85,13 @@ export function TreeLibraryPage() {
const [showImportModal, setShowImportModal] = useState(false)
const [exportTarget, setExportTarget] = useState<TreeListItem | null>(null)
// Prepare session modal state
const [prepareTarget, setPrepareTarget] = useState<{
tree: TreeListItem
intakeFields: IntakeFormField[]
teamMembers: { id: string; name: string; email: string }[]
} | null>(null)
// AI builder state
const { aiEnabled } = useCachedQuota()
@@ -268,6 +277,24 @@ export function TreeLibraryPage() {
if (tree) setExportTarget(tree)
}
const handlePrepareSession = async (tree: TreeListItem) => {
try {
const [treeDetail, members] = await Promise.all([
treesApi.get(tree.id),
accountsApi.getMembers().catch(() => []),
])
setPrepareTarget({
tree,
intakeFields: treeDetail.intake_form || [],
teamMembers: members
.filter(m => m.is_active)
.map(m => ({ id: m.id, name: m.name, email: m.email })),
})
} catch {
toast.error('Failed to load flow details')
}
}
const hasActiveFilters =
selectedCategoryId || selectedTags.length > 0 || searchQuery || selectedFolderId
@@ -436,7 +463,7 @@ export function TreeLibraryPage() {
</p>
<p className="text-sm text-muted-foreground">
{s.client_name && `${s.client_name} · `}
Started {formatTimeAgo(s.started_at)}
{s.started_at ? `Started ${formatTimeAgo(s.started_at)}` : 'Not started'}
</p>
</div>
<div className="flex items-center gap-2">
@@ -498,6 +525,7 @@ export function TreeLibraryPage() {
<TreeGridView
trees={trees}
onStartSession={handleStartSession}
onPrepareSession={handlePrepareSession}
onTagClick={handleTagClick}
onFolderCreated={handleCreateFolder}
onDeleteTree={(tree) => {
@@ -515,6 +543,7 @@ export function TreeLibraryPage() {
<TreeListView
trees={trees}
onStartSession={handleStartSession}
onPrepareSession={handlePrepareSession}
onTagClick={handleTagClick}
onFolderCreated={handleCreateFolder}
onDeleteTree={(tree) => {
@@ -532,6 +561,7 @@ export function TreeLibraryPage() {
<TreeTableView
trees={trees}
onStartSession={handleStartSession}
onPrepareSession={handlePrepareSession}
onTagClick={handleTagClick}
onFolderCreated={handleCreateFolder}
onDeleteTree={(tree) => {
@@ -604,6 +634,18 @@ export function TreeLibraryPage() {
onClose={() => { setShowImportModal(false); loadTrees() }}
/>
)}
{prepareTarget && (
<PrepareSessionModal
isOpen
onClose={() => setPrepareTarget(null)}
treeId={prepareTarget.tree.id}
treeName={prepareTarget.tree.name}
intakeFields={prepareTarget.intakeFields}
teamMembers={prepareTarget.teamMembers}
onPrepared={() => setPrepareTarget(null)}
/>
)}
</div>
</div>
)

View File

@@ -177,10 +177,10 @@ export function TreeNavigationPage() {
const deriveCurrentStepEnteredAt = (sessionData: Session): string => {
if (!sessionData.decisions || sessionData.decisions.length === 0) {
return sessionData.started_at
return sessionData.started_at || new Date().toISOString()
}
const lastDecision = sessionData.decisions[sessionData.decisions.length - 1]
return lastDecision.exited_at || lastDecision.timestamp || sessionData.started_at
return lastDecision.exited_at || lastDecision.timestamp || sessionData.started_at || new Date().toISOString()
}
const openCompletionModal = (completionDecision: DecisionRecord, source: CompletionSource) => {
@@ -320,7 +320,7 @@ export function TreeNavigationPage() {
client_name: clientName || undefined,
})
setSession(newSession)
setCurrentStepEnteredAt(newSession.started_at)
setCurrentStepEnteredAt(newSession.started_at || new Date().toISOString())
setShowMetadataForm(false)
// Save for "Repeat Last Session"
safeSetItem('last-session', JSON.stringify({