feat(tasks): Add "Make recurring" toggle to Create Task modal

- 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 <noreply@anthropic.com>
This commit is contained in:
Kelly
2025-12-13 02:27:39 -07:00
parent e50f54e621
commit 987ed062d5
2 changed files with 172 additions and 52 deletions

View File

@@ -7,8 +7,8 @@
<title>CannaIQ - Cannabis Menu Intelligence Platform</title> <title>CannaIQ - Cannabis Menu Intelligence Platform</title>
<meta name="description" content="CannaIQ provides real-time cannabis dispensary menu data, product tracking, and analytics for dispensaries across Arizona." /> <meta name="description" content="CannaIQ provides real-time cannabis dispensary menu data, product tracking, and analytics for dispensaries across Arizona." />
<meta name="keywords" content="cannabis, dispensary, menu, products, analytics, Arizona" /> <meta name="keywords" content="cannabis, dispensary, menu, products, analytics, Arizona" />
<script type="module" crossorigin src="/assets/index-onY4oipq.js"></script> <script type="module" crossorigin src="/assets/index-4sr2NZsP.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B0KNyXCG.css"> <link rel="stylesheet" crossorigin href="/assets/index-DcW_XTOx.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -110,9 +110,20 @@ function CreateTaskModal({ isOpen, onClose, onTaskCreated }: CreateTaskModalProp
const [storesLoading, setStoresLoading] = useState(false); const [storesLoading, setStoresLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(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<string[]>([]);
useEffect(() => { useEffect(() => {
if (isOpen) { if (isOpen) {
fetchStores(); fetchStores();
// Fetch available states for schedule
api.getOrchestratorStates().then(data => {
setAvailableStates(data.states?.map((s: any) => s.state) || []);
}).catch(console.error);
} }
}, [isOpen]); }, [isOpen]);
@@ -149,6 +160,40 @@ function CreateTaskModal({ isOpen, onClose, onTaskCreated }: CreateTaskModalProp
setError(null); setError(null);
try { 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 const scheduledDate = scheduleType === 'scheduled' && scheduledFor
? new Date(scheduledFor).toISOString() ? new Date(scheduledFor).toISOString()
: undefined; : undefined;
@@ -178,10 +223,7 @@ function CreateTaskModal({ isOpen, onClose, onTaskCreated }: CreateTaskModalProp
onTaskCreated(); onTaskCreated();
onClose(); onClose();
setSelectedStores([]); resetForm();
setPriority(10);
setScheduleType('now');
setScheduledFor('');
} catch (err: any) { } catch (err: any) {
setError(err.response?.data?.error || err.message || 'Failed to create task'); setError(err.response?.data?.error || err.message || 'Failed to create task');
} finally { } 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; if (!isOpen) return null;
const needsStore = role !== 'store_discovery' && role !== 'analytics_refresh'; const needsStore = role !== 'store_discovery' && role !== 'analytics_refresh';
@@ -320,8 +373,10 @@ function CreateTaskModal({ isOpen, onClose, onTaskCreated }: CreateTaskModalProp
</div> </div>
</div> </div>
{/* One-time scheduling options - only show if not recurring */}
{!makeRecurring && (
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2">Schedule</label> <label className="block text-sm font-medium text-gray-700 mb-2">When to Run</label>
<div className="flex gap-4"> <div className="flex gap-4">
<label className="flex items-center gap-2 cursor-pointer"> <label className="flex items-center gap-2 cursor-pointer">
<input <input
@@ -356,11 +411,83 @@ function CreateTaskModal({ isOpen, onClose, onTaskCreated }: CreateTaskModalProp
</div> </div>
)} )}
</div> </div>
)}
{/* Make Recurring Toggle */}
<div className="border-t border-gray-200 pt-4">
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={makeRecurring}
onChange={(e) => setMakeRecurring(e.target.checked)}
className="w-5 h-5 text-emerald-600 rounded"
/>
<div>
<span className="text-sm font-medium text-gray-900">Make this a recurring schedule</span>
<p className="text-xs text-gray-500">Creates a schedule that runs automatically on an interval</p>
</div>
</label>
{/* Schedule-specific fields */}
{makeRecurring && (
<div className="mt-4 p-4 bg-emerald-50 rounded-lg border border-emerald-200 space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Schedule Name *</label>
<input
type="text"
value={scheduleName}
onChange={(e) => setScheduleName(e.target.value)}
placeholder="e.g., AZ Product Refresh"
className="w-full px-3 py-2 text-sm border border-gray-200 rounded"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Run Every</label>
<select
value={intervalHours}
onChange={(e) => setIntervalHours(parseInt(e.target.value))}
className="w-full px-3 py-2 text-sm border border-gray-200 rounded"
>
<option value={1}>1 hour</option>
<option value={2}>2 hours</option>
<option value={4}>4 hours</option>
<option value={6}>6 hours</option>
<option value={8}>8 hours</option>
<option value={12}>12 hours</option>
<option value={24}>24 hours</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">State Filter</label>
<select
value={scheduleStateCode}
onChange={(e) => setScheduleStateCode(e.target.value)}
className="w-full px-3 py-2 text-sm border border-gray-200 rounded"
>
<option value="">All States</option>
{availableStates.map(state => (
<option key={state} value={state}>{state}</option>
))}
</select>
</div>
</div>
<p className="text-xs text-emerald-700">
This schedule will appear in the Schedules table below where you can edit or delete it.
</p>
</div>
)}
</div>
</div> </div>
<div className="px-6 py-4 border-t border-gray-200 bg-gray-50 flex items-center justify-between"> <div className="px-6 py-4 border-t border-gray-200 bg-gray-50 flex items-center justify-between">
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">
{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' selectedStores.length > 0 ? `Will create ${selectedStores.length} task${selectedStores.length > 1 ? 's' : ''}` : 'Select stores to create tasks'
) : 'Will create 1 task'} ) : 'Will create 1 task'}
</div> </div>
@@ -370,11 +497,11 @@ function CreateTaskModal({ isOpen, onClose, onTaskCreated }: CreateTaskModalProp
</button> </button>
<button <button
onClick={handleSubmit} onClick={handleSubmit}
disabled={loading || (needsStore && selectedStores.length === 0)} disabled={loading || (!makeRecurring && needsStore && selectedStores.length === 0)}
className="px-4 py-2 text-sm bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2" className="px-4 py-2 text-sm bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
> >
{loading && <RefreshCw className="w-4 h-4 animate-spin" />} {loading && <RefreshCw className="w-4 h-4 animate-spin" />}
Create Task{selectedStores.length > 1 ? 's' : ''} {makeRecurring ? 'Create Schedule & Run' : `Create Task${selectedStores.length > 1 ? 's' : ''}`}
</button> </button>
</div> </div>
</div> </div>
@@ -1176,16 +1303,6 @@ export default function TasksDashboard() {
{/* Schedule Actions */} {/* Schedule Actions */}
<div className="p-4 bg-gray-50 border-b border-gray-200 flex flex-wrap items-center justify-between gap-2"> <div className="p-4 bg-gray-50 border-b border-gray-200 flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button
onClick={() => {
setEditingSchedule(null);
setShowScheduleModal(true);
}}
className="flex items-center gap-1 px-3 py-1.5 text-sm bg-emerald-600 text-white rounded hover:bg-emerald-700"
>
<Plus className="w-4 h-4" />
New Schedule
</button>
{selectedSchedules.size > 0 && ( {selectedSchedules.size > 0 && (
<button <button
onClick={handleBulkDeleteSchedules} onClick={handleBulkDeleteSchedules}
@@ -1195,6 +1312,9 @@ export default function TasksDashboard() {
Delete ({selectedSchedules.size}) Delete ({selectedSchedules.size})
</button> </button>
)} )}
<span className="text-xs text-gray-500">
Create a task with "Make recurring" to add a schedule
</span>
</div> </div>
<span className="text-sm text-gray-500"> <span className="text-sm text-gray-500">
{schedules.filter(s => s.enabled).length} enabled {schedules.filter(s => s.enabled).length} enabled
@@ -1203,7 +1323,7 @@ export default function TasksDashboard() {
{schedules.length === 0 ? ( {schedules.length === 0 ? (
<div className="p-8 text-center text-gray-500"> <div className="p-8 text-center text-gray-500">
No schedules configured. Click "New Schedule" to create one. No schedules configured. Create a task with "Make recurring" enabled to add one.
</div> </div>
) : ( ) : (
<div className="overflow-x-auto"> <div className="overflow-x-auto">