feat: enhance CreateTaskDialog with sound and playlist selection, including loading states and task-specific parameters

This commit is contained in:
JSC
2025-08-29 02:47:58 +02:00
parent 4251057668
commit 40b053c446

View File

@@ -22,10 +22,12 @@ import {
getRecurrenceTypeLabel,
getTaskTypeLabel,
} from '@/lib/api/services/schedulers'
import { soundsService } from '@/lib/api/services/sounds'
import { playlistsService } from '@/lib/api/services/playlists'
import { getSupportedTimezones } from '@/utils/locale'
import { useLocale } from '@/hooks/use-locale'
import { CalendarPlus, Loader2 } from 'lucide-react'
import { useState } from 'react'
import { CalendarPlus, Loader2, Music, PlayCircle } from 'lucide-react'
import { useState, useEffect } from 'react'
interface CreateTaskDialogProps {
open: boolean
@@ -61,32 +63,99 @@ export function CreateTaskDialog({
const [parametersJson, setParametersJson] = useState('{}')
const [parametersError, setParametersError] = useState<string | null>(null)
// Task-specific parameters
const [selectedSoundId, setSelectedSoundId] = useState<string>('')
const [selectedPlaylistId, setSelectedPlaylistId] = useState<string>('')
const [playMode, setPlayMode] = useState<string>('continuous')
const [shuffle, setShuffle] = useState<boolean>(false)
// Data loading
const [sounds, setSounds] = useState<Array<{id: number, name?: string, filename: string}>>([])
const [playlists, setPlaylists] = useState<Array<{id: number, name: string}>>([])
const [loadingSounds, setLoadingSounds] = useState(false)
const [loadingPlaylists, setLoadingPlaylists] = useState(false)
// Load sounds and playlists when dialog opens
useEffect(() => {
if (open) {
loadSounds()
loadPlaylists()
}
}, [open])
const loadSounds = async () => {
setLoadingSounds(true)
try {
const soundsData = await soundsService.getSounds({
types: ['SDB', 'TTS']
})
setSounds(soundsData || [])
} catch (error) {
console.error('Failed to load sounds:', error)
} finally {
setLoadingSounds(false)
}
}
const loadPlaylists = async () => {
setLoadingPlaylists(true)
try {
const playlistsData = await playlistsService.getPlaylists({})
setPlaylists(playlistsData.playlists || [])
} catch (error) {
console.error('Failed to load playlists:', error)
} finally {
setLoadingPlaylists(false)
}
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
// Validate parameters JSON
try {
const parameters = JSON.parse(parametersJson)
// Send the datetime as UTC to prevent backend timezone conversion
// The user's selected time should be stored exactly as entered
let scheduledAt = formData.scheduled_at
if (scheduledAt.length === 16) {
scheduledAt += ':00' // Add seconds if missing
// Build parameters based on task type
let parameters: Record<string, unknown> = {}
if (formData.task_type === 'play_sound') {
if (!selectedSoundId) {
setParametersError('Please select a sound')
return
}
parameters = { sound_id: parseInt(selectedSoundId) }
} else if (formData.task_type === 'play_playlist') {
if (!selectedPlaylistId) {
setParametersError('Please select a playlist')
return
}
parameters = {
playlist_id: parseInt(selectedPlaylistId),
play_mode: playMode,
shuffle: shuffle,
}
} else if (formData.task_type === 'credit_recharge') {
// For credit recharge, use the JSON textarea for custom parameters
try {
parameters = JSON.parse(parametersJson)
} catch {
setParametersError('Invalid JSON format')
return
}
// Add Z to indicate this is already UTC time, preventing backend conversion
const scheduledAtUTC = scheduledAt + 'Z'
onSubmit({
...formData,
parameters,
scheduled_at: scheduledAtUTC,
})
} catch {
setParametersError('Invalid JSON format')
}
// Send the datetime as UTC to prevent backend timezone conversion
let scheduledAt = formData.scheduled_at
if (scheduledAt.length === 16) {
scheduledAt += ':00' // Add seconds if missing
}
// Add Z to indicate this is already UTC time, preventing backend conversion
const scheduledAtUTC = scheduledAt + 'Z'
onSubmit({
...formData,
parameters,
scheduled_at: scheduledAtUTC,
})
}
const handleParametersChange = (value: string) => {
@@ -118,6 +187,10 @@ export function CreateTaskDialog({
})
setParametersJson('{}')
setParametersError(null)
setSelectedSoundId('')
setSelectedPlaylistId('')
setPlayMode('continuous')
setShuffle(false)
onCancel()
}
@@ -281,23 +354,120 @@ export function CreateTaskDialog({
/>
</div>
<div className="space-y-2">
<Label htmlFor="parameters">Task Parameters</Label>
<Textarea
id="parameters"
value={parametersJson}
onChange={(e) => handleParametersChange(e.target.value)}
placeholder='{"sound_id": 123, "playlist_id": 456}'
className="font-mono text-sm"
rows={4}
/>
{parametersError && (
<p className="text-sm text-destructive">{parametersError}</p>
)}
<p className="text-xs text-muted-foreground">
Enter task-specific parameters as JSON. For PLAY_SOUND use {"sound_id"}, for PLAY_PLAYLIST use {"playlist_id"}.
</p>
</div>
{/* Task-specific parameters based on task type */}
{formData.task_type === 'play_sound' && (
<div className="space-y-2">
<Label htmlFor="sound">Sound to Play</Label>
<Select
value={selectedSoundId}
onValueChange={setSelectedSoundId}
>
<SelectTrigger>
<SelectValue placeholder={loadingSounds ? "Loading sounds..." : "Select a sound"} />
</SelectTrigger>
<SelectContent>
{sounds.map((sound) => (
<SelectItem key={sound.id} value={sound.id.toString()}>
<div className="flex items-center gap-2">
<Music className="h-4 w-4" />
<span>{sound.name || sound.filename}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
{parametersError && (
<p className="text-sm text-destructive">{parametersError}</p>
)}
</div>
)}
{formData.task_type === 'play_playlist' && (
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="playlist">Playlist to Play</Label>
<Select
value={selectedPlaylistId}
onValueChange={setSelectedPlaylistId}
>
<SelectTrigger>
<SelectValue placeholder={loadingPlaylists ? "Loading playlists..." : "Select a playlist"} />
</SelectTrigger>
<SelectContent>
{playlists.map((playlist) => (
<SelectItem key={playlist.id} value={playlist.id.toString()}>
<div className="flex items-center gap-2">
<PlayCircle className="h-4 w-4" />
<span>{playlist.name}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="playMode">Play Mode</Label>
<Select
value={playMode}
onValueChange={setPlayMode}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="continuous">Continuous</SelectItem>
<SelectItem value="loop">Loop</SelectItem>
<SelectItem value="loop_one">Loop One</SelectItem>
<SelectItem value="random">Random</SelectItem>
<SelectItem value="single">Single</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="shuffle">Shuffle</Label>
<Select
value={shuffle.toString()}
onValueChange={(value) => setShuffle(value === 'true')}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="false">No</SelectItem>
<SelectItem value="true">Yes</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{parametersError && (
<p className="text-sm text-destructive">{parametersError}</p>
)}
</div>
)}
{formData.task_type === 'credit_recharge' && (
<div className="space-y-2">
<Label htmlFor="parameters">Credit Recharge Parameters</Label>
<Textarea
id="parameters"
value={parametersJson}
onChange={(e) => handleParametersChange(e.target.value)}
placeholder='{"user_id": 123} or {} for all users'
className="font-mono text-sm"
rows={3}
/>
{parametersError && (
<p className="text-sm text-destructive">{parametersError}</p>
)}
<p className="text-xs text-muted-foreground">
Optional: specify {"user_id"} to recharge specific user, or leave empty {} to recharge all users.
</p>
</div>
)}
<div className="flex justify-end gap-2">
<Button