'use client'; import { useState, useEffect } from 'react'; import Link from 'next/link'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Switch } from '@/components/ui/switch'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { ArrowLeft, ListFilter, Plus, Pencil, Trash2, Loader2, } from 'lucide-react'; import { toast } from 'sonner'; import { WorkflowRuleWithDetails, ConditionOperator } from '@/lib/types/workflow'; const CONDITION_FIELDS = [ 'person_id', 'creator_resource_id', 'ticket_category', 'company_id', 'title', 'description', 'priority', 'ticket_type', 'status', 'queue_id', ]; const CONDITION_OPERATORS: { value: ConditionOperator; label: string }[] = [ { value: 'equals', label: 'Equals' }, { value: 'not_equals', label: 'Not Equals' }, { value: 'in', label: 'In List' }, { value: 'not_in', label: 'Not In List' }, { value: 'contains', label: 'Contains' }, { value: 'not_contains', label: 'Not Contains' }, { value: 'is_null', label: 'Is Null' }, { value: 'is_not_null', label: 'Is Not Null' }, ]; export default function WorkflowRulesPage() { const [rules, setRules] = useState([]); const [isLoading, setIsLoading] = useState(true); const [editingRule, setEditingRule] = useState(null); const [isSaving, setIsSaving] = useState(false); useEffect(() => { loadRules(); }, []); const loadRules = async () => { setIsLoading(true); try { const res = await fetch('/api/workflow/rules'); if (res.ok) setRules(await res.json()); } catch { toast.error('Failed to load rules'); } finally { setIsLoading(false); } }; const handleCreate = () => { setEditingRule({ name: '', description: '', trigger_event: 'ticket.created', is_active: true, sort_order: 0, stop_processing: true, conditions: [{ condition_group: 0, field: 'ticket_category', operator: 'in', value: [] }], actions: [{ sort_order: 0, action_type: 'skip', config: { reason: '' } }], }); }; const handleEdit = (rule: WorkflowRuleWithDetails) => { setEditingRule({ ...rule, conditions: rule.conditions.map(c => ({ ...c, value: c.value })), actions: rule.actions.map(a => ({ ...a })), }); }; const handleSave = async () => { if (!editingRule) return; setIsSaving(true); try { const isUpdate = editingRule.id; const url = isUpdate ? `/api/workflow/rules/${editingRule.id}` : '/api/workflow/rules'; const res = await fetch(url, { method: isUpdate ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(editingRule), }); if (res.ok) { toast.success(isUpdate ? 'Rule updated' : 'Rule created'); setEditingRule(null); loadRules(); } else { toast.error('Failed to save rule'); } } catch { toast.error('Failed to save rule'); } finally { setIsSaving(false); } }; const handleDelete = async (id: number) => { if (!confirm('Delete this workflow rule and all its conditions?')) return; try { const res = await fetch(`/api/workflow/rules/${id}`, { method: 'DELETE' }); if (res.ok) { toast.success('Rule deleted'); loadRules(); } } catch { toast.error('Failed to delete rule'); } }; const handleToggle = async (rule: WorkflowRuleWithDetails) => { try { await fetch(`/api/workflow/rules/${rule.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ is_active: !rule.is_active }), }); loadRules(); } catch { toast.error('Failed to toggle rule'); } }; const summarizeConditions = (rule: WorkflowRuleWithDetails): string => { return rule.conditions.map(c => `${c.field} ${c.operator} ${JSON.stringify(c.value)}`).join(' AND '); }; return (

Filter Rules

Exclusion/inclusion filters for ticket processing

Active Filter Rules Rules are evaluated in sort order. Matching a "skip" action stops processing. {isLoading ? (
) : rules.length === 0 ? (

No filter rules configured.

) : ( Order Name Trigger Conditions Action Active Actions {rules.map((rule) => ( {rule.sort_order} {rule.name} {rule.trigger_event}
{summarizeConditions(rule)}
{rule.actions.map(a => ( {a.action_type} ))} handleToggle(rule)} />
))}
)}
{/* Edit Dialog */} !open && setEditingRule(null)}> {editingRule?.id ? 'Edit' : 'New'} Filter Rule Configure conditions that determine which tickets to process or skip {editingRule && (
setEditingRule({ ...editingRule, name: e.target.value })} />
setEditingRule({ ...editingRule, sort_order: parseInt(e.target.value) })} />
setEditingRule({ ...editingRule, description: e.target.value })} />
setEditingRule({ ...editingRule, stop_processing: v })} />
{/* Conditions */}

Conditions

{editingRule.conditions.map((cond: any, i: number) => (
{ const updated = [...editingRule.conditions]; let val: any = e.target.value; try { val = JSON.parse(val); } catch {} updated[i] = { ...updated[i], value: val }; setEditingRule({ ...editingRule, conditions: updated }); }} placeholder="value or [1,2,3]" />
))}
{/* Action */}

Action

{ const actions = [{ ...editingRule.actions[0], config: { reason: e.target.value } }]; setEditingRule({ ...editingRule, actions }); }} placeholder="Skip reason" />
)}
); }