From 009780e64cd2a9c62d655c2528abb1c8c4d72592 Mon Sep 17 00:00:00 2001 From: JSC Date: Fri, 29 Aug 2025 00:09:45 +0200 Subject: [PATCH] feat: add schedulers feature with task management - Introduced SchedulersPage for managing scheduled tasks. - Implemented CreateTaskDialog for creating new scheduled tasks. - Added SchedulersHeader for filtering and searching tasks. - Created SchedulersTable to display scheduled tasks with actions. - Implemented loading and error states with SchedulersLoadingStates. - Added API service for task management in schedulers. - Enhanced date formatting utility to handle timezone. - Updated AppSidebar and AppRoutes to include SchedulersPage. --- src/App.tsx | 9 + src/components/AppSidebar.tsx | 2 + .../schedulers/CreateTaskDialog.tsx | 308 ++++++++++++++++++ .../schedulers/SchedulersHeader.tsx | 138 ++++++++ .../schedulers/SchedulersLoadingStates.tsx | 124 +++++++ src/components/schedulers/SchedulersTable.tsx | 216 ++++++++++++ src/lib/api/services/index.ts | 1 + src/lib/api/services/schedulers.ts | 201 ++++++++++++ src/pages/SchedulersPage.tsx | 176 ++++++++++ src/utils/format-date.ts | 30 ++ 10 files changed, 1205 insertions(+) create mode 100644 src/components/schedulers/CreateTaskDialog.tsx create mode 100644 src/components/schedulers/SchedulersHeader.tsx create mode 100644 src/components/schedulers/SchedulersLoadingStates.tsx create mode 100644 src/components/schedulers/SchedulersTable.tsx create mode 100644 src/lib/api/services/schedulers.ts create mode 100644 src/pages/SchedulersPage.tsx diff --git a/src/App.tsx b/src/App.tsx index 4c36933..c73b477 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,6 +12,7 @@ import { LoginPage } from './pages/LoginPage' import { PlaylistEditPage } from './pages/PlaylistEditPage' import { PlaylistsPage } from './pages/PlaylistsPage' import { RegisterPage } from './pages/RegisterPage' +import { SchedulersPage } from './pages/SchedulersPage' import { SoundsPage } from './pages/SoundsPage' import { SettingsPage } from './pages/admin/SettingsPage' import { UsersPage } from './pages/admin/UsersPage' @@ -110,6 +111,14 @@ function AppRoutes() { } /> + + + + } + /> + {user.role === 'admin' && ( diff --git a/src/components/schedulers/CreateTaskDialog.tsx b/src/components/schedulers/CreateTaskDialog.tsx new file mode 100644 index 0000000..bec2189 --- /dev/null +++ b/src/components/schedulers/CreateTaskDialog.tsx @@ -0,0 +1,308 @@ +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { Textarea } from '@/components/ui/textarea' +import { + type CreateScheduledTaskRequest, + type RecurrenceType, + type TaskType, + getRecurrenceTypeLabel, + getTaskTypeLabel, +} from '@/lib/api/services/schedulers' +import { getSupportedTimezones } from '@/utils/locale' +import { useLocale } from '@/hooks/use-locale' +import { CalendarPlus, Loader2 } from 'lucide-react' +import { useState } from 'react' + +interface CreateTaskDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + loading: boolean + onSubmit: (data: CreateScheduledTaskRequest) => void + onCancel: () => void +} + +const TASK_TYPES: TaskType[] = ['CREDIT_RECHARGE', 'PLAY_SOUND', 'PLAY_PLAYLIST'] +const RECURRENCE_TYPES: RecurrenceType[] = ['NONE', 'HOURLY', 'DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY', 'CRON'] + +export function CreateTaskDialog({ + open, + onOpenChange, + loading, + onSubmit, + onCancel, +}: CreateTaskDialogProps) { + const { timezone } = useLocale() + + const [formData, setFormData] = useState({ + name: '', + task_type: 'PLAY_SOUND', + scheduled_at: '', + timezone: timezone, + parameters: {}, + recurrence_type: 'NONE', + cron_expression: null, + recurrence_count: null, + expires_at: null, + }) + + const [parametersJson, setParametersJson] = useState('{}') + const [parametersError, setParametersError] = useState(null) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + + // Validate parameters JSON + try { + const parameters = JSON.parse(parametersJson) + onSubmit({ + ...formData, + parameters, + }) + } catch { + setParametersError('Invalid JSON format') + } + } + + const handleParametersChange = (value: string) => { + setParametersJson(value) + setParametersError(null) + + // Try to parse JSON to validate + try { + JSON.parse(value) + } catch { + if (value.trim()) { + setParametersError('Invalid JSON format') + } + } + } + + const handleCancel = () => { + // Reset form + setFormData({ + name: '', + task_type: 'PLAY_SOUND', + scheduled_at: '', + timezone: timezone, + parameters: {}, + recurrence_type: 'NONE', + cron_expression: null, + recurrence_count: null, + expires_at: null, + }) + setParametersJson('{}') + setParametersError(null) + onCancel() + } + + const getDefaultScheduledTime = () => { + const now = new Date() + now.setMinutes(now.getMinutes() + 10) // Default to 10 minutes from now + + // Format the time in the user's timezone + const formatter = new Intl.DateTimeFormat('fr-FR', { + timeZone: timezone, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + hour12: false, + }) + + const parts = formatter.formatToParts(now) + const partsObj = parts.reduce((acc, part) => { + acc[part.type] = part.value + return acc + }, {} as Record) + + // Return in datetime-local format (YYYY-MM-DDTHH:MM) + return `${partsObj.year}-${partsObj.month}-${partsObj.day}T${partsObj.hour}:${partsObj.minute}` + } + + return ( + + + + + + Create Scheduled Task + + + +
+
+
+ + setFormData(prev => ({ ...prev, name: e.target.value }))} + placeholder="Enter task name" + required + /> +
+ +
+ + +
+
+ +
+
+ + setFormData(prev => ({ ...prev, scheduled_at: e.target.value }))} + required + /> +
+ +
+ + +
+
+ +
+
+ + +
+ + {formData.recurrence_type === 'CRON' && ( +
+ + setFormData(prev => ({ ...prev, cron_expression: e.target.value }))} + placeholder="0 0 * * *" + /> +
+ )} + + {formData.recurrence_type !== 'NONE' && formData.recurrence_type !== 'CRON' && ( +
+ + setFormData(prev => ({ + ...prev, + recurrence_count: e.target.value ? parseInt(e.target.value) : null + }))} + placeholder="Leave empty for infinite" + /> +
+ )} +
+ +
+ + setFormData(prev => ({ ...prev, expires_at: e.target.value || null }))} + /> +
+ +
+ +