Files
resolutionflow/frontend/src/components/procedural-editor/MaintenanceScheduleSection.tsx
Michael Chihlas 303a558432 refactor: replace hardcoded hex values with Tailwind semantic tokens
3,200+ hardcoded color values replaced with CSS variable-backed
Tailwind classes (bg-card, text-foreground, border-border, etc.).
Enables light mode via CSS variable swap. Only syntax highlighting
colors and intentional one-offs remain hardcoded (~15 values).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 04:34:35 -04:00

242 lines
8.6 KiB
TypeScript

import { useState, useEffect } from 'react'
import { Clock } from 'lucide-react'
import { maintenanceSchedulesApi } from '@/api/maintenanceSchedules'
import { targetListsApi } from '@/api/targetLists'
import type { MaintenanceSchedule, TargetList } from '@/types'
import { cn } from '@/lib/utils'
import { toast } from '@/lib/toast'
interface MaintenanceScheduleSectionProps {
treeId: string | null
onScheduleLoaded?: (schedule: MaintenanceSchedule | null, targetList: TargetList | null) => void
}
const FREQUENCY_OPTIONS = [
{ value: 'daily', label: 'Daily', cron: (hour: number, min: number) => `${min} ${hour} * * *` },
{ value: 'weekly-mon', label: 'Weekly (Monday)', cron: (hour: number, min: number) => `${min} ${hour} * * 1` },
{ value: 'weekly-fri', label: 'Weekly (Friday)', cron: (hour: number, min: number) => `${min} ${hour} * * 5` },
{ value: 'monthly', label: 'Monthly (1st)', cron: (hour: number, min: number) => `${min} ${hour} 1 * *` },
] as const
const TIMEZONE_OPTIONS = [
'UTC',
'America/New_York',
'America/Chicago',
'America/Denver',
'America/Los_Angeles',
'Europe/London',
'Europe/Berlin',
'Asia/Tokyo',
'Australia/Sydney',
]
export function MaintenanceScheduleSection({ treeId, onScheduleLoaded }: MaintenanceScheduleSectionProps) {
const [schedule, setSchedule] = useState<MaintenanceSchedule | null>(null)
const [targetLists, setTargetLists] = useState<TargetList[]>([])
const [isLoading, setIsLoading] = useState(false)
const [isSaving, setIsSaving] = useState(false)
// Draft form state (local UI only — not in store)
const [frequency, setFrequency] = useState('weekly-mon')
const [hour, setHour] = useState(9)
const [minute, setMinute] = useState(0)
const [timezone, setTimezone] = useState('UTC')
const [selectedTargetListId, setSelectedTargetListId] = useState<string>('')
// Load existing schedule and target lists
useEffect(() => {
const load = async () => {
setIsLoading(true)
try {
const lists = await targetListsApi.list()
setTargetLists(lists)
if (treeId) {
try {
const existing = await maintenanceSchedulesApi.getForTree(treeId)
setSchedule(existing)
// Hydrate form from existing schedule
hydrateFromSchedule(existing)
if (existing.target_list_id) {
setSelectedTargetListId(existing.target_list_id)
const tl = lists.find(l => l.id === existing.target_list_id) || null
onScheduleLoaded?.(existing, tl)
} else {
onScheduleLoaded?.(existing, null)
}
} catch {
// 404 = no schedule yet (valid state)
onScheduleLoaded?.(null, null)
}
}
} catch {
// Target lists may not load — non-critical
} finally {
setIsLoading(false)
}
}
load()
}, [treeId, onScheduleLoaded])
const hydrateFromSchedule = (s: MaintenanceSchedule) => {
const parts = s.cron_expression.split(' ')
const min = parseInt(parts[0] || '0', 10)
const hr = parseInt(parts[1] || '9', 10)
setMinute(min)
setHour(hr)
setTimezone(s.timezone)
// Detect frequency from cron
const dayOfMonth = parts[2]
const dayOfWeek = parts[4]
if (dayOfMonth === '1') {
setFrequency('monthly')
} else if (dayOfWeek === '*') {
setFrequency('daily')
} else if (dayOfWeek === '5') {
setFrequency('weekly-fri')
} else {
setFrequency('weekly-mon')
}
}
const handleSaveSchedule = async () => {
if (!treeId) {
toast.error('Save the flow first before configuring a schedule')
return
}
setIsSaving(true)
try {
const freqOption = FREQUENCY_OPTIONS.find(f => f.value === frequency)
const cronExpression = freqOption?.cron(hour, minute) ?? `${minute} ${hour} * * 1`
if (schedule) {
const updated = await maintenanceSchedulesApi.update(schedule.id, {
cron_expression: cronExpression,
timezone,
target_list_id: selectedTargetListId || undefined,
})
setSchedule(updated)
const tl = targetLists.find(l => l.id === selectedTargetListId) || null
onScheduleLoaded?.(updated, tl)
toast.success('Schedule updated')
} else {
const created = await maintenanceSchedulesApi.create({
tree_id: treeId,
cron_expression: cronExpression,
timezone,
target_list_id: selectedTargetListId || undefined,
})
setSchedule(created)
const tl = targetLists.find(l => l.id === selectedTargetListId) || null
onScheduleLoaded?.(created, tl)
toast.success('Schedule created')
}
} catch {
toast.error('Failed to save schedule. The flow was saved successfully — you can retry the schedule.')
} finally {
setIsSaving(false)
}
}
if (isLoading) {
return (
<div className="py-3 text-sm text-muted-foreground">Loading schedule...</div>
)
}
return (
<div className="space-y-4">
{/* Frequency */}
<div>
<label className="mb-1 block text-sm font-medium text-muted-foreground">Frequency</label>
<select
value={frequency}
onChange={(e) => setFrequency(e.target.value)}
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
>
{FREQUENCY_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</div>
{/* Time */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="mb-1 block text-sm font-medium text-muted-foreground">Hour</label>
<input
type="number"
min={0}
max={23}
value={hour}
onChange={(e) => setHour(Number(e.target.value))}
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-muted-foreground">Minute</label>
<input
type="number"
min={0}
max={59}
value={minute}
onChange={(e) => setMinute(Number(e.target.value))}
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
/>
</div>
</div>
{/* Timezone */}
<div>
<label className="mb-1 block text-sm font-medium text-muted-foreground">Timezone</label>
<select
value={timezone}
onChange={(e) => setTimezone(e.target.value)}
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
>
{TIMEZONE_OPTIONS.map(tz => (
<option key={tz} value={tz}>{tz}</option>
))}
</select>
</div>
{/* Target List */}
{targetLists.length > 0 && (
<div>
<label className="mb-1 block text-sm font-medium text-muted-foreground">Target List (optional)</label>
<select
value={selectedTargetListId}
onChange={(e) => setSelectedTargetListId(e.target.value)}
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-foreground focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20"
>
<option value="">None manual targets only</option>
{targetLists.map(tl => (
<option key={tl.id} value={tl.id}>{tl.name} ({tl.targets.length} targets)</option>
))}
</select>
</div>
)}
{/* Save button */}
<button
onClick={handleSaveSchedule}
disabled={isSaving || !treeId}
className={cn(
'flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium',
treeId
? 'bg-primary text-white hover:brightness-110'
: 'cursor-not-allowed border border-border bg-card text-muted-foreground opacity-50'
)}
>
<Clock className="h-4 w-4" />
{isSaving ? 'Saving...' : schedule ? 'Update Schedule' : 'Create Schedule'}
</button>
{!treeId && (
<p className="text-xs text-muted-foreground">Save the flow first to configure a schedule.</p>
)}
</div>
)
}