import { useState } from 'react'; import { useQuery, useMutation } from '@tanstack/react-query'; import { queryClient, apiRequest } from '@/lib/queryClient'; import { useAuth } from '@/hooks/useAuth'; import { hasPermission } from '@/lib/permissions'; import { useToast } from '@/hooks/use-toast'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Badge } from '@/components/ui/badge'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'; import { Switch } from '@/components/ui/switch'; import { Label } from '@/components/ui/label'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Zap, Plus, Pencil, Trash2, Play, ChevronRight, Circle, X, Terminal } from 'lucide-react'; type Automation = { id: number; name: string; description: string | null; code: string; trigger: string; triggerConfig: Record | null; isActive: boolean; runOffline: boolean; clientCompatible: boolean; createdAt: string | null; updatedAt: string | null; }; type Form = { id: number; name: string }; const TRIGGER_LABELS: Record = { manual: 'Вручную', 'task.before_create': 'Перед созданием задачи', 'task.created': 'Задача создана', 'task.status_changed': 'Статус изменён', 'qr.scan': 'Сканирование QR', schedule: 'По расписанию', }; const CODE_TEMPLATE = `// ============================================================ // СПРАВОЧНИК ПО АВТОМАТИЗАЦИЯМ // ============================================================ // // Переменные контекста: // ctx.organizationId — ID текущей организации // ctx.triggerData — данные события (только для triggered-автоматизаций) // ctx.result — объект результата, который увидит CRM // // Данные события ctx.triggerData: // • task.before_create: { formId, task: { formId, title, currentStatusId, dueDate, customFields } } // • 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) — запись в лог выполнения // // Работа с формами: // ctx.forms.list() — список форм // ctx.forms.get(formId) — получить форму // ctx.forms.getFields(formId) — поля формы // ctx.forms.getStatuses(formId) — статусы формы // // Работа с задачами: // ctx.tasks.list(formId) — задачи формы // ctx.tasks.get(taskId) — получить задачу // ctx.tasks.create({ formId, title, assignedTo?, dueDate? }) — создать задачу // ctx.tasks.update(taskId, updates) — обновить задачу // ctx.tasks.delete(taskId) — удалить задачу // ctx.tasks.getFieldValues(taskId) — значения полей задачи // ctx.tasks.getAssignees(taskId) — назначенные пользователи // ctx.tasks.setFieldValue(taskId, fieldCode, value) — установить значение поля // // Работа с пользователями: // ctx.users.list() — список пользователей организации // ctx.users.get(id) — получить пользователя // ctx.users.update(id, updates) — обновить пользователя (поля firstName, lastName, position, statusId и др.) // ctx.users.updateStatus(id, statusId) — сменить статус пользователя // ctx.users.setFieldValue(id, fieldCodeOrId, value) — записать значение кастомного поля профиля // пользователя (по id или code поля, с аудитом от имени «Автоматизация»). Для полей типа // «history-number» значение дополнительно добавляется в историю user_field_history // (с привязкой к задаче триггера); нечисловое значение вызовет ошибку автоматизации. // // Триггеры: // manual — запуск вручную // task.before_create — перед созданием задачи (можно отменить через ctx.result = { allow: false, error: '...' }) // task.created — после создания задачи // task.status_changed — при изменении статуса задачи // (triggerConfig: { formId?, statusId? }; statusId — срабатывать только при переходе В этот статус) // qr.scan — при сканировании QR-кода в PWA (только офлайн-автоматизации) // schedule — ежедневно в заданное время по МСК (triggerConfig: { time: 'HH:MM' }) // // Результат автоматизации (ctx.result): // { allow: false, error: '...' } — отменить действие (task.before_create) // { action: 'open_task', taskId: 123 } — открыть задачу после скана QR // { action: 'show_list', tasks: [{ id, title }] } — показать список задач // { action: 'create_task', formId: 11, prefill: {} } — открыть диалог создания задачи // { action: 'error', message: '...' } — показать ошибку // // Действия для офлайн-автоматизаций (qr.scan, PWA): // ctx.result.actions.push({ type: 'notify', title?, message, variant? }) — показать уведомление // ctx.result.actions.push({ type: 'navigate', path: '/forms/...' }) — перейти по маршруту // ctx.result.actions.push({ type: 'updateFormCache', taskId, values: { fieldCode: value } }) — обновить поля задачи офлайн // ctx.result.actions.push({ type: 'createOfflineTask', formId, title?, values: { fieldCode: value } }) — создать задачу офлайн // // Пример: подсчёт задач по всем формам const forms = await ctx.forms.list(); ctx.log('Форм в системе: ' + forms.length); for (const form of forms) { const tasks = await ctx.tasks.list(form.id); ctx.log(\`Форма "\${form.name}": \${tasks.length} задач\`); } // Пример для триггера task.status_changed — начислить рейтинг исполнителю // при переходе задачи в нужный статус (statusId задаётся в настройках триггера): // const { taskId, oldStatusId, newStatusId, task } = ctx.triggerData; // ctx.log('Задача ' + taskId + ': статус ' + oldStatusId + ' -> ' + newStatusId); // if (task.assignedTo) { // await ctx.users.setFieldValue(task.assignedTo, 'rating', 5); // } `; export function AutomationsContent() { const { toast } = useToast(); const { user } = useAuth(); const [editItem, setEditItem] = useState(null); const [isCreating, setIsCreating] = useState(false); const [runLogs, setRunLogs] = useState<{ id: number; logs: string[]; error?: string } | null>(null); const [isRunning, setIsRunning] = useState(false); const [formName, setFormName] = useState(''); const [formDesc, setFormDesc] = useState(''); 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); const { data, isLoading } = useQuery<{ success: boolean; automations: Automation[] }>({ queryKey: ['/api/automations'], enabled: hasPermission(user, 'automations.manage'), }); const { data: formsData } = useQuery<{ success: boolean; forms: Form[] }>({ queryKey: ['/api/forms'], enabled: hasPermission(user, 'automations.manage'), }); const items = data?.automations ?? []; const forms = formsData?.forms ?? []; const saveMutation = useMutation({ mutationFn: async (payload: Partial & { name: string }) => { if (editItem && !isCreating) { const res = await apiRequest('PUT', `/api/automations/${editItem.id}`, payload); return res.json(); } else { const res = await apiRequest('POST', '/api/automations', payload); return res.json(); } }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['/api/automations'] }); closeDialog(); toast({ title: 'Сохранено' }); }, onError: () => toast({ title: 'Ошибка сохранения', variant: 'destructive' }), }); const deleteMutation = useMutation({ mutationFn: async (id: number) => { const res = await apiRequest('DELETE', `/api/automations/${id}`); return res.json(); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['/api/automations'] }); toast({ title: 'Удалено' }); }, onError: () => toast({ title: 'Ошибка удаления', variant: 'destructive' }), }); const toggleMutation = useMutation({ mutationFn: async ({ id, isActive }: { id: number; isActive: boolean }) => { const res = await apiRequest('PUT', `/api/automations/${id}`, { isActive }); return res.json(); }, onSuccess: () => queryClient.invalidateQueries({ queryKey: ['/api/automations'] }), }); function openCreate() { setIsCreating(true); setEditItem(null); setFormName(''); setFormDesc(''); setFormCode(CODE_TEMPLATE); setFormTrigger('manual'); setFormTriggerFormId('all'); setFormTriggerTime('08:00'); setFormActive(true); setFormRunOffline(false); setFormClientCompatible(false); } function openEdit(item: Automation) { setIsCreating(false); setEditItem(item); setFormName(item.name); setFormDesc(item.description ?? ''); 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); } function closeDialog() { setEditItem(null); setIsCreating(false); } function handleSave() { if (!formName.trim()) return; const triggerConfig: Record = {}; 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 if (formTrigger === 'qr.scan') { triggerConfig.templateKey = (editItem?.triggerConfig as any)?.templateKey ?? formName.trim().toLowerCase().replace(/\s+/g, '-'); if (formTriggerFormId && formTriggerFormId !== 'all') { triggerConfig.formId = parseInt(formTriggerFormId); } } saveMutation.mutate({ name: formName.trim(), description: formDesc || null, code: formCode, trigger: formTrigger, triggerConfig: Object.keys(triggerConfig).length ? triggerConfig : null, isActive: formActive, runOffline: formRunOffline, clientCompatible: formClientCompatible, } as any); } async function handleRun(item: Automation) { setIsRunning(true); setRunLogs({ id: item.id, logs: ['⏳ Запуск...'], error: undefined }); try { const res = await apiRequest('POST', `/api/automations/${item.id}/run`, {}); const data = await res.json(); setRunLogs({ id: item.id, logs: data.logs ?? [], error: data.error }); if (data.success) { toast({ title: 'Выполнено успешно' }); } else { toast({ title: 'Ошибка выполнения', variant: 'destructive' }); } } catch { setRunLogs({ id: item.id, logs: ['Ошибка сети'], error: 'Network error' }); } finally { setIsRunning(false); } } const isDialogOpen = isCreating || editItem !== null; return (

JavaScript-скрипты для автоматизации задач. ИИ может создавать и редактировать их через MCP.

Доступные функции и переменные
{`Переменные: ctx.organizationId — ID организации ctx.triggerData — данные события (для triggered-автоматизаций) ctx.result — результат автоматизации События ctx.triggerData: task.before_create: { formId, task: { formId, title, currentStatusId, dueDate, customFields } } 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) Задачи: ctx.tasks.list(formId) / ctx.tasks.get(taskId) ctx.tasks.create({ formId, title, assignedTo?, dueDate? }) ctx.tasks.update(taskId, updates) / ctx.tasks.delete(taskId) ctx.tasks.getFieldValues(taskId) / ctx.tasks.getAssignees(taskId) ctx.tasks.setFieldValue(taskId, fieldCode, value) Пользователи: ctx.users.list() / ctx.users.get(userId) ctx.users.update(userId, updates) / ctx.users.updateStatus(userId, statusId) ctx.users.setFieldValue(userId, fieldCodeOrId, value) — записать кастомное поле профиля (с аудитом; для полей «history-number» значение также пишется в историю user_field_history, нечисловое значение — ошибка автоматизации) Логирование: ctx.log(msg) Триггеры: manual — запуск вручную task.before_create — перед созданием задачи (можно отменить через ctx.result = { allow: false, error: '...' }) task.created — после создания задачи task.status_changed — при изменении статуса задачи (triggerConfig: { formId?, statusId? }; statusId — срабатывать только при переходе В этот статус) qr.scan — при сканировании QR-кода в PWA schedule — ежедневно в заданное время по МСК (настройка: время запуска) Результат автоматизации (ctx.result): { allow: false, error: '...' } — отменить действие (task.before_create) { action: 'open_task', taskId: 123 } — открыть задачу { action: 'show_list', tasks: [{ id, title }] } — показать список задач { action: 'create_task', formId: 11, prefill: {} } — открыть диалог создания { action: 'error', message: '...' } — показать ошибку Блокировка создания (task.before_create): ctx.result = { allow: false, error: 'Пояснение...' }`}
{isLoading ? (
Загрузка...
) : items.length === 0 ? (
Нет автоматизаций. Создайте первую или попросите ИИ написать скрипт через MCP.
) : (
{items.map(item => (
toggleMutation.mutate({ id: item.id, isActive: v })} className="scale-75" />
{item.name} {TRIGGER_LABELS[item.trigger] ?? item.trigger} {!item.isActive && ( Отключена )} {item.runOffline && ( Offline )}
{item.description && (
{item.description}
)}
{runLogs?.id === item.id && (
Лог выполнения
                      {runLogs.logs.join('\n') || '(нет вывода)'}
                      {runLogs.error && `\n\n❌ ${runLogs.error}`}
                    
)}
))}
)} { if (!v) closeDialog(); }}> {isCreating ? 'Новая автоматизация' : `Редактировать: ${editItem?.name}`} {isCreating ? 'Создание новой автоматизации' : `Редактирование автоматизации ${editItem?.name}`}
setFormName(e.target.value)} />
{formTrigger === 'schedule' && (
setFormTriggerTime(e.target.value)} />
)} {formTrigger !== 'manual' && formTrigger !== 'schedule' && (
)}