'use client'; import { useState, useEffect } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Clock, Play, Pause, Trash2, Plus, Calendar, AlertCircle, CheckCircle2, XCircle, RefreshCw } from 'lucide-react'; import { useUserTimezone } from '@/lib/hooks/use-user-timezone'; interface ScheduleConfig { id: string; name: string; description: string; cron_expression: string; sync_type: 'incremental' | 'full'; years_back?: number; is_enabled: boolean; last_run?: string; next_run?: string; last_status?: 'success' | 'failed'; last_error?: string; created_at: string; updated_at: string; } interface ScheduleStatus { config: ScheduleConfig; isRunning: boolean; isValid: boolean; nextRun?: string; } export default function SyncScheduler() { const tz = useUserTimezone(); const [schedules, setSchedules] = useState([]); const [reloading, setReloading] = useState(false); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [editingSchedule, setEditingSchedule] = useState(null); const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); // Form state const [formData, setFormData] = useState({ id: '', name: '', description: '', cron_expression: '', sync_type: 'incremental' as 'incremental' | 'full', years_back: 2, is_enabled: false, }); // Common cron presets const cronPresets = [ { label: 'Every day at 2 AM', value: '0 2 * * *' }, { label: 'Every day at 3 AM', value: '0 3 * * *' }, { label: 'Every Sunday at 3 AM', value: '0 3 * * 0' }, { label: 'Every Monday at 2 AM', value: '0 2 * * 1' }, { label: 'Every 6 hours', value: '0 */6 * * *' }, { label: 'Every 12 hours', value: '0 */12 * * *' }, ]; useEffect(() => { fetchSchedules(); // Refresh every 30 seconds const interval = setInterval(fetchSchedules, 30000); return () => clearInterval(interval); }, []); const fetchSchedules = async () => { try { const response = await fetch('/api/sync/schedules'); const data = await response.json(); if (data.success) { setSchedules(data.schedules); setError(null); } else { setError(data.error || 'Failed to fetch schedules'); } } catch (err) { setError('Failed to fetch schedules'); console.error(err); } finally { setLoading(false); } }; const reloadSchedules = async () => { setReloading(true); try { const res = await fetch('/api/sync/schedules/reload', { method: 'POST' }); const data = await res.json(); if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`); await fetchSchedules(); } catch (err) { alert(`Reload failed: ${err instanceof Error ? err.message : err}`); } finally { setReloading(false); } }; const toggleSchedule = async (scheduleId: string, currentState: boolean) => { try { const response = await fetch(`/api/sync/schedules/${scheduleId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ is_enabled: !currentState }), }); const data = await response.json(); if (data.success) { fetchSchedules(); } else { alert(`Failed to toggle schedule: ${data.error}`); } } catch (err) { alert('Failed to toggle schedule'); console.error(err); } }; const triggerSchedule = async (scheduleId: string) => { if (!confirm('Are you sure you want to trigger this sync now?')) return; try { const response = await fetch(`/api/sync/schedules/${scheduleId}/trigger`, { method: 'POST', }); const data = await response.json(); if (data.success) { alert('Sync triggered successfully'); fetchSchedules(); } else { alert(`Failed to trigger sync: ${data.error}`); } } catch (err) { alert('Failed to trigger sync'); console.error(err); } }; const deleteSchedule = async (scheduleId: string) => { if (!confirm('Are you sure you want to delete this schedule?')) return; try { const response = await fetch(`/api/sync/schedules/${scheduleId}`, { method: 'DELETE', }); const data = await response.json(); if (data.success) { fetchSchedules(); } else { alert(`Failed to delete schedule: ${data.error}`); } } catch (err) { alert('Failed to delete schedule'); console.error(err); } }; const openEditDialog = (schedule: ScheduleConfig) => { setEditingSchedule(schedule); setFormData({ id: schedule.id, name: schedule.name, description: schedule.description, cron_expression: schedule.cron_expression, sync_type: schedule.sync_type, years_back: schedule.years_back || 2, is_enabled: schedule.is_enabled, }); setIsEditDialogOpen(true); }; const openCreateDialog = () => { setFormData({ id: '', name: '', description: '', cron_expression: '0 2 * * *', sync_type: 'incremental', years_back: 2, is_enabled: false, }); setIsCreateDialogOpen(true); }; const handleSubmit = async (isEdit: boolean) => { try { const url = isEdit ? `/api/sync/schedules/${formData.id}` : '/api/sync/schedules'; const method = isEdit ? 'PATCH' : 'POST'; const response = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData), }); const data = await response.json(); if (data.success) { setIsEditDialogOpen(false); setIsCreateDialogOpen(false); fetchSchedules(); } else { alert(`Failed to ${isEdit ? 'update' : 'create'} schedule: ${data.error}`); } } catch (err) { alert(`Failed to ${isEdit ? 'update' : 'create'} schedule`); console.error(err); } }; const formatDate = (dateString?: string) => { if (!dateString) return 'Never'; return new Date(dateString).toLocaleString(undefined, { timeZone: tz }); }; const formatNextRun = (dateString?: string) => { if (!dateString) return 'Not scheduled'; const date = new Date(dateString); const now = new Date(); const diff = date.getTime() - now.getTime(); if (diff < 0) return 'Calculating...'; const hours = Math.floor(diff / (1000 * 60 * 60)); const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); if (hours > 24) { const days = Math.floor(hours / 24); return `in ${days} day${days > 1 ? 's' : ''}`; } if (hours > 0) { return `in ${hours}h ${minutes}m`; } return `in ${minutes}m`; }; if (loading) { return (
Loading schedules...
); } return (
Sync Schedules Manage automatic sync schedules
{error && ( {error} )} {schedules.length === 0 ? (
No schedules configured. Create one to enable automatic syncs.
) : (
{schedules.map((schedule) => (

{schedule.config.name}

{schedule.config.is_enabled ? 'Enabled' : 'Disabled'} {schedule.isRunning && ( Running )} {!schedule.isValid && ( Invalid Cron )}

{schedule.config.description}

Type
{schedule.config.sync_type}
Schedule
{schedule.config.cron_expression}
Next Run
{schedule.config.is_enabled ? formatNextRun(schedule.config.next_run) : 'Disabled' }
Last Run
{schedule.config.last_status === 'success' && ( )} {schedule.config.last_status === 'failed' && ( )} {formatDate(schedule.config.last_run)}
{schedule.config.last_error && ( {schedule.config.last_error} )}
))}
)}
{/* Create Dialog */} Create New Schedule Configure a new automatic sync schedule
setFormData({ ...formData, id: e.target.value })} placeholder="e.g., daily-incremental" />
setFormData({ ...formData, name: e.target.value })} placeholder="e.g., Daily Incremental Sync" />
setFormData({ ...formData, description: e.target.value })} placeholder="Brief description of this schedule" />
{formData.sync_type === 'full' && (
setFormData({ ...formData, years_back: parseInt(e.target.value) })} />
)}
setFormData({ ...formData, cron_expression: e.target.value })} placeholder="0 2 * * *" className="font-mono" />

Format: minute hour day month weekday

setFormData({ ...formData, is_enabled: checked })} />
{/* Edit Dialog */} Edit Schedule Update schedule configuration
setFormData({ ...formData, name: e.target.value })} />
setFormData({ ...formData, description: e.target.value })} />
{formData.sync_type === 'full' && (
setFormData({ ...formData, years_back: parseInt(e.target.value) })} />
)}
setFormData({ ...formData, cron_expression: e.target.value })} className="font-mono" />
setFormData({ ...formData, is_enabled: checked })} />
); }