feat: enhance CreateTaskDialog with sound and playlist selection, including loading states and task-specific parameters
This commit is contained in:
@@ -22,10 +22,12 @@ import {
|
|||||||
getRecurrenceTypeLabel,
|
getRecurrenceTypeLabel,
|
||||||
getTaskTypeLabel,
|
getTaskTypeLabel,
|
||||||
} from '@/lib/api/services/schedulers'
|
} 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 { getSupportedTimezones } from '@/utils/locale'
|
||||||
import { useLocale } from '@/hooks/use-locale'
|
import { useLocale } from '@/hooks/use-locale'
|
||||||
import { CalendarPlus, Loader2 } from 'lucide-react'
|
import { CalendarPlus, Loader2, Music, PlayCircle } from 'lucide-react'
|
||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
interface CreateTaskDialogProps {
|
interface CreateTaskDialogProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -62,15 +64,85 @@ export function CreateTaskDialog({
|
|||||||
const [parametersJson, setParametersJson] = useState('{}')
|
const [parametersJson, setParametersJson] = useState('{}')
|
||||||
const [parametersError, setParametersError] = useState<string | null>(null)
|
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) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
// Validate parameters JSON
|
// 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 {
|
try {
|
||||||
const parameters = JSON.parse(parametersJson)
|
parameters = JSON.parse(parametersJson)
|
||||||
|
} catch {
|
||||||
|
setParametersError('Invalid JSON format')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Send the datetime as UTC to prevent backend timezone conversion
|
// 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
|
let scheduledAt = formData.scheduled_at
|
||||||
if (scheduledAt.length === 16) {
|
if (scheduledAt.length === 16) {
|
||||||
scheduledAt += ':00' // Add seconds if missing
|
scheduledAt += ':00' // Add seconds if missing
|
||||||
@@ -84,9 +156,6 @@ export function CreateTaskDialog({
|
|||||||
parameters,
|
parameters,
|
||||||
scheduled_at: scheduledAtUTC,
|
scheduled_at: scheduledAtUTC,
|
||||||
})
|
})
|
||||||
} catch {
|
|
||||||
setParametersError('Invalid JSON format')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleParametersChange = (value: string) => {
|
const handleParametersChange = (value: string) => {
|
||||||
@@ -118,6 +187,10 @@ export function CreateTaskDialog({
|
|||||||
})
|
})
|
||||||
setParametersJson('{}')
|
setParametersJson('{}')
|
||||||
setParametersError(null)
|
setParametersError(null)
|
||||||
|
setSelectedSoundId('')
|
||||||
|
setSelectedPlaylistId('')
|
||||||
|
setPlayMode('continuous')
|
||||||
|
setShuffle(false)
|
||||||
onCancel()
|
onCancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,23 +354,120 @@ export function CreateTaskDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Task-specific parameters based on task type */}
|
||||||
|
{formData.task_type === 'play_sound' && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="parameters">Task Parameters</Label>
|
<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
|
<Textarea
|
||||||
id="parameters"
|
id="parameters"
|
||||||
value={parametersJson}
|
value={parametersJson}
|
||||||
onChange={(e) => handleParametersChange(e.target.value)}
|
onChange={(e) => handleParametersChange(e.target.value)}
|
||||||
placeholder='{"sound_id": 123, "playlist_id": 456}'
|
placeholder='{"user_id": 123} or {} for all users'
|
||||||
className="font-mono text-sm"
|
className="font-mono text-sm"
|
||||||
rows={4}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
{parametersError && (
|
{parametersError && (
|
||||||
<p className="text-sm text-destructive">{parametersError}</p>
|
<p className="text-sm text-destructive">{parametersError}</p>
|
||||||
)}
|
)}
|
||||||
<p className="text-xs text-muted-foreground">
|
<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"}.
|
Optional: specify {"user_id"} to recharge specific user, or leave empty {} to recharge all users.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
Reference in New Issue
Block a user