/** * Organizations page */ import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { getOrganizations, createOrganization, updateOrganization, deleteOrganization, scanOrganization, } from '../api/organizations'; import { Organization, OrganizationCreate, OrganizationPlatform } from '../types/organization'; import { Modal, ConfirmModal } from '../components/Modal'; export default function Organizations() { const queryClient = useQueryClient(); const [isFormOpen, setIsFormOpen] = useState(false); const [editingOrg, setEditingOrg] = useState(null); // Modal states const [modalMessage, setModalMessage] = useState(''); const [showModal, setShowModal] = useState(false); const [confirmAction, setConfirmAction] = useState<(() => void) | null>(null); const [confirmMessage, setConfirmMessage] = useState(''); const [showConfirm, setShowConfirm] = useState(false); const { data, isLoading } = useQuery({ queryKey: ['organizations'], queryFn: () => getOrganizations(), }); const createMutation = useMutation({ mutationFn: createOrganization, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['organizations'] }); setIsFormOpen(false); setModalMessage('✅ Организация успешно добавлена'); setShowModal(true); }, onError: (error: Error) => { setModalMessage(`❌ Ошибка: ${error.message}`); setShowModal(true); }, }); const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: any }) => updateOrganization(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['organizations'] }); setEditingOrg(null); setModalMessage('✅ Организация успешно обновлена'); setShowModal(true); }, onError: (error: Error) => { setModalMessage(`❌ Ошибка: ${error.message}`); setShowModal(true); }, }); const deleteMutation = useMutation({ mutationFn: deleteOrganization, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['organizations'] }); setModalMessage('✅ Организация успешно удалена'); setShowModal(true); }, onError: (error: Error) => { setModalMessage(`❌ Ошибка: ${error.message}`); setShowModal(true); }, }); const scanMutation = useMutation({ mutationFn: scanOrganization, onSuccess: (result) => { queryClient.invalidateQueries({ queryKey: ['organizations'] }); queryClient.invalidateQueries({ queryKey: ['tasks'] }); let message = `✅ Сканирование завершено!\n\n`; message += `📦 Репозиториев найдено: ${result.repositories_found}\n`; message += `➕ Репозиториев добавлено: ${result.repositories_added}\n`; message += `🔀 PR найдено: ${result.pull_requests_found}\n`; message += `📝 Задач создано: ${result.tasks_created}`; if (result.errors.length > 0) { message += `\n\n⚠️ Ошибки:\n${result.errors.join('\n')}`; } setModalMessage(message); setShowModal(true); }, onError: (error: Error) => { setModalMessage(`❌ Ошибка сканирования: ${error.message}`); setShowModal(true); }, }); const handleDelete = (org: Organization) => { setConfirmMessage(`Вы уверены, что хотите удалить организацию "${org.name}"?`); setConfirmAction(() => () => deleteMutation.mutate(org.id)); setShowConfirm(true); }; const handleScan = (org: Organization) => { setConfirmMessage(`Начать сканирование организации "${org.name}"?\n\nБудут найдены все репозитории и PR.`); setConfirmAction(() => () => scanMutation.mutate(org.id)); setShowConfirm(true); }; if (isLoading) { return (
Загрузка...
); } return (
{/* Header */}

Организации

Управление организациями и автоматическое сканирование репозиториев

{/* Organizations list */}
{data?.items.map((org) => (

{org.name}

{org.is_active ? 'Активна' : 'Неактивна'} {org.platform.toUpperCase()}
🌐 {org.base_url}
{org.last_scan_at && (
🔍 Последнее сканирование:{' '} {new Date(org.last_scan_at).toLocaleString('ru-RU')}
)}
Webhook: {org.webhook_url}
))}
{data?.items.length === 0 && (

Нет организаций

)} {/* Create Form Modal */} {isFormOpen && ( createMutation.mutate(data)} onCancel={() => setIsFormOpen(false)} isSubmitting={createMutation.isPending} /> )} {/* Edit Form Modal */} {editingOrg && ( updateMutation.mutate({ id: editingOrg.id, data })} onCancel={() => setEditingOrg(null)} isSubmitting={updateMutation.isPending} /> )} {/* Modals */} setShowModal(false)} title={modalMessage.includes('❌') ? 'Ошибка' : modalMessage.includes('✅') ? 'Успешно' : 'Уведомление'} type={modalMessage.includes('❌') ? 'error' : modalMessage.includes('✅') ? 'success' : 'info'} >

{modalMessage}

setShowConfirm(false)} onConfirm={() => { if (confirmAction) confirmAction(); setShowConfirm(false); }} title="Подтверждение" message={confirmMessage} />
); } // Organization Form Component function OrganizationForm({ organization, onSubmit, onCancel, isSubmitting, }: { organization?: Organization; onSubmit: (data: OrganizationCreate) => void; onCancel: () => void; isSubmitting: boolean; }) { const [formData, setFormData] = useState({ name: organization?.name || '', platform: (organization?.platform as OrganizationPlatform) || 'gitea', base_url: organization?.base_url || '', api_token: '', webhook_secret: '', }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); onSubmit(formData); }; return (

{organization ? 'Редактировать организацию' : 'Новая организация'}

setFormData({ ...formData, name: e.target.value })} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" placeholder="inno-js" />
setFormData({ ...formData, base_url: e.target.value })} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" placeholder="https://git.example.com" />
setFormData({ ...formData, api_token: e.target.value })} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" placeholder="Опционально (используется master токен если не указан)" />

💡 Если не указан, будет использован master токен из конфигурации сервера

setFormData({ ...formData, webhook_secret: e.target.value })} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" placeholder="Опционально (генерируется автоматически)" />
); }