feat: триггер schedule для автоматизаций — ежедневный запуск по МСК
- планировщик server/workers/automation-scheduler.ts (тик 60 сек, все организации)
- защита от дублей: automations.last_scheduled_run_at (миграция 0065), catch-up в пределах суток
- UI: выбор триггера 'По расписанию' и времени запуска в Automations.tsx
- MCP: create_automation/update_automation принимают trigger 'schedule'
- triggerData: { trigger: 'schedule', scheduledAt }
This commit is contained in:
@@ -38,6 +38,7 @@ const TRIGGER_LABELS: Record<string, string> = {
|
||||
'task.created': 'Задача создана',
|
||||
'task.status_changed': 'Статус изменён',
|
||||
'qr.scan': 'Сканирование QR',
|
||||
schedule: 'По расписанию',
|
||||
};
|
||||
|
||||
const CODE_TEMPLATE = `// ============================================================
|
||||
@@ -54,6 +55,7 @@ const CODE_TEMPLATE = `// ======================================================
|
||||
// • task.created: { formId, taskId, task: { ... } }
|
||||
// • task.status_changed:{ formId, taskId, oldStatusId, newStatusId, task: { ... } }
|
||||
// • qr.scan: { template, objectId, scannedByUserId }
|
||||
// • schedule: { trigger: 'schedule', scheduledAt }
|
||||
//
|
||||
// Логирование:
|
||||
// ctx.log(msg) — запись в лог выполнения
|
||||
@@ -86,6 +88,7 @@ const CODE_TEMPLATE = `// ======================================================
|
||||
// task.created — после создания задачи
|
||||
// task.status_changed — при изменении статуса задачи
|
||||
// qr.scan — при сканировании QR-кода в PWA (только офлайн-автоматизации)
|
||||
// schedule — ежедневно в заданное время по МСК (triggerConfig: { time: 'HH:MM' })
|
||||
//
|
||||
// Результат автоматизации (ctx.result):
|
||||
// { allow: false, error: '...' } — отменить действие (task.before_create)
|
||||
@@ -123,6 +126,7 @@ export function AutomationsContent() {
|
||||
const [formCode, setFormCode] = useState(CODE_TEMPLATE);
|
||||
const [formTrigger, setFormTrigger] = useState('manual');
|
||||
const [formTriggerFormId, setFormTriggerFormId] = useState('');
|
||||
const [formTriggerTime, setFormTriggerTime] = useState('08:00');
|
||||
const [formActive, setFormActive] = useState(true);
|
||||
const [formRunOffline, setFormRunOffline] = useState(false);
|
||||
const [formClientCompatible, setFormClientCompatible] = useState(false);
|
||||
@@ -186,6 +190,7 @@ export function AutomationsContent() {
|
||||
setFormCode(CODE_TEMPLATE);
|
||||
setFormTrigger('manual');
|
||||
setFormTriggerFormId('all');
|
||||
setFormTriggerTime('08:00');
|
||||
setFormActive(true);
|
||||
setFormRunOffline(false);
|
||||
setFormClientCompatible(false);
|
||||
@@ -199,6 +204,7 @@ export function AutomationsContent() {
|
||||
setFormCode(item.code);
|
||||
setFormTrigger(item.trigger);
|
||||
setFormTriggerFormId(String((item.triggerConfig as any)?.formId ?? 'all'));
|
||||
setFormTriggerTime(String((item.triggerConfig as any)?.time ?? '08:00'));
|
||||
setFormActive(item.isActive);
|
||||
setFormRunOffline(item.runOffline ?? false);
|
||||
setFormClientCompatible(item.clientCompatible ?? false);
|
||||
@@ -212,7 +218,9 @@ export function AutomationsContent() {
|
||||
function handleSave() {
|
||||
if (!formName.trim()) return;
|
||||
const triggerConfig: Record<string, unknown> = {};
|
||||
if (formTrigger !== 'manual' && formTriggerFormId && formTriggerFormId !== 'all') {
|
||||
if (formTrigger === 'schedule') {
|
||||
triggerConfig.time = formTriggerTime || '08:00';
|
||||
} else if (formTrigger !== 'manual' && formTriggerFormId && formTriggerFormId !== 'all') {
|
||||
triggerConfig.formId = parseInt(formTriggerFormId);
|
||||
}
|
||||
// For qr.scan trigger, object matching uses triggerConfig.templateKey
|
||||
@@ -282,6 +290,7 @@ export function AutomationsContent() {
|
||||
task.created: { formId, taskId, task: { ... } }
|
||||
task.status_changed:{ formId, taskId, oldStatusId, newStatusId, task: { ... } }
|
||||
qr.scan: { template, objectId, scannedByUserId }
|
||||
schedule: { trigger: 'schedule', scheduledAt }
|
||||
|
||||
Формы:
|
||||
ctx.forms.list() / ctx.forms.get(formId) / ctx.forms.getFields(formId) / ctx.forms.getStatuses(formId)
|
||||
@@ -305,6 +314,7 @@ export function AutomationsContent() {
|
||||
task.created — после создания задачи
|
||||
task.status_changed — при изменении статуса задачи
|
||||
qr.scan — при сканировании QR-кода в PWA
|
||||
schedule — ежедневно в заданное время по МСК (настройка: время запуска)
|
||||
|
||||
Результат автоматизации (ctx.result):
|
||||
{ allow: false, error: '...' } — отменить действие (task.before_create)
|
||||
@@ -444,12 +454,25 @@ export function AutomationsContent() {
|
||||
<SelectItem value="task.created">При создании задачи</SelectItem>
|
||||
<SelectItem value="task.status_changed">При изменении статуса</SelectItem>
|
||||
<SelectItem value="qr.scan">Сканирование QR</SelectItem>
|
||||
<SelectItem value="schedule">По расписанию (ежедневно, МСК)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{formTrigger !== 'manual' && (
|
||||
{formTrigger === 'schedule' && (
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Время запуска (МСК)</Label>
|
||||
<Input
|
||||
type="time"
|
||||
className="h-7 text-xs w-auto"
|
||||
value={formTriggerTime}
|
||||
onChange={e => setFormTriggerTime(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formTrigger !== 'manual' && formTrigger !== 'schedule' && (
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Форма (опционально)</Label>
|
||||
<Select value={formTriggerFormId} onValueChange={setFormTriggerFormId}>
|
||||
|
||||
Reference in New Issue
Block a user