From 987ed062d5cb26d5ad6e0c9e64a212e6e8c3f0aa Mon Sep 17 00:00:00 2001 From: Kelly Date: Sat, 13 Dec 2025 02:27:39 -0700 Subject: [PATCH] feat(tasks): Add "Make recurring" toggle to Create Task modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add checkbox to convert one-time task into recurring schedule - When enabled, shows schedule name, interval, and state filter options - Schedule runs immediately after creation so tasks appear right away - Update button text to "Create Schedule & Run" when recurring - Remove separate "New Schedule" button from schedules section - Update empty state text to guide users to new flow 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cannaiq/dist/index.html | 4 +- cannaiq/src/pages/TasksDashboard.tsx | 220 +++++++++++++++++++++------ 2 files changed, 172 insertions(+), 52 deletions(-) diff --git a/cannaiq/dist/index.html b/cannaiq/dist/index.html index 3d4663b9..fc585c57 100644 --- a/cannaiq/dist/index.html +++ b/cannaiq/dist/index.html @@ -7,8 +7,8 @@ CannaIQ - Cannabis Menu Intelligence Platform - - + +
diff --git a/cannaiq/src/pages/TasksDashboard.tsx b/cannaiq/src/pages/TasksDashboard.tsx index 57956a16..1cefe414 100644 --- a/cannaiq/src/pages/TasksDashboard.tsx +++ b/cannaiq/src/pages/TasksDashboard.tsx @@ -110,9 +110,20 @@ function CreateTaskModal({ isOpen, onClose, onTaskCreated }: CreateTaskModalProp const [storesLoading, setStoresLoading] = useState(false); const [error, setError] = useState(null); + // Recurring schedule options + const [makeRecurring, setMakeRecurring] = useState(false); + const [scheduleName, setScheduleName] = useState(''); + const [intervalHours, setIntervalHours] = useState(4); + const [scheduleStateCode, setScheduleStateCode] = useState(''); + const [availableStates, setAvailableStates] = useState([]); + useEffect(() => { if (isOpen) { fetchStores(); + // Fetch available states for schedule + api.getOrchestratorStates().then(data => { + setAvailableStates(data.states?.map((s: any) => s.state) || []); + }).catch(console.error); } }, [isOpen]); @@ -149,6 +160,40 @@ function CreateTaskModal({ isOpen, onClose, onTaskCreated }: CreateTaskModalProp setError(null); try { + // If making recurring, create a schedule and run it immediately + if (makeRecurring) { + if (!scheduleName.trim()) { + setError('Schedule name is required'); + setLoading(false); + return; + } + + const result = await api.createTaskSchedule({ + name: scheduleName.trim(), + role, + enabled: true, + interval_hours: intervalHours, + priority, + state_code: scheduleStateCode || undefined, + platform: 'dutchie', + }); + + // Run the schedule immediately so tasks appear right away + if (result?.id) { + try { + await api.runTaskScheduleNow(result.id); + } catch (runErr) { + console.warn('Schedule created but failed to run immediately:', runErr); + } + } + + onTaskCreated(); + onClose(); + resetForm(); + return; + } + + // One-time task creation const scheduledDate = scheduleType === 'scheduled' && scheduledFor ? new Date(scheduledFor).toISOString() : undefined; @@ -178,10 +223,7 @@ function CreateTaskModal({ isOpen, onClose, onTaskCreated }: CreateTaskModalProp onTaskCreated(); onClose(); - setSelectedStores([]); - setPriority(10); - setScheduleType('now'); - setScheduledFor(''); + resetForm(); } catch (err: any) { setError(err.response?.data?.error || err.message || 'Failed to create task'); } finally { @@ -189,6 +231,17 @@ function CreateTaskModal({ isOpen, onClose, onTaskCreated }: CreateTaskModalProp } }; + const resetForm = () => { + setSelectedStores([]); + setPriority(10); + setScheduleType('now'); + setScheduledFor(''); + setMakeRecurring(false); + setScheduleName(''); + setIntervalHours(4); + setScheduleStateCode(''); + }; + if (!isOpen) return null; const needsStore = role !== 'store_discovery' && role !== 'analytics_refresh'; @@ -320,39 +373,111 @@ function CreateTaskModal({ isOpen, onClose, onTaskCreated }: CreateTaskModalProp -
- -
- - + {/* One-time scheduling options - only show if not recurring */} + {!makeRecurring && ( +
+ +
+ + +
+ {scheduleType === 'scheduled' && ( +
+ + setScheduledFor(e.target.value)} + className="w-full pl-9 pr-3 py-2 text-sm border border-gray-200 rounded" + /> +
+ )}
- {scheduleType === 'scheduled' && ( -
- - setScheduledFor(e.target.value)} - className="w-full pl-9 pr-3 py-2 text-sm border border-gray-200 rounded" - /> + )} + + {/* Make Recurring Toggle */} +
+ + + {/* Schedule-specific fields */} + {makeRecurring && ( +
+
+ + setScheduleName(e.target.value)} + placeholder="e.g., AZ Product Refresh" + className="w-full px-3 py-2 text-sm border border-gray-200 rounded" + /> +
+ +
+
+ + +
+ +
+ + +
+
+ +

+ This schedule will appear in the Schedules table below where you can edit or delete it. +

)}
@@ -360,7 +485,9 @@ function CreateTaskModal({ isOpen, onClose, onTaskCreated }: CreateTaskModalProp
- {needsStore ? ( + {makeRecurring ? ( + 'Will create a recurring schedule' + ) : needsStore ? ( selectedStores.length > 0 ? `Will create ${selectedStores.length} task${selectedStores.length > 1 ? 's' : ''}` : 'Select stores to create tasks' ) : 'Will create 1 task'}
@@ -370,11 +497,11 @@ function CreateTaskModal({ isOpen, onClose, onTaskCreated }: CreateTaskModalProp
@@ -1176,16 +1303,6 @@ export default function TasksDashboard() { {/* Schedule Actions */}
- {selectedSchedules.size > 0 && (
{schedules.filter(s => s.enabled).length} enabled @@ -1203,7 +1323,7 @@ export default function TasksDashboard() { {schedules.length === 0 ? (
- No schedules configured. Click "New Schedule" to create one. + No schedules configured. Create a task with "Make recurring" enabled to add one.
) : (