feat: add calendar plan create and edit ui

This commit is contained in:
2026-04-04 15:30:03 +00:00
parent f39e7da33c
commit 8014dcd602

View File

@@ -51,6 +51,9 @@ interface PlanListResponse {
plans: SchedulePlanResponse[] plans: SchedulePlanResponse[]
} }
type Weekday = 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'
type MonthName = 'jan' | 'feb' | 'mar' | 'apr' | 'may' | 'jun' | 'jul' | 'aug' | 'sep' | 'oct' | 'nov' | 'dec'
interface WorkloadWarning { interface WorkloadWarning {
period: string period: string
slot_type: string slot_type: string
@@ -77,6 +80,31 @@ const EVENT_TYPES: { value: EventType; label: string }[] = [
{ value: 'system_event', label: 'System Event' }, { value: 'system_event', label: 'System Event' },
] ]
const WEEKDAYS: { value: Weekday; label: string }[] = [
{ value: 'sun', label: 'Sunday' },
{ value: 'mon', label: 'Monday' },
{ value: 'tue', label: 'Tuesday' },
{ value: 'wed', label: 'Wednesday' },
{ value: 'thu', label: 'Thursday' },
{ value: 'fri', label: 'Friday' },
{ value: 'sat', label: 'Saturday' },
]
const MONTHS: { value: MonthName; label: string }[] = [
{ value: 'jan', label: 'January' },
{ value: 'feb', label: 'February' },
{ value: 'mar', label: 'March' },
{ value: 'apr', label: 'April' },
{ value: 'may', label: 'May' },
{ value: 'jun', label: 'June' },
{ value: 'jul', label: 'July' },
{ value: 'aug', label: 'August' },
{ value: 'sep', label: 'September' },
{ value: 'oct', label: 'October' },
{ value: 'nov', label: 'November' },
{ value: 'dec', label: 'December' },
]
const SLOT_TYPE_ICONS: Record<string, string> = { const SLOT_TYPE_ICONS: Record<string, string> = {
work: '💼', work: '💼',
on_call: '📞', on_call: '📞',
@@ -116,6 +144,20 @@ export default function CalendarPage() {
}) })
const [slotSaving, setSlotSaving] = useState(false) const [slotSaving, setSlotSaving] = useState(false)
const [warnings, setWarnings] = useState<WorkloadWarning[]>([]) const [warnings, setWarnings] = useState<WorkloadWarning[]>([])
const [showPlanModal, setShowPlanModal] = useState(false)
const [editingPlan, setEditingPlan] = useState<SchedulePlanResponse | null>(null)
const [planSaving, setPlanSaving] = useState(false)
const [planForm, setPlanForm] = useState({
slot_type: 'work' as SlotType,
estimated_duration: 25,
at_time: '09:00',
on_day: '' as Weekday | '',
on_week: '' as string,
on_month: '' as MonthName | '',
event_type: '' as string,
event_data_code: '',
event_data_event: '',
})
const fetchSlots = async (date: string) => { const fetchSlots = async (date: string) => {
setLoading(true) setLoading(true)
@@ -211,6 +253,84 @@ export default function CalendarPage() {
return null return null
} }
const buildPlanEventData = () => {
if (!planForm.event_type) return null
if (planForm.event_type === 'job') {
return planForm.event_data_code ? { type: 'Task', code: planForm.event_data_code } : null
}
if (planForm.event_type === 'system_event') {
return planForm.event_data_event ? { event: planForm.event_data_event } : null
}
return null
}
const openCreatePlanModal = () => {
setEditingPlan(null)
setPlanForm({
slot_type: 'work',
estimated_duration: 25,
at_time: '09:00',
on_day: '',
on_week: '',
on_month: '',
event_type: '',
event_data_code: '',
event_data_event: '',
})
setError('')
setShowPlanModal(true)
}
const openEditPlanModal = (plan: SchedulePlanResponse) => {
setEditingPlan(plan)
setPlanForm({
slot_type: plan.slot_type,
estimated_duration: plan.estimated_duration,
at_time: plan.at_time.slice(0, 5),
on_day: (plan.on_day?.toLowerCase() as Weekday | undefined) || '',
on_week: plan.on_week ? String(plan.on_week) : '',
on_month: (plan.on_month?.toLowerCase() as MonthName | undefined) || '',
event_type: plan.event_type || '',
event_data_code: plan.event_data?.code || '',
event_data_event: plan.event_data?.event || '',
})
setError('')
setShowPlanModal(true)
}
const handleSavePlan = async () => {
setPlanSaving(true)
setError('')
try {
const payload: any = {
slot_type: planForm.slot_type,
estimated_duration: planForm.estimated_duration,
at_time: planForm.at_time,
on_day: planForm.on_day || null,
on_week: planForm.on_week ? Number(planForm.on_week) : null,
on_month: planForm.on_month || null,
event_type: planForm.event_type || null,
event_data: buildPlanEventData(),
}
if (editingPlan) {
await api.patch(`/calendar/plans/${editingPlan.id}`, payload)
} else {
await api.post('/calendar/plans', payload)
}
setShowPlanModal(false)
fetchPlans()
if (activeTab === 'daily') {
fetchSlots(selectedDate)
}
} catch (err: any) {
setError(err.response?.data?.detail || 'Save plan failed')
} finally {
setPlanSaving(false)
}
}
const handleSaveSlot = async () => { const handleSaveSlot = async () => {
setSlotSaving(true) setSlotSaving(true)
setError('') setError('')
@@ -429,7 +549,12 @@ export default function CalendarPage() {
)} )}
{activeTab === 'plans' && ( {activeTab === 'plans' && (
<div className="milestone-grid"> <>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 16 }}>
<button className="btn-primary" onClick={openCreatePlanModal}>+ New Plan</button>
</div>
<div className="milestone-grid">
{plans.length === 0 ? ( {plans.length === 0 ? (
<div className="empty" style={{ padding: '40px 0', color: 'var(--text-secondary)', textAlign: 'center' }}> <div className="empty" style={{ padding: '40px 0', color: 'var(--text-secondary)', textAlign: 'center' }}>
No schedule plans configured. No schedule plans configured.
@@ -449,7 +574,14 @@ export default function CalendarPage() {
{plan.event_type && <div>📌 {plan.event_type.replace('_', ' ')}</div>} {plan.event_type && <div>📌 {plan.event_type.replace('_', ' ')}</div>}
</div> </div>
{plan.is_active && ( {plan.is_active && (
<div style={{ marginTop: 8 }}> <div style={{ marginTop: 8, display: 'flex', gap: 8 }}>
<button
className="btn-transition"
style={{ padding: '4px 10px', fontSize: '0.8rem' }}
onClick={() => openEditPlanModal(plan)}
>
Edit Plan
</button>
<button <button
className="btn-danger" className="btn-danger"
style={{ padding: '4px 10px', fontSize: '0.8rem' }} style={{ padding: '4px 10px', fontSize: '0.8rem' }}
@@ -462,7 +594,8 @@ export default function CalendarPage() {
</div> </div>
)) ))
)} )}
</div> </div>
</>
)} )}
{/* Create/Edit Slot Modal */} {/* Create/Edit Slot Modal */}
@@ -577,6 +710,142 @@ export default function CalendarPage() {
</div> </div>
</div> </div>
)} )}
{showPlanModal && (
<div className="modal-overlay" onClick={() => setShowPlanModal(false)}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<h3>{editingPlan ? 'Edit Plan' : 'New Plan'}</h3>
<label style={{ display: 'block', marginBottom: 8 }}>
<strong>Slot Type</strong>
<select
value={planForm.slot_type}
onChange={(e) => setPlanForm({ ...planForm, slot_type: e.target.value as SlotType })}
style={{ width: '100%', marginTop: 4 }}
>
{SLOT_TYPES.map((t) => (
<option key={t.value} value={t.value}>{t.label}</option>
))}
</select>
</label>
<label style={{ display: 'block', marginBottom: 8 }}>
<strong>At Time</strong>
<input
type="time"
value={planForm.at_time}
onChange={(e) => setPlanForm({ ...planForm, at_time: e.target.value })}
style={{ width: '100%', marginTop: 4 }}
/>
</label>
<label style={{ display: 'block', marginBottom: 8 }}>
<strong>Estimated Duration (minutes, 150)</strong>
<input
type="number"
min={1}
max={50}
value={planForm.estimated_duration}
onChange={(e) => setPlanForm({ ...planForm, estimated_duration: Number(e.target.value) })}
style={{ width: '100%', marginTop: 4 }}
/>
</label>
<label style={{ display: 'block', marginBottom: 8 }}>
<strong>On Day (optional)</strong>
<select
value={planForm.on_day}
onChange={(e) => setPlanForm({ ...planForm, on_day: e.target.value as Weekday | '' })}
style={{ width: '100%', marginTop: 4 }}
>
<option value="">Every day</option>
{WEEKDAYS.map((d) => (
<option key={d.value} value={d.value}>{d.label}</option>
))}
</select>
</label>
<label style={{ display: 'block', marginBottom: 8 }}>
<strong>On Week (optional)</strong>
<select
value={planForm.on_week}
onChange={(e) => setPlanForm({ ...planForm, on_week: e.target.value })}
style={{ width: '100%', marginTop: 4 }}
>
<option value="">Every matching week</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select>
</label>
<label style={{ display: 'block', marginBottom: 8 }}>
<strong>On Month (optional)</strong>
<select
value={planForm.on_month}
onChange={(e) => setPlanForm({ ...planForm, on_month: e.target.value as MonthName | '' })}
style={{ width: '100%', marginTop: 4 }}
>
<option value="">Every month</option>
{MONTHS.map((m) => (
<option key={m.value} value={m.value}>{m.label}</option>
))}
</select>
</label>
<label style={{ display: 'block', marginBottom: 8 }}>
<strong>Event Type</strong>
<select
value={planForm.event_type}
onChange={(e) => setPlanForm({ ...planForm, event_type: e.target.value })}
style={{ width: '100%', marginTop: 4 }}
>
<option value="">None</option>
{EVENT_TYPES.map((t) => (
<option key={t.value} value={t.value}>{t.label}</option>
))}
</select>
</label>
{planForm.event_type === 'job' && (
<label style={{ display: 'block', marginBottom: 8 }}>
<strong>Job Code</strong>
<input
type="text"
value={planForm.event_data_code}
onChange={(e) => setPlanForm({ ...planForm, event_data_code: e.target.value })}
placeholder="e.g. TASK-42"
style={{ width: '100%', marginTop: 4 }}
/>
</label>
)}
{planForm.event_type === 'system_event' && (
<label style={{ display: 'block', marginBottom: 8 }}>
<strong>System Event</strong>
<select
value={planForm.event_data_event}
onChange={(e) => setPlanForm({ ...planForm, event_data_event: e.target.value })}
style={{ width: '100%', marginTop: 4 }}
>
<option value="">Select event</option>
<option value="ScheduleToday">Schedule Today</option>
<option value="SummaryToday">Summary Today</option>
<option value="ScheduledGatewayRestart">Scheduled Gateway Restart</option>
</select>
</label>
)}
<div style={{ marginTop: 12, display: 'flex', gap: 8 }}>
<button className="btn-primary" onClick={handleSavePlan} disabled={planSaving}>
{planSaving ? 'Saving...' : 'Save Plan'}
</button>
<button className="btn-back" onClick={() => setShowPlanModal(false)}>Cancel</button>
</div>
</div>
</div>
)}
</div> </div>
) )
} }