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:
4
cannaiq/dist/index.html
vendored
4
cannaiq/dist/index.html
vendored
@@ -7,8 +7,8 @@
|
||||
<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="keywords" content="cannabis, dispensary, menu, products, analytics, Arizona" />
|
||||
<script type="module" crossorigin src="/assets/index-onY4oipq.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-B0KNyXCG.css">
|
||||
<script type="module" crossorigin src="/assets/index-4sr2NZsP.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DcW_XTOx.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -110,9 +110,20 @@ function CreateTaskModal({ isOpen, onClose, onTaskCreated }: CreateTaskModalProp
|
||||
const [storesLoading, setStoresLoading] = useState(false);
|
||||
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(() => {
|
||||
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,8 +373,10 @@ function CreateTaskModal({ isOpen, onClose, onTaskCreated }: CreateTaskModalProp
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* One-time scheduling options - only show if not recurring */}
|
||||
{!makeRecurring && (
|
||||
<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">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
@@ -356,11 +411,83 @@ function CreateTaskModal({ isOpen, onClose, onTaskCreated }: CreateTaskModalProp
|
||||
</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 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">
|
||||
{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'}
|
||||
</div>
|
||||
@@ -370,11 +497,11 @@ function CreateTaskModal({ isOpen, onClose, onTaskCreated }: CreateTaskModalProp
|
||||
</button>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
{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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1176,16 +1303,6 @@ export default function TasksDashboard() {
|
||||
{/* 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="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 && (
|
||||
<button
|
||||
onClick={handleBulkDeleteSchedules}
|
||||
@@ -1195,6 +1312,9 @@ export default function TasksDashboard() {
|
||||
Delete ({selectedSchedules.size})
|
||||
</button>
|
||||
)}
|
||||
<span className="text-xs text-gray-500">
|
||||
Create a task with "Make recurring" to add a schedule
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500">
|
||||
{schedules.filter(s => s.enabled).length} enabled
|
||||
@@ -1203,7 +1323,7 @@ export default function TasksDashboard() {
|
||||
|
||||
{schedules.length === 0 ? (
|
||||
<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 className="overflow-x-auto">
|
||||
|
||||
Reference in New Issue
Block a user