orange-400→blue-400, orange-500→blue-500, orange-600→blue-600 across ~21 component files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
240 lines
9.8 KiB
TypeScript
240 lines
9.8 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { X, UserPlus, FileText } from 'lucide-react'
|
|
import type { IntakeFormField } from '@/types'
|
|
import { sessionsApi } from '@/api/sessions'
|
|
import { cn } from '@/lib/utils'
|
|
import { toast } from '@/lib/toast'
|
|
|
|
interface TeamMember {
|
|
id: string
|
|
name: string
|
|
email: string
|
|
}
|
|
|
|
interface PrepareSessionModalProps {
|
|
isOpen: boolean
|
|
onClose: () => void
|
|
treeId: string
|
|
treeName: string
|
|
intakeFields: IntakeFormField[]
|
|
teamMembers?: TeamMember[]
|
|
onPrepared?: () => void
|
|
}
|
|
|
|
export function PrepareSessionModal({
|
|
isOpen,
|
|
onClose,
|
|
treeId,
|
|
treeName,
|
|
intakeFields,
|
|
teamMembers = [],
|
|
onPrepared,
|
|
}: PrepareSessionModalProps) {
|
|
const [values, setValues] = useState<Record<string, string>>({})
|
|
const [assignedToId, setAssignedToId] = useState('')
|
|
const [ticketNumber, setTicketNumber] = useState('')
|
|
const [clientName, setClientName] = useState('')
|
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
|
|
// Reset on open
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
setValues({})
|
|
setAssignedToId('')
|
|
setTicketNumber('')
|
|
setClientName('')
|
|
}
|
|
}, [isOpen])
|
|
|
|
if (!isOpen) return null
|
|
|
|
const handleFieldChange = (variableName: string, value: string) => {
|
|
setValues(prev => ({ ...prev, [variableName]: value }))
|
|
}
|
|
|
|
const handleSubmit = async () => {
|
|
setIsSubmitting(true)
|
|
try {
|
|
// Clean empty values
|
|
const cleanedVars: Record<string, string> = {}
|
|
for (const [k, v] of Object.entries(values)) {
|
|
if (v.trim()) cleanedVars[k] = v.trim()
|
|
}
|
|
|
|
await sessionsApi.prepare({
|
|
tree_id: treeId,
|
|
session_variables: Object.keys(cleanedVars).length > 0 ? cleanedVars : undefined,
|
|
assigned_to_id: assignedToId || undefined,
|
|
ticket_number: ticketNumber.trim() || undefined,
|
|
client_name: clientName.trim() || undefined,
|
|
})
|
|
|
|
toast.success('Session prepared successfully')
|
|
onPrepared?.()
|
|
onClose()
|
|
} catch {
|
|
toast.error('Failed to prepare session')
|
|
} finally {
|
|
setIsSubmitting(false)
|
|
}
|
|
}
|
|
|
|
// Group fields by group_name
|
|
const grouped = new Map<string, IntakeFormField[]>()
|
|
for (const field of [...intakeFields].sort((a, b) => a.display_order - b.display_order)) {
|
|
const group = field.group_name || 'General'
|
|
if (!grouped.has(group)) grouped.set(group, [])
|
|
grouped.get(group)!.push(field)
|
|
}
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-end justify-center p-0 sm:items-center sm:p-4">
|
|
<div className="absolute inset-0 bg-background/60" onClick={onClose} />
|
|
<div className="relative w-full max-w-full rounded-t-2xl border border-border bg-card shadow-2xl sm:max-w-lg sm:rounded-2xl">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between border-b border-border px-5 py-4">
|
|
<div className="flex items-center gap-2">
|
|
<FileText className="h-4 w-4 text-blue-400" />
|
|
<h3 className="text-sm font-semibold text-foreground">Prepare Session</h3>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="rounded-lg p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Body */}
|
|
<div className="max-h-[70vh] overflow-y-auto p-4 space-y-5 sm:max-h-[60vh] sm:p-5">
|
|
{/* Flow name */}
|
|
<div className="rounded-lg bg-accent px-3 py-2">
|
|
<p className="text-xs text-muted-foreground">Flow</p>
|
|
<p className="text-sm font-medium text-foreground">{treeName}</p>
|
|
</div>
|
|
|
|
{/* Context fields */}
|
|
<div className="space-y-3">
|
|
<div>
|
|
<label className="mb-1 block text-xs font-medium text-muted-foreground">Ticket Number</label>
|
|
<input
|
|
type="text"
|
|
value={ticketNumber}
|
|
onChange={(e) => setTicketNumber(e.target.value)}
|
|
placeholder="e.g. TKT-12345"
|
|
className="w-full rounded-md border border-border bg-card px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="mb-1 block text-xs font-medium text-muted-foreground">Client Name</label>
|
|
<input
|
|
type="text"
|
|
value={clientName}
|
|
onChange={(e) => setClientName(e.target.value)}
|
|
placeholder="e.g. Acme Corp"
|
|
className="w-full rounded-md border border-border bg-card px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Assignee */}
|
|
{teamMembers.length > 0 && (
|
|
<div>
|
|
<label className="mb-1 flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
|
|
<UserPlus className="h-3.5 w-3.5" />
|
|
Assign to Engineer
|
|
</label>
|
|
<select
|
|
value={assignedToId}
|
|
onChange={(e) => setAssignedToId(e.target.value)}
|
|
className="w-full rounded-md border border-border bg-card px-3 py-1.5 text-sm text-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
|
>
|
|
<option value="">Unassigned (visible to all)</option>
|
|
{teamMembers.map(m => (
|
|
<option key={m.id} value={m.id}>{m.name} ({m.email})</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)}
|
|
|
|
{/* Intake form fields */}
|
|
{intakeFields.length > 0 && (
|
|
<div className="space-y-4">
|
|
<h4 className="font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
|
Variables (optional — can be filled later)
|
|
</h4>
|
|
{Array.from(grouped.entries()).map(([groupName, fields]) => (
|
|
<div key={groupName} className="space-y-3">
|
|
{grouped.size > 1 && (
|
|
<p className="text-xs font-medium text-muted-foreground">{groupName}</p>
|
|
)}
|
|
{fields.map(field => (
|
|
<div key={field.variable_name}>
|
|
<label className="mb-1 block text-xs font-medium text-muted-foreground">
|
|
{field.label}
|
|
{field.required && <span className="ml-1 text-amber-400">*</span>}
|
|
</label>
|
|
{field.field_type === 'select' && field.options?.length ? (
|
|
<select
|
|
value={values[field.variable_name] || ''}
|
|
onChange={(e) => handleFieldChange(field.variable_name, e.target.value)}
|
|
className="w-full rounded-md border border-border bg-card px-3 py-1.5 text-sm text-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
|
>
|
|
<option value="">{field.placeholder || 'Select...'}</option>
|
|
{field.options.map(opt => (
|
|
<option key={opt} value={opt}>{opt}</option>
|
|
))}
|
|
</select>
|
|
) : field.field_type === 'textarea' ? (
|
|
<textarea
|
|
value={values[field.variable_name] || ''}
|
|
onChange={(e) => handleFieldChange(field.variable_name, e.target.value)}
|
|
placeholder={field.placeholder}
|
|
rows={3}
|
|
className="w-full rounded-md border border-border bg-card px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
|
/>
|
|
) : (
|
|
<input
|
|
type={field.field_type === 'number' ? 'number' : field.field_type === 'email' ? 'email' : 'text'}
|
|
value={values[field.variable_name] || ''}
|
|
onChange={(e) => handleFieldChange(field.variable_name, e.target.value)}
|
|
placeholder={field.placeholder}
|
|
className="w-full rounded-md border border-border bg-card px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(96,165,250,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20"
|
|
/>
|
|
)}
|
|
{field.help_text && (
|
|
<p className="mt-1 text-[0.625rem] text-muted-foreground">{field.help_text}</p>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="flex items-center justify-end gap-3 border-t border-border px-5 py-4">
|
|
<button
|
|
onClick={onClose}
|
|
className="rounded-lg px-4 py-2 text-sm text-muted-foreground hover:text-foreground"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onClick={handleSubmit}
|
|
disabled={isSubmitting}
|
|
className={cn(
|
|
'rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-white',
|
|
'hover:brightness-110 active:scale-[0.98]',
|
|
'disabled:opacity-40'
|
|
)}
|
|
>
|
|
{isSubmitting ? 'Preparing...' : 'Prepare Session'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|