jira-ai-fixer/portal/src/pages/Modules.tsx

292 lines
11 KiB
TypeScript

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import {
Plus,
Edit2,
Trash2,
Brain,
Save,
X,
Activity,
BookOpen
} from 'lucide-react'
import { api } from '../lib/api'
interface Module {
name: string
description?: string
program_patterns: string[]
keywords: string[]
rules: string[]
restrictions: string[]
}
export default function Modules() {
const queryClient = useQueryClient()
const [editingModule, setEditingModule] = useState<Module | null>(null)
const [isNew, setIsNew] = useState(false)
const { data: modules, isLoading } = useQuery({
queryKey: ['modules'],
queryFn: () => api.get('/api/config/modules').then(r => r.data),
})
const saveMutation = useMutation({
mutationFn: (module: Module) =>
isNew
? api.post('/api/config/modules', module)
: api.put(`/api/config/modules/${module.name}`, module),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['modules'] })
setEditingModule(null)
setIsNew(false)
},
})
const deleteMutation = useMutation({
mutationFn: (name: string) => api.delete(`/api/config/modules/${name}`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['modules'] })
},
})
const modulesList: Module[] = modules || []
const handleNewModule = () => {
setEditingModule({
name: '',
description: '',
program_patterns: [],
keywords: [],
rules: [],
restrictions: [],
})
setIsNew(true)
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<p className="text-gray-500">
Módulos de regras de negócio para contextualização da IA
</p>
</div>
<button
onClick={handleNewModule}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<Plus className="h-4 w-4" />
Novo Módulo
</button>
</div>
{/* Modules List */}
{isLoading ? (
<div className="flex items-center justify-center h-64">
<Activity className="h-8 w-8 animate-spin text-blue-500" />
</div>
) : modulesList.length === 0 && !editingModule ? (
<div className="bg-white rounded-lg shadow p-12 text-center">
<Brain className="h-12 w-12 mx-auto mb-4 text-gray-300" />
<h3 className="text-lg font-medium text-gray-900">Nenhum módulo configurado</h3>
<p className="text-gray-500 mt-2 max-w-md mx-auto">
Módulos definem regras de negócio específicas que a IA usa para entender o contexto do sistema
</p>
<button
onClick={handleNewModule}
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Criar primeiro módulo
</button>
</div>
) : (
<div className="space-y-4">
{modulesList.map(module => (
<div key={module.name} className="bg-white rounded-lg shadow p-6">
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<Brain className="h-8 w-8 text-purple-500" />
<div>
<h3 className="font-semibold text-lg">{module.name}</h3>
<p className="text-gray-500">{module.description || 'Sem descrição'}</p>
</div>
</div>
<div className="flex gap-2">
<button
onClick={() => { setEditingModule(module); setIsNew(false); }}
className="p-2 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded-lg"
>
<Edit2 className="h-4 w-4" />
</button>
<button
onClick={() => {
if (confirm(`Remover módulo ${module.name}?`)) {
deleteMutation.mutate(module.name)
}
}}
className="p-2 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-lg"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
<div className="mt-4 grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-500">Padrões de programa:</span>
<div className="flex flex-wrap gap-1 mt-1">
{module.program_patterns.map(p => (
<span key={p} className="px-2 py-0.5 bg-blue-100 text-blue-800 rounded text-xs">
{p}
</span>
))}
{module.program_patterns.length === 0 && <span className="text-gray-400">-</span>}
</div>
</div>
<div>
<span className="text-gray-500">Keywords:</span>
<div className="flex flex-wrap gap-1 mt-1">
{module.keywords.slice(0, 5).map(k => (
<span key={k} className="px-2 py-0.5 bg-purple-100 text-purple-800 rounded text-xs">
{k}
</span>
))}
{module.keywords.length > 5 && (
<span className="text-gray-400 text-xs">+{module.keywords.length - 5}</span>
)}
{module.keywords.length === 0 && <span className="text-gray-400">-</span>}
</div>
</div>
</div>
</div>
))}
</div>
)}
{/* Edit/Create Modal */}
{editingModule && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl w-full max-w-2xl max-h-[90vh] overflow-y-auto">
<div className="p-6 border-b border-gray-200 flex items-center justify-between">
<h2 className="text-xl font-semibold">
{isNew ? 'Novo Módulo' : `Editar: ${editingModule.name}`}
</h2>
<button onClick={() => setEditingModule(null)} className="p-2 hover:bg-gray-100 rounded-lg">
<X className="h-5 w-5" />
</button>
</div>
<div className="p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Nome *</label>
<input
type="text"
value={editingModule.name}
onChange={e => setEditingModule({ ...editingModule, name: e.target.value })}
disabled={!isNew}
placeholder="ACQ-Auth"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Descrição</label>
<textarea
value={editingModule.description || ''}
onChange={e => setEditingModule({ ...editingModule, description: e.target.value })}
placeholder="Módulo de autorização de transações..."
rows={2}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Padrões de Programa (um por linha)
</label>
<textarea
value={editingModule.program_patterns.join('\n')}
onChange={e => setEditingModule({
...editingModule,
program_patterns: e.target.value.split('\n').filter(Boolean)
})}
placeholder="AUTH*&#10;ACQAUTH*&#10;*AUTOR*"
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 font-mono text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Keywords (uma por linha)
</label>
<textarea
value={editingModule.keywords.join('\n')}
onChange={e => setEditingModule({
...editingModule,
keywords: e.target.value.split('\n').filter(Boolean)
})}
placeholder="autorização&#10;ISO8583&#10;network&#10;timeout"
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Regras de Negócio (uma por linha)
</label>
<textarea
value={editingModule.rules.join('\n')}
onChange={e => setEditingModule({
...editingModule,
rules: e.target.value.split('\n').filter(Boolean)
})}
placeholder="Sempre validar CVV antes de enviar para rede&#10;Timeout de rede = 30 segundos"
rows={4}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Restrições (uma por linha)
</label>
<textarea
value={editingModule.restrictions.join('\n')}
onChange={e => setEditingModule({
...editingModule,
restrictions: e.target.value.split('\n').filter(Boolean)
})}
placeholder="Não alterar formato de mensagem ISO sem aprovação&#10;Não modificar copybook ISOMSGO1"
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
<div className="p-6 border-t border-gray-200 flex gap-3">
<button
onClick={() => setEditingModule(null)}
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
>
Cancelar
</button>
<button
onClick={() => saveMutation.mutate(editingModule)}
disabled={!editingModule.name || saveMutation.isPending}
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
<Save className="h-4 w-4" />
{saveMutation.isPending ? 'Salvando...' : 'Salvar'}
</button>
</div>
</div>
</div>
)}
</div>
)
}