import React, { useState, useEffect } from 'react'; import { authFetch } from '../../services/apiClient'; import { Calendar, Clock, Users, MapPin, Plus, CheckCircle2, AlertCircle, X, Edit, Save, Play, Square, UserPlus, Calendar as CalendarIcon, FileText, MessageSquare, Building2 } from 'lucide-react'; import { Meeting, MeetingRoom, MeetingBooking } from '../../types'; import { CURRENT_USER_MOCK } from '../../constants'; export const MeetingsAndRooms: React.FC = () => { const [meetings, setMeetings] = useState([]); const [rooms, setRooms] = useState([]); const [bookings, setBookings] = useState([]); const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]); const [showCreateModal, setShowCreateModal] = useState(false); const [showCreateRoomModal, setShowCreateRoomModal] = useState(false); const [editingRoom, setEditingRoom] = useState(null); const [showRescheduleModal, setShowRescheduleModal] = useState(false); const [selectedMeeting, setSelectedMeeting] = useState(null); const [showEditModal, setShowEditModal] = useState(false); const [rescheduleData, setRescheduleData] = useState({ date: '', startTime: '', endTime: '' }); const [roomFormData, setRoomFormData] = useState({ name: '', capacity: 10, location: '', description: '', equipment: [] as string[], isActive: true }); const [showMeetingCard, setShowMeetingCard] = useState(false); const [meetingCardData, setMeetingCardData] = useState({ notes: '', conclusions: '', questions: '', theses: '' }); const [employees, setEmployees] = useState>([]); const [formData, setFormData] = useState({ title: '', description: '', roomId: '', startTime: '', endTime: '', participantIds: [] as string[], agenda: '' }); const [editData, setEditData] = useState({ notes: '', conclusions: '' }); useEffect(() => { fetchMeetings(); fetchRooms(); fetchBookings(); }, [selectedDate]); useEffect(() => { const fetchEmployees = async () => { try { const res = await authFetch('/api/employees/list'); if (res.ok) { const data = await res.json(); setEmployees(Array.isArray(data) ? data : []); } } catch (e) { console.error('Ошибка загрузки списка сотрудников:', e); } }; fetchEmployees(); }, []); const fetchMeetings = async () => { try { const response = await authFetch(`/api/office/meetings?startDate=${selectedDate}&endDate=${selectedDate}`); if (response.ok) { const data = await response.json(); // Нормализуем данные из API const normalizedData = data.map((meeting: any) => ({ id: meeting.id, title: meeting.title || '', description: meeting.description, organizer: meeting.organizer || '', startTime: meeting.start_time || meeting.startTime, endTime: meeting.end_time || meeting.endTime, roomId: meeting.room_id || meeting.roomId, room: meeting.room_name ? { id: meeting.room_id, name: meeting.room_name, capacity: meeting.room_capacity } : meeting.room, participants: Array.isArray(meeting.participants) ? meeting.participants : (typeof meeting.participants === 'string' ? JSON.parse(meeting.participants || '[]') : []), agenda: meeting.agenda, notes: meeting.notes || null, conclusions: meeting.conclusions || null, status: meeting.status || 'scheduled', reminderSent: meeting.reminder_sent || meeting.reminderSent || false, reminderTime: meeting.reminder_time || meeting.reminderTime, attachments: Array.isArray(meeting.attachments) ? meeting.attachments : (typeof meeting.attachments === 'string' ? JSON.parse(meeting.attachments || '[]') : []), createdAt: meeting.created_at || meeting.createdAt, updatedAt: meeting.updated_at || meeting.updatedAt })); setMeetings(normalizedData); } } catch (error) { console.error('Ошибка загрузки совещаний:', error); } }; const fetchRooms = async () => { try { const response = await authFetch('/api/office/meeting-rooms?all=true'); if (response.ok) { const data = await response.json(); // Нормализуем данные из API const normalizedData = data.map((room: any) => ({ id: room.id, name: room.name || '', capacity: room.capacity || 0, location: room.location, equipment: Array.isArray(room.equipment) ? room.equipment : (typeof room.equipment === 'string' ? JSON.parse(room.equipment || '[]') : []), description: room.description, isActive: room.is_active !== undefined ? room.is_active : (room.isActive !== undefined ? room.isActive : true), createdAt: room.created_at || room.createdAt, updatedAt: room.updated_at || room.updatedAt })); setRooms(normalizedData); } } catch (error) { console.error('Ошибка загрузки переговорных:', error); } }; const handleCreateMeeting = async () => { try { if (!formData.title || !formData.title.trim()) { alert('Пожалуйста, укажите название совещания'); return; } if (!formData.startTime) { alert('Пожалуйста, укажите время начала'); return; } if (!formData.endTime) { alert('Пожалуйста, укажите время окончания'); return; } const startDateTime = new Date(`${selectedDate}T${formData.startTime}`); const endDateTime = new Date(`${selectedDate}T${formData.endTime}`); if (endDateTime <= startDateTime) { alert('Время окончания должно быть позже времени начала'); return; } const participantsArray = formData.participantIds .map(id => employees.find(e => e.id === id)?.name) .filter((n): n is string => !!n); const response = await authFetch('/api/office/meetings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: formData.title, description: formData.description || null, organizer: CURRENT_USER_MOCK.name, startTime: startDateTime.toISOString(), endTime: endDateTime.toISOString(), roomId: formData.roomId ? parseInt(formData.roomId) : null, participants: participantsArray, agenda: formData.agenda || null }) }); if (response.ok) { const newMeeting = await response.json(); // Нормализуем данные const normalizedMeeting = { id: newMeeting.id, title: newMeeting.title || formData.title, description: newMeeting.description || formData.description, organizer: newMeeting.organizer || CURRENT_USER_MOCK.name, startTime: newMeeting.start_time || startDateTime.toISOString(), endTime: newMeeting.end_time || endDateTime.toISOString(), roomId: newMeeting.room_id || (formData.roomId ? parseInt(formData.roomId) : null), room: rooms.find(r => r.id === (formData.roomId ? parseInt(formData.roomId) : null)), participants: participantsArray, agenda: newMeeting.agenda || formData.agenda, notes: newMeeting.notes, conclusions: newMeeting.conclusions, status: newMeeting.status || 'scheduled', reminderSent: false, attachments: [], createdAt: newMeeting.created_at, updatedAt: newMeeting.updated_at }; // Устанавливаем выбранную дату на дату созданного совещания, чтобы оно отобразилось const meetingDate = new Date(normalizedMeeting.startTime).toISOString().split('T')[0]; setSelectedDate(meetingDate); // Обновляем список (useEffect автоматически вызовет fetchMeetings при изменении selectedDate) setShowCreateModal(false); setFormData({ title: '', description: '', roomId: '', startTime: '', endTime: '', participantIds: [], agenda: '' }); } else { const errorData = await response.json().catch(() => ({ error: 'Неизвестная ошибка' })); alert(`Ошибка создания совещания: ${errorData.error || 'Неизвестная ошибка'}`); } } catch (error: any) { console.error('Ошибка создания совещания:', error); alert(`Ошибка создания совещания: ${error.message || 'Неизвестная ошибка'}`); } }; const handleOpenEditModal = (meeting: Meeting) => { setSelectedMeeting(meeting); setEditData({ notes: meeting.notes || '', conclusions: meeting.conclusions || '' }); setShowEditModal(true); }; const handleSaveMeeting = async () => { if (!selectedMeeting) return; try { const response = await authFetch(`/api/office/meetings/${selectedMeeting.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ notes: editData.notes, conclusions: editData.conclusions }) }); if (response.ok) { const updatedMeeting = await response.json(); // Обновляем в списке setMeetings(meetings.map(m => m.id === selectedMeeting.id ? { ...m, notes: updatedMeeting.notes || editData.notes, conclusions: updatedMeeting.conclusions || editData.conclusions } : m )); setShowEditModal(false); setSelectedMeeting(null); fetchMeetings(); // Обновляем список } else { const errorData = await response.json().catch(() => ({ error: 'Неизвестная ошибка' })); alert(`Ошибка сохранения: ${errorData.error || 'Неизвестная ошибка'}`); } } catch (error: any) { console.error('Ошибка сохранения совещания:', error); alert(`Ошибка сохранения: ${error.message || 'Неизвестная ошибка'}`); } }; const handleStartMeeting = async () => { if (!selectedMeeting) return; try { const response = await authFetch(`/api/office/meetings/${selectedMeeting.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'in_progress' }) }); if (response.ok) { fetchMeetings(); if (selectedMeeting) { setSelectedMeeting({ ...selectedMeeting, status: 'in_progress' }); } alert('Совещание начато'); } else { const errorData = await response.json().catch(() => ({ error: 'Неизвестная ошибка' })); alert(`Ошибка: ${errorData.error || 'Неизвестная ошибка'}`); } } catch (error: any) { console.error('Ошибка начала совещания:', error); alert(`Ошибка: ${error.message || 'Неизвестная ошибка'}`); } }; const handleEndMeeting = async () => { if (!selectedMeeting) return; try { const response = await authFetch(`/api/office/meetings/${selectedMeeting.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'completed', notes: meetingCardData.notes, conclusions: meetingCardData.conclusions }) }); if (response.ok) { fetchMeetings(); if (selectedMeeting) { setSelectedMeeting({ ...selectedMeeting, status: 'completed' }); } alert('Совещание завершено'); } else { const errorData = await response.json().catch(() => ({ error: 'Неизвестная ошибка' })); alert(`Ошибка: ${errorData.error || 'Неизвестная ошибка'}`); } } catch (error: any) { console.error('Ошибка завершения совещания:', error); alert(`Ошибка: ${error.message || 'Неизвестная ошибка'}`); } }; const handleSaveMeetingCard = async () => { if (!selectedMeeting) return; try { const response = await authFetch(`/api/office/meetings/${selectedMeeting.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ notes: meetingCardData.notes, conclusions: meetingCardData.conclusions }) }); if (response.ok) { fetchMeetings(); alert('Данные сохранены'); } else { const errorData = await response.json().catch(() => ({ error: 'Неизвестная ошибка' })); alert(`Ошибка сохранения: ${errorData.error || 'Неизвестная ошибка'}`); } } catch (error: any) { console.error('Ошибка сохранения:', error); alert(`Ошибка: ${error.message || 'Неизвестная ошибка'}`); } }; const handleRescheduleMeeting = async () => { if (!selectedMeeting) return; if (!rescheduleData.date || !rescheduleData.startTime || !rescheduleData.endTime) { alert('Заполните все поля для переноса'); return; } try { const newStartDateTime = new Date(`${rescheduleData.date}T${rescheduleData.startTime}`); const newEndDateTime = new Date(`${rescheduleData.date}T${rescheduleData.endTime}`); if (newEndDateTime <= newStartDateTime) { alert('Время окончания должно быть позже времени начала'); return; } const response = await authFetch(`/api/office/meetings/${selectedMeeting.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ startTime: newStartDateTime.toISOString(), endTime: newEndDateTime.toISOString() }) }); if (response.ok) { fetchMeetings(); setSelectedDate(rescheduleData.date); setShowRescheduleModal(false); setShowMeetingCard(false); alert('Совещание перенесено'); } else { const errorData = await response.json().catch(() => ({ error: 'Неизвестная ошибка' })); alert(`Ошибка: ${errorData.error || 'Неизвестная ошибка'}`); } } catch (error: any) { console.error('Ошибка переноса совещания:', error); alert(`Ошибка: ${error.message || 'Неизвестная ошибка'}`); } }; const handleOpenEditRoom = (room: MeetingRoom) => { setEditingRoom(room); setRoomFormData({ name: room.name, capacity: room.capacity, location: room.location || '', description: room.description || '', equipment: Array.isArray(room.equipment) ? [...room.equipment] : [], isActive: room.isActive !== false }); setShowCreateRoomModal(true); }; const handleCreateRoom = async () => { try { if (!roomFormData.name || !roomFormData.name.trim()) { alert('Пожалуйста, укажите название переговорной'); return; } if (editingRoom) { const response = await authFetch(`/api/office/meeting-rooms/${editingRoom.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: roomFormData.name, capacity: roomFormData.capacity, location: roomFormData.location || null, description: roomFormData.description || null, equipment: roomFormData.equipment, isActive: roomFormData.isActive !== false }) }); if (response.ok) { fetchRooms(); setShowCreateRoomModal(false); setEditingRoom(null); setRoomFormData({ name: '', capacity: 10, location: '', description: '', equipment: [], isActive: true }); alert('Переговорная обновлена'); } else { const errorData = await response.json().catch(() => ({ error: 'Неизвестная ошибка' })); alert(`Ошибка сохранения: ${errorData.error || 'Неизвестная ошибка'}`); } return; } const response = await authFetch('/api/office/meeting-rooms', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: roomFormData.name, capacity: roomFormData.capacity, location: roomFormData.location || null, description: roomFormData.description || null, equipment: roomFormData.equipment }) }); if (response.ok) { fetchRooms(); setShowCreateRoomModal(false); setRoomFormData({ name: '', capacity: 10, location: '', description: '', equipment: [], isActive: true }); alert('Переговорная комната успешно создана'); } else { const errorData = await response.json().catch(() => ({ error: 'Неизвестная ошибка' })); alert(`Ошибка создания переговорной: ${errorData.error || 'Неизвестная ошибка'}`); } } catch (error: any) { console.error('Ошибка создания переговорной:', error); alert(`Ошибка создания переговорной: ${error.message || 'Неизвестная ошибка'}`); } }; const handleAddParticipants = async (newParticipants: string[]) => { if (!selectedMeeting) return; try { const currentParticipants = selectedMeeting.participants || []; const updatedParticipants = [...new Set([...currentParticipants, ...newParticipants])]; const response = await authFetch(`/api/office/meetings/${selectedMeeting.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ participants: updatedParticipants }) }); if (response.ok) { fetchMeetings(); if (selectedMeeting) { setSelectedMeeting({ ...selectedMeeting, participants: updatedParticipants }); } alert('Участники добавлены'); } else { const errorData = await response.json().catch(() => ({ error: 'Неизвестная ошибка' })); alert(`Ошибка: ${errorData.error || 'Неизвестная ошибка'}`); } } catch (error: any) { console.error('Ошибка добавления участников:', error); alert(`Ошибка: ${error.message || 'Неизвестная ошибка'}`); } }; const fetchBookings = async () => { try { const response = await authFetch(`/api/office/meeting-bookings?startDate=${selectedDate}&endDate=${selectedDate}`); if (response.ok) { const data = await response.json(); const normalized = (Array.isArray(data) ? data : []).map((b: any) => ({ id: b.id, roomId: b.room_id ?? b.roomId, meetingId: b.meeting_id ?? b.meetingId, bookedBy: b.booked_by ?? b.bookedBy, startTime: b.start_time ?? b.startTime, endTime: b.end_time ?? b.endTime, purpose: b.purpose, status: b.status ?? 'active', room: b.room_name ? { id: b.room_id, name: b.room_name, capacity: 0, isActive: true, createdAt: '', updatedAt: '' } : b.room, meeting: b.meeting_title ? { id: 0, title: b.meeting_title, organizer: '', startTime: b.start_time, endTime: b.end_time, participants: [], status: 'scheduled', reminderSent: false, createdAt: '', updatedAt: '' } : b.meeting, createdAt: b.created_at ?? b.createdAt, updatedAt: b.updated_at ?? b.updatedAt })); setBookings(normalized); } } catch (error) { console.error('Ошибка загрузки бронирований:', error); } }; return (

Совещания и переговорные

{/* Выбор даты */}
setSelectedDate(e.target.value)} className="px-3 py-2 border border-slate-300 rounded-lg text-sm" />
{/* Совещания */}

Совещания

{meetings.length > 0 ? (
{meetings.map((meeting) => (
{ setSelectedMeeting(meeting); setMeetingCardData({ notes: meeting.notes || '', conclusions: meeting.conclusions || '', questions: '', theses: '' }); setShowMeetingCard(true); }} >
{meeting.title}
{(meeting.notes || meeting.conclusions) && ( Есть записи )}
{new Date(meeting.startTime).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })} - {new Date(meeting.endTime).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })}
{meeting.room && (
{meeting.room.name}
)}
{(meeting.participants || []).length} участников
))}
) : (

Нет совещаний на выбранную дату

)}
{/* Переговорные */}

Переговорные комнаты

{rooms.length > 0 ? (
{rooms.map((room) => { const roomBookings = bookings.filter(b => b.roomId === room.id); const activeBookings = roomBookings .filter(b => b.status === 'active') .sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime()); const isFree = room.isActive !== false && activeBookings.length === 0; return (
{room.name}
{room.isActive === false && ( Неактивна )}
{room.isActive !== false && ( )} {room.isActive === false ? 'Неактивна' : isFree ? 'Свободна' : 'Занята'}

Вместимость: {room.capacity} чел. {room.location && ` · ${room.location}`}

{room.isActive !== false && (
{activeBookings.length > 0 ? (

Занято:

{activeBookings.map((booking) => (
{new Date(booking.startTime).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })}–{new Date(booking.endTime).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })} {(booking.purpose || booking.meeting?.title) && ( · {booking.meeting?.title ? `Совещание: ${booking.meeting.title}` : booking.purpose} )}
))}
) : (

На выбранную дату свободна весь день

)}
)} {room.isActive === false && ( )}
); })}
) : (

Нет переговорных комнат

)}
{/* Meeting Card Modal */} {showMeetingCard && selectedMeeting && (

{selectedMeeting.title}

{new Date(selectedMeeting.startTime).toLocaleString('ru-RU')} - {new Date(selectedMeeting.endTime).toLocaleString('ru-RU')}

{/* Статус и действия */}
{selectedMeeting.status === 'in_progress' ? 'Идет' : selectedMeeting.status === 'completed' ? 'Завершено' : selectedMeeting.status === 'canceled' ? 'Отменено' : 'Запланировано'} {selectedMeeting.room && ( {selectedMeeting.room.name} )}
{selectedMeeting.status === 'scheduled' && ( )} {selectedMeeting.status === 'in_progress' && ( )}
{/* Информация о совещании */}
Организатор: {selectedMeeting.organizer}
Участников: {(selectedMeeting.participants || []).length}
{selectedMeeting.agenda && (
Повестка:

{selectedMeeting.agenda}

)} {selectedMeeting.description && (
Описание:

{selectedMeeting.description}

)}
{/* Участники */}

Участники

{(selectedMeeting.participants || []).map((participant, index) => ( {participant} ))} {(!selectedMeeting.participants || selectedMeeting.participants.length === 0) && ( Нет участников )}
{/* Тезисы */}

Тезисы