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>
|
<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>
|
||||||
|
|||||||
@@ -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,39 +373,111 @@ function CreateTaskModal({ isOpen, onClose, onTaskCreated }: CreateTaskModalProp
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{/* One-time scheduling options - only show if not recurring */}
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">Schedule</label>
|
{!makeRecurring && (
|
||||||
<div className="flex gap-4">
|
<div>
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
<label className="block text-sm font-medium text-gray-700 mb-2">When to Run</label>
|
||||||
<input
|
<div className="flex gap-4">
|
||||||
type="radio"
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
name="schedule"
|
<input
|
||||||
checked={scheduleType === 'now'}
|
type="radio"
|
||||||
onChange={() => setScheduleType('now')}
|
name="schedule"
|
||||||
className="w-4 h-4 text-emerald-600"
|
checked={scheduleType === 'now'}
|
||||||
/>
|
onChange={() => setScheduleType('now')}
|
||||||
<span className="text-sm text-gray-700">Run immediately</span>
|
className="w-4 h-4 text-emerald-600"
|
||||||
</label>
|
/>
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
<span className="text-sm text-gray-700">Run immediately</span>
|
||||||
<input
|
</label>
|
||||||
type="radio"
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
name="schedule"
|
<input
|
||||||
checked={scheduleType === 'scheduled'}
|
type="radio"
|
||||||
onChange={() => setScheduleType('scheduled')}
|
name="schedule"
|
||||||
className="w-4 h-4 text-emerald-600"
|
checked={scheduleType === 'scheduled'}
|
||||||
/>
|
onChange={() => setScheduleType('scheduled')}
|
||||||
<span className="text-sm text-gray-700">Schedule for later</span>
|
className="w-4 h-4 text-emerald-600"
|
||||||
</label>
|
/>
|
||||||
|
<span className="text-sm text-gray-700">Schedule for later</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{scheduleType === 'scheduled' && (
|
||||||
|
<div className="mt-3 relative">
|
||||||
|
<Calendar className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
value={scheduledFor}
|
||||||
|
onChange={(e) => setScheduledFor(e.target.value)}
|
||||||
|
className="w-full pl-9 pr-3 py-2 text-sm border border-gray-200 rounded"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{scheduleType === 'scheduled' && (
|
)}
|
||||||
<div className="mt-3 relative">
|
|
||||||
<Calendar className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
{/* Make Recurring Toggle */}
|
||||||
<input
|
<div className="border-t border-gray-200 pt-4">
|
||||||
type="datetime-local"
|
<label className="flex items-center gap-3 cursor-pointer">
|
||||||
value={scheduledFor}
|
<input
|
||||||
onChange={(e) => setScheduledFor(e.target.value)}
|
type="checkbox"
|
||||||
className="w-full pl-9 pr-3 py-2 text-sm border border-gray-200 rounded"
|
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>
|
||||||
@@ -360,7 +485,9 @@ function CreateTaskModal({ isOpen, onClose, onTaskCreated }: CreateTaskModalProp
|
|||||||
|
|
||||||
<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">
|
||||||
|
|||||||
Reference in New Issue
Block a user