1424 lines
64 KiB
TypeScript
Executable File
1424 lines
64 KiB
TypeScript
Executable File
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<Meeting[]>([]);
|
||
const [rooms, setRooms] = useState<MeetingRoom[]>([]);
|
||
const [bookings, setBookings] = useState<MeetingBooking[]>([]);
|
||
const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]);
|
||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||
const [showCreateRoomModal, setShowCreateRoomModal] = useState(false);
|
||
const [editingRoom, setEditingRoom] = useState<MeetingRoom | null>(null);
|
||
const [showRescheduleModal, setShowRescheduleModal] = useState(false);
|
||
const [selectedMeeting, setSelectedMeeting] = useState<Meeting | null>(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<Array<{ id: string; name: string }>>([]);
|
||
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 (
|
||
<div className="space-y-6 animate-fade-in">
|
||
<div className="flex justify-between items-center">
|
||
<h3 className="text-lg font-bold text-slate-800">Совещания и переговорные</h3>
|
||
<button
|
||
onClick={() => {
|
||
setShowCreateModal(true);
|
||
// Устанавливаем время по умолчанию
|
||
const now = new Date();
|
||
const defaultStart = new Date(now.getTime() + 60 * 60 * 1000); // Через час
|
||
const defaultEnd = new Date(defaultStart.getTime() + 60 * 60 * 1000); // Через 2 часа
|
||
setFormData(prev => ({
|
||
...prev,
|
||
startTime: defaultStart.toTimeString().slice(0, 5),
|
||
endTime: defaultEnd.toTimeString().slice(0, 5)
|
||
}));
|
||
}}
|
||
className="bg-primary-600 text-white px-4 py-2 rounded-lg text-sm font-bold hover:bg-primary-700 transition-colors flex items-center gap-2"
|
||
>
|
||
<Plus className="w-4 h-4" />
|
||
Новое совещание
|
||
</button>
|
||
</div>
|
||
|
||
{/* Выбор даты */}
|
||
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-4">
|
||
<div className="flex items-center gap-4">
|
||
<label className="text-sm font-medium text-slate-700">Дата:</label>
|
||
<input
|
||
type="date"
|
||
value={selectedDate}
|
||
onChange={(e) => setSelectedDate(e.target.value)}
|
||
className="px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||
{/* Совещания */}
|
||
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6">
|
||
<h4 className="font-bold text-slate-800 mb-4 flex items-center gap-2">
|
||
<Calendar className="w-5 h-5" />
|
||
Совещания
|
||
</h4>
|
||
{meetings.length > 0 ? (
|
||
<div className="space-y-3">
|
||
{meetings.map((meeting) => (
|
||
<div
|
||
key={meeting.id}
|
||
className="p-4 bg-slate-50 rounded-lg border border-slate-200 cursor-pointer hover:border-primary-300 transition-colors"
|
||
onClick={() => {
|
||
setSelectedMeeting(meeting);
|
||
setMeetingCardData({
|
||
notes: meeting.notes || '',
|
||
conclusions: meeting.conclusions || '',
|
||
questions: '',
|
||
theses: ''
|
||
});
|
||
setShowMeetingCard(true);
|
||
}}
|
||
>
|
||
<div className="flex justify-between items-start mb-2">
|
||
<h5 className="font-bold text-slate-800">{meeting.title}</h5>
|
||
{(meeting.notes || meeting.conclusions) && (
|
||
<span className="text-xs text-emerald-600 font-bold flex items-center gap-1">
|
||
<Edit className="w-3 h-3" />
|
||
Есть записи
|
||
</span>
|
||
)}
|
||
</div>
|
||
<div className="space-y-1 text-sm text-slate-600">
|
||
<div className="flex items-center gap-2">
|
||
<Clock className="w-4 h-4" />
|
||
{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' })}
|
||
</div>
|
||
{meeting.room && (
|
||
<div className="flex items-center gap-2">
|
||
<MapPin className="w-4 h-4" />
|
||
{meeting.room.name}
|
||
</div>
|
||
)}
|
||
<div className="flex items-center gap-2">
|
||
<Users className="w-4 h-4" />
|
||
{(meeting.participants || []).length} участников
|
||
</div>
|
||
<div className="flex gap-2 mt-2">
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setSelectedMeeting(meeting);
|
||
// Инициализируем данные карточки из совещания
|
||
setMeetingCardData({
|
||
notes: meeting.notes || '',
|
||
conclusions: meeting.conclusions || '',
|
||
questions: '', // Пока нет поля в БД, можно добавить позже
|
||
theses: '' // Пока нет поля в БД, можно добавить позже
|
||
});
|
||
setShowMeetingCard(true);
|
||
}}
|
||
className="px-3 py-1.5 bg-blue-50 text-blue-600 rounded-lg text-xs font-bold hover:bg-blue-100 transition-colors flex items-center gap-1"
|
||
title="Открыть карточку совещания"
|
||
>
|
||
<CheckCircle2 className="w-3 h-3" />
|
||
Карточка
|
||
</button>
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
handleOpenEditModal(meeting);
|
||
}}
|
||
className="px-3 py-1.5 bg-emerald-50 text-emerald-600 rounded-lg text-xs font-bold hover:bg-emerald-100 transition-colors flex items-center gap-1"
|
||
title="Редактировать заметки и заключения"
|
||
>
|
||
<Edit className="w-3 h-3" />
|
||
Заметки
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<p className="text-slate-500 text-sm">Нет совещаний на выбранную дату</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* Переговорные */}
|
||
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6">
|
||
<div className="flex justify-between items-center mb-4">
|
||
<h4 className="font-bold text-slate-800">Переговорные комнаты</h4>
|
||
<button
|
||
onClick={() => {
|
||
setEditingRoom(null);
|
||
setRoomFormData({
|
||
name: '',
|
||
capacity: 10,
|
||
location: '',
|
||
description: '',
|
||
equipment: [],
|
||
isActive: true
|
||
});
|
||
setShowCreateRoomModal(true);
|
||
}}
|
||
className="px-3 py-1.5 bg-primary-600 text-white rounded-lg text-xs font-bold hover:bg-primary-700 transition-colors flex items-center gap-1"
|
||
>
|
||
<Plus className="w-3 h-3" />
|
||
Создать
|
||
</button>
|
||
</div>
|
||
{rooms.length > 0 ? (
|
||
<div className="space-y-3">
|
||
{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 (
|
||
<div
|
||
key={room.id}
|
||
className={`p-4 rounded-lg border ${room.isActive === false ? 'bg-slate-100 border-slate-200' : 'bg-slate-50 border-slate-200'}`}
|
||
>
|
||
<div className="flex items-start justify-between gap-2 mb-2">
|
||
<div className="min-w-0">
|
||
<h5 className="font-bold text-slate-800">{room.name}</h5>
|
||
{room.isActive === false && (
|
||
<span className="text-xs text-amber-600 font-medium">Неактивна</span>
|
||
)}
|
||
</div>
|
||
<div className="flex items-center gap-1 flex-shrink-0">
|
||
{room.isActive !== false && (
|
||
<button
|
||
onClick={(e) => { e.stopPropagation(); handleOpenEditRoom(room); }}
|
||
className="p-1.5 text-slate-500 hover:text-primary-600 hover:bg-primary-50 rounded-lg transition-colors"
|
||
title="Редактировать переговорную"
|
||
>
|
||
<Edit className="w-4 h-4" />
|
||
</button>
|
||
)}
|
||
<span className={`px-2 py-1 rounded text-xs ${
|
||
room.isActive === false
|
||
? 'bg-slate-200 text-slate-600'
|
||
: isFree
|
||
? 'bg-emerald-100 text-emerald-700'
|
||
: 'bg-amber-100 text-amber-700'
|
||
}`}>
|
||
{room.isActive === false ? 'Неактивна' : isFree ? 'Свободна' : 'Занята'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<p className="text-sm text-slate-600 mb-2">
|
||
Вместимость: {room.capacity} чел.
|
||
{room.location && ` · ${room.location}`}
|
||
</p>
|
||
{room.isActive !== false && (
|
||
<div className="mt-2">
|
||
{activeBookings.length > 0 ? (
|
||
<div className="space-y-1.5">
|
||
<p className="text-xs font-medium text-slate-500">Занято:</p>
|
||
{activeBookings.map((booking) => (
|
||
<div key={booking.id} className="text-xs text-slate-600 flex items-center gap-2">
|
||
<Clock className="w-3.5 h-3.5 flex-shrink-0" />
|
||
{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) && (
|
||
<span className="text-slate-500">
|
||
· {booking.meeting?.title ? `Совещание: ${booking.meeting.title}` : booking.purpose}
|
||
</span>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<p className="text-xs text-emerald-600">На выбранную дату свободна весь день</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
{room.isActive === false && (
|
||
<button
|
||
onClick={(e) => { e.stopPropagation(); handleOpenEditRoom(room); }}
|
||
className="mt-2 text-xs text-primary-600 hover:underline"
|
||
>
|
||
Редактировать
|
||
</button>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
) : (
|
||
<p className="text-slate-500 text-sm">Нет переговорных комнат</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Meeting Card Modal */}
|
||
{showMeetingCard && selectedMeeting && (
|
||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||
<div className="bg-white rounded-2xl p-6 max-w-4xl w-full max-h-[90vh] overflow-y-auto">
|
||
<div className="flex justify-between items-center mb-4">
|
||
<div>
|
||
<h3 className="text-lg font-bold text-slate-800">{selectedMeeting.title}</h3>
|
||
<p className="text-sm text-slate-500 mt-1">
|
||
{new Date(selectedMeeting.startTime).toLocaleString('ru-RU')} - {new Date(selectedMeeting.endTime).toLocaleString('ru-RU')}
|
||
</p>
|
||
</div>
|
||
<button
|
||
onClick={() => {
|
||
setShowMeetingCard(false);
|
||
setSelectedMeeting(null);
|
||
}}
|
||
className="p-2 hover:bg-slate-100 rounded-lg transition-colors"
|
||
>
|
||
<X className="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
|
||
{/* Статус и действия */}
|
||
<div className="mb-6 p-4 bg-slate-50 rounded-lg">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="flex items-center gap-3">
|
||
<span className={`px-3 py-1 rounded-lg text-xs font-bold ${
|
||
selectedMeeting.status === 'in_progress' ? 'bg-emerald-100 text-emerald-700' :
|
||
selectedMeeting.status === 'completed' ? 'bg-blue-100 text-blue-700' :
|
||
selectedMeeting.status === 'canceled' ? 'bg-red-100 text-red-700' :
|
||
'bg-slate-100 text-slate-700'
|
||
}`}>
|
||
{selectedMeeting.status === 'in_progress' ? 'Идет' :
|
||
selectedMeeting.status === 'completed' ? 'Завершено' :
|
||
selectedMeeting.status === 'canceled' ? 'Отменено' :
|
||
'Запланировано'}
|
||
</span>
|
||
{selectedMeeting.room && (
|
||
<span className="text-sm text-slate-600 flex items-center gap-1">
|
||
<MapPin className="w-4 h-4" />
|
||
{selectedMeeting.room.name}
|
||
</span>
|
||
)}
|
||
</div>
|
||
<div className="flex gap-2">
|
||
{selectedMeeting.status === 'scheduled' && (
|
||
<button
|
||
onClick={handleStartMeeting}
|
||
className="px-4 py-2 bg-emerald-600 text-white rounded-lg text-sm font-bold hover:bg-emerald-700 transition-colors flex items-center gap-2"
|
||
>
|
||
<Play className="w-4 h-4" />
|
||
Начать совещание
|
||
</button>
|
||
)}
|
||
{selectedMeeting.status === 'in_progress' && (
|
||
<button
|
||
onClick={handleEndMeeting}
|
||
className="px-4 py-2 bg-red-600 text-white rounded-lg text-sm font-bold hover:bg-red-700 transition-colors flex items-center gap-2"
|
||
>
|
||
<Square className="w-4 h-4" />
|
||
Завершить совещание
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Информация о совещании */}
|
||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||
<div>
|
||
<span className="text-slate-600">Организатор:</span>
|
||
<span className="ml-2 font-medium">{selectedMeeting.organizer}</span>
|
||
</div>
|
||
<div>
|
||
<span className="text-slate-600">Участников:</span>
|
||
<span className="ml-2 font-medium">{(selectedMeeting.participants || []).length}</span>
|
||
</div>
|
||
{selectedMeeting.agenda && (
|
||
<div className="col-span-2">
|
||
<span className="text-slate-600">Повестка:</span>
|
||
<p className="mt-1 text-slate-800">{selectedMeeting.agenda}</p>
|
||
</div>
|
||
)}
|
||
{selectedMeeting.description && (
|
||
<div className="col-span-2">
|
||
<span className="text-slate-600">Описание:</span>
|
||
<p className="mt-1 text-slate-800">{selectedMeeting.description}</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Участники */}
|
||
<div className="mb-6">
|
||
<div className="flex justify-between items-center mb-2">
|
||
<h4 className="font-bold text-slate-800 flex items-center gap-2">
|
||
<Users className="w-4 h-4" />
|
||
Участники
|
||
</h4>
|
||
<button
|
||
onClick={() => {
|
||
const newParticipant = prompt('Введите имя участника:');
|
||
if (newParticipant && newParticipant.trim()) {
|
||
handleAddParticipants([newParticipant.trim()]);
|
||
}
|
||
}}
|
||
className="px-3 py-1.5 bg-primary-50 text-primary-600 rounded-lg text-xs font-bold hover:bg-primary-100 transition-colors flex items-center gap-1"
|
||
>
|
||
<UserPlus className="w-3 h-3" />
|
||
Добавить
|
||
</button>
|
||
</div>
|
||
<div className="flex flex-wrap gap-2">
|
||
{(selectedMeeting.participants || []).map((participant, index) => (
|
||
<span key={index} className="px-3 py-1 bg-slate-100 text-slate-700 rounded-lg text-sm">
|
||
{participant}
|
||
</span>
|
||
))}
|
||
{(!selectedMeeting.participants || selectedMeeting.participants.length === 0) && (
|
||
<span className="text-sm text-slate-400">Нет участников</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Тезисы */}
|
||
<div className="mb-6">
|
||
<h4 className="font-bold text-slate-800 mb-2 flex items-center gap-2">
|
||
<FileText className="w-4 h-4" />
|
||
Тезисы
|
||
</h4>
|
||
<textarea
|
||
value={meetingCardData.theses}
|
||
onChange={(e) => setMeetingCardData({ ...meetingCardData, theses: e.target.value })}
|
||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
||
rows={4}
|
||
placeholder="Введите тезисы совещания..."
|
||
/>
|
||
</div>
|
||
|
||
{/* Вопросы */}
|
||
<div className="mb-6">
|
||
<h4 className="font-bold text-slate-800 mb-2 flex items-center gap-2">
|
||
<MessageSquare className="w-4 h-4" />
|
||
Вопросы
|
||
</h4>
|
||
<textarea
|
||
value={meetingCardData.questions}
|
||
onChange={(e) => setMeetingCardData({ ...meetingCardData, questions: e.target.value })}
|
||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
||
rows={4}
|
||
placeholder="Введите вопросы для обсуждения..."
|
||
/>
|
||
</div>
|
||
|
||
{/* Заметки */}
|
||
<div className="mb-6">
|
||
<h4 className="font-bold text-slate-800 mb-2">Заметки / Протокол</h4>
|
||
<textarea
|
||
value={meetingCardData.notes}
|
||
onChange={(e) => setMeetingCardData({ ...meetingCardData, notes: e.target.value })}
|
||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
||
rows={6}
|
||
placeholder="Введите заметки или протокол совещания..."
|
||
/>
|
||
</div>
|
||
|
||
{/* Заключения */}
|
||
<div className="mb-6">
|
||
<h4 className="font-bold text-slate-800 mb-2">Заключения</h4>
|
||
<textarea
|
||
value={meetingCardData.conclusions}
|
||
onChange={(e) => setMeetingCardData({ ...meetingCardData, conclusions: e.target.value })}
|
||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
||
rows={6}
|
||
placeholder="Введите заключения совещания, принятые решения, задачи..."
|
||
/>
|
||
</div>
|
||
|
||
{/* Кнопки действий */}
|
||
<div className="flex gap-3 pt-4 border-t border-slate-200">
|
||
<button
|
||
onClick={() => {
|
||
setRescheduleData({
|
||
date: new Date(selectedMeeting.startTime).toISOString().split('T')[0],
|
||
startTime: new Date(selectedMeeting.startTime).toTimeString().slice(0, 5),
|
||
endTime: new Date(selectedMeeting.endTime).toTimeString().slice(0, 5)
|
||
});
|
||
setShowRescheduleModal(true);
|
||
}}
|
||
className="px-4 py-2 bg-amber-50 text-amber-600 rounded-lg text-sm font-bold hover:bg-amber-100 transition-colors flex items-center gap-2"
|
||
>
|
||
<CalendarIcon className="w-4 h-4" />
|
||
Перенести
|
||
</button>
|
||
<button
|
||
onClick={handleSaveMeetingCard}
|
||
className="flex-1 bg-primary-600 text-white px-4 py-2 rounded-lg font-bold text-sm hover:bg-primary-700 transition-colors flex items-center justify-center gap-2"
|
||
>
|
||
<Save className="w-4 h-4" />
|
||
Сохранить
|
||
</button>
|
||
<button
|
||
onClick={() => {
|
||
setShowMeetingCard(false);
|
||
setSelectedMeeting(null);
|
||
}}
|
||
className="px-4 py-2 bg-slate-100 text-slate-600 rounded-lg font-bold text-sm hover:bg-slate-200 transition-colors"
|
||
>
|
||
Закрыть
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Edit Meeting Modal (Notes & Conclusions) */}
|
||
{showEditModal && selectedMeeting && (
|
||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||
<div className="bg-white rounded-2xl p-6 max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||
<div className="flex justify-between items-center mb-4">
|
||
<div>
|
||
<h3 className="text-lg font-bold text-slate-800">Заметки и заключения</h3>
|
||
<p className="text-sm text-slate-500 mt-1">{selectedMeeting.title}</p>
|
||
</div>
|
||
<button
|
||
onClick={() => {
|
||
setShowEditModal(false);
|
||
setSelectedMeeting(null);
|
||
}}
|
||
className="p-2 hover:bg-slate-100 rounded-lg transition-colors"
|
||
>
|
||
<X className="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Заметки / Протокол</label>
|
||
<textarea
|
||
value={editData.notes}
|
||
onChange={(e) => setEditData({ ...editData, notes: e.target.value })}
|
||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
||
rows={8}
|
||
placeholder="Введите заметки или протокол совещания..."
|
||
/>
|
||
<p className="text-xs text-slate-400 mt-1">Заметки во время совещания, протокол</p>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Заключения</label>
|
||
<textarea
|
||
value={editData.conclusions}
|
||
onChange={(e) => setEditData({ ...editData, conclusions: e.target.value })}
|
||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
||
rows={6}
|
||
placeholder="Введите заключения совещания, принятые решения, задачи..."
|
||
/>
|
||
<p className="text-xs text-slate-400 mt-1">Заключения, решения, задачи по итогам совещания</p>
|
||
</div>
|
||
|
||
<div className="flex gap-3 pt-4">
|
||
<button
|
||
onClick={handleSaveMeeting}
|
||
className="flex-1 bg-primary-600 text-white px-4 py-2 rounded-lg font-bold text-sm hover:bg-primary-700 transition-colors flex items-center justify-center gap-2"
|
||
>
|
||
<Save className="w-4 h-4" />
|
||
Сохранить
|
||
</button>
|
||
<button
|
||
onClick={() => {
|
||
setShowEditModal(false);
|
||
setSelectedMeeting(null);
|
||
}}
|
||
className="px-4 py-2 bg-slate-100 text-slate-600 rounded-lg font-bold text-sm hover:bg-slate-200 transition-colors"
|
||
>
|
||
Отмена
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Create Meeting Modal */}
|
||
{showCreateModal && (
|
||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||
<div className="bg-white rounded-2xl p-6 max-w-md w-full max-h-[90vh] overflow-y-auto">
|
||
<div className="flex justify-between items-center mb-4">
|
||
<h3 className="text-lg font-bold text-slate-800">Создать совещание</h3>
|
||
<button
|
||
onClick={() => setShowCreateModal(false)}
|
||
className="p-2 hover:bg-slate-100 rounded-lg transition-colors"
|
||
>
|
||
<X className="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Название *</label>
|
||
<input
|
||
type="text"
|
||
value={formData.title}
|
||
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
||
placeholder="Введите название совещания"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Дата</label>
|
||
<input
|
||
type="date"
|
||
value={selectedDate}
|
||
onChange={(e) => setSelectedDate(e.target.value)}
|
||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Время начала *</label>
|
||
<input
|
||
type="time"
|
||
value={formData.startTime}
|
||
onChange={(e) => setFormData({ ...formData, startTime: e.target.value })}
|
||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
||
required
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Время окончания *</label>
|
||
<input
|
||
type="time"
|
||
value={formData.endTime}
|
||
onChange={(e) => setFormData({ ...formData, endTime: e.target.value })}
|
||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
||
required
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Переговорная</label>
|
||
<select
|
||
value={formData.roomId}
|
||
onChange={(e) => setFormData({ ...formData, roomId: e.target.value })}
|
||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
||
>
|
||
<option value="">Без переговорной</option>
|
||
{rooms.filter((r) => r.isActive !== false).map((room) => (
|
||
<option key={room.id} value={room.id.toString()}>
|
||
{room.name} (вместимость: {room.capacity})
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Участники</label>
|
||
<div className="border border-slate-300 rounded-lg p-2 max-h-40 overflow-y-auto bg-slate-50/50">
|
||
{employees.length === 0 ? (
|
||
<p className="text-xs text-slate-500 py-2">Список сотрудников пуст</p>
|
||
) : (
|
||
<div className="space-y-1">
|
||
{employees.map((emp) => (
|
||
<label key={emp.id} className="flex items-center gap-2 py-1 px-2 rounded hover:bg-white cursor-pointer">
|
||
<input
|
||
type="checkbox"
|
||
checked={formData.participantIds.includes(emp.id)}
|
||
onChange={(e) => {
|
||
if (e.target.checked) {
|
||
setFormData({ ...formData, participantIds: [...formData.participantIds, emp.id] });
|
||
} else {
|
||
setFormData({ ...formData, participantIds: formData.participantIds.filter(id => id !== emp.id) });
|
||
}
|
||
}}
|
||
className="w-4 h-4 text-primary-600 border-slate-300 rounded"
|
||
/>
|
||
<span className="text-sm text-slate-700">{emp.name}</span>
|
||
</label>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
{formData.participantIds.length > 0 && (
|
||
<p className="text-xs text-slate-600 mt-1">Выбрано: {formData.participantIds.length}</p>
|
||
)}
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Описание</label>
|
||
<textarea
|
||
value={formData.description}
|
||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
||
rows={3}
|
||
placeholder="Описание совещания..."
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Повестка дня</label>
|
||
<textarea
|
||
value={formData.agenda}
|
||
onChange={(e) => setFormData({ ...formData, agenda: e.target.value })}
|
||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
||
rows={3}
|
||
placeholder="Повестка дня..."
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex gap-3 pt-4">
|
||
<button
|
||
onClick={handleCreateMeeting}
|
||
disabled={!formData.title || !formData.startTime || !formData.endTime}
|
||
className="flex-1 bg-primary-600 text-white px-4 py-2 rounded-lg font-bold text-sm hover:bg-primary-700 transition-colors disabled:bg-slate-300 disabled:cursor-not-allowed"
|
||
>
|
||
Создать
|
||
</button>
|
||
<button
|
||
onClick={() => setShowCreateModal(false)}
|
||
className="px-4 py-2 bg-slate-100 text-slate-600 rounded-lg font-bold text-sm hover:bg-slate-200 transition-colors"
|
||
>
|
||
Отмена
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Reschedule Meeting Modal */}
|
||
{showRescheduleModal && selectedMeeting && (
|
||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||
<div className="bg-white rounded-2xl p-6 max-w-md w-full">
|
||
<div className="flex justify-between items-center mb-4">
|
||
<h3 className="text-lg font-bold text-slate-800">Перенести совещание</h3>
|
||
<button
|
||
onClick={() => {
|
||
setShowRescheduleModal(false);
|
||
}}
|
||
className="p-2 hover:bg-slate-100 rounded-lg transition-colors"
|
||
>
|
||
<X className="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Название совещания</label>
|
||
<input
|
||
type="text"
|
||
value={selectedMeeting.title}
|
||
disabled
|
||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm bg-slate-50"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Новая дата *</label>
|
||
<input
|
||
type="date"
|
||
value={rescheduleData.date}
|
||
onChange={(e) => setRescheduleData({ ...rescheduleData, date: e.target.value })}
|
||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Время начала *</label>
|
||
<input
|
||
type="time"
|
||
value={rescheduleData.startTime}
|
||
onChange={(e) => setRescheduleData({ ...rescheduleData, startTime: e.target.value })}
|
||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||
required
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Время окончания *</label>
|
||
<input
|
||
type="time"
|
||
value={rescheduleData.endTime}
|
||
onChange={(e) => setRescheduleData({ ...rescheduleData, endTime: e.target.value })}
|
||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||
required
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3 text-xs text-blue-800">
|
||
<p className="font-bold mb-1">ℹ️ Текущее время:</p>
|
||
<p>
|
||
{new Date(selectedMeeting.startTime).toLocaleString('ru-RU')} - {new Date(selectedMeeting.endTime).toLocaleString('ru-RU')}
|
||
</p>
|
||
</div>
|
||
|
||
<div className="flex gap-3 pt-4">
|
||
<button
|
||
onClick={handleRescheduleMeeting}
|
||
disabled={!rescheduleData.date || !rescheduleData.startTime || !rescheduleData.endTime}
|
||
className="flex-1 bg-amber-600 text-white px-4 py-2 rounded-lg font-bold text-sm hover:bg-amber-700 transition-colors disabled:bg-slate-300 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||
>
|
||
<CalendarIcon className="w-4 h-4" />
|
||
Перенести
|
||
</button>
|
||
<button
|
||
onClick={() => {
|
||
setShowRescheduleModal(false);
|
||
}}
|
||
className="px-4 py-2 bg-slate-100 text-slate-600 rounded-lg font-bold text-sm hover:bg-slate-200 transition-colors"
|
||
>
|
||
Отмена
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Create / Edit Room Modal */}
|
||
{showCreateRoomModal && (
|
||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||
<div className="bg-white rounded-2xl p-6 max-w-md w-full max-h-[90vh] overflow-y-auto">
|
||
<div className="flex justify-between items-center mb-4">
|
||
<h3 className="text-lg font-bold text-slate-800">
|
||
{editingRoom ? 'Редактировать переговорную' : 'Создать переговорную'}
|
||
</h3>
|
||
<button
|
||
onClick={() => {
|
||
setShowCreateRoomModal(false);
|
||
setEditingRoom(null);
|
||
}}
|
||
className="p-2 hover:bg-slate-100 rounded-lg transition-colors"
|
||
>
|
||
<X className="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Название *</label>
|
||
<input
|
||
type="text"
|
||
value={roomFormData.name}
|
||
onChange={(e) => setRoomFormData({ ...roomFormData, name: e.target.value })}
|
||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||
placeholder="Например: Переговорная №1"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Вместимость *</label>
|
||
<input
|
||
type="number"
|
||
value={roomFormData.capacity}
|
||
onChange={(e) => setRoomFormData({ ...roomFormData, capacity: parseInt(e.target.value) || 10 })}
|
||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||
min="1"
|
||
required
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Местоположение</label>
|
||
<input
|
||
type="text"
|
||
value={roomFormData.location}
|
||
onChange={(e) => setRoomFormData({ ...roomFormData, location: e.target.value })}
|
||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||
placeholder="Например: 3 этаж"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Оборудование</label>
|
||
<div className="space-y-2">
|
||
{['projector', 'whiteboard', 'video_conference', 'screen', 'microphone'].map((eq) => (
|
||
<label key={eq} className="flex items-center gap-2 cursor-pointer">
|
||
<input
|
||
type="checkbox"
|
||
checked={roomFormData.equipment.includes(eq)}
|
||
onChange={(e) => {
|
||
if (e.target.checked) {
|
||
setRoomFormData({ ...roomFormData, equipment: [...roomFormData.equipment, eq] });
|
||
} else {
|
||
setRoomFormData({ ...roomFormData, equipment: roomFormData.equipment.filter(item => item !== eq) });
|
||
}
|
||
}}
|
||
className="w-4 h-4 text-primary-600 border-slate-300 rounded focus:ring-primary-500"
|
||
/>
|
||
<span className="text-sm text-slate-700">
|
||
{eq === 'projector' ? 'Проектор' :
|
||
eq === 'whiteboard' ? 'Доска' :
|
||
eq === 'video_conference' ? 'Видеоконференция' :
|
||
eq === 'screen' ? 'Экран' :
|
||
eq === 'microphone' ? 'Микрофон' : eq}
|
||
</span>
|
||
</label>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Описание</label>
|
||
<textarea
|
||
value={roomFormData.description}
|
||
onChange={(e) => setRoomFormData({ ...roomFormData, description: e.target.value })}
|
||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||
rows={3}
|
||
placeholder="Дополнительная информация о переговорной..."
|
||
/>
|
||
</div>
|
||
|
||
{editingRoom && (
|
||
<div>
|
||
<label className="flex items-center gap-2 cursor-pointer">
|
||
<input
|
||
type="checkbox"
|
||
checked={roomFormData.isActive}
|
||
onChange={(e) => setRoomFormData({ ...roomFormData, isActive: e.target.checked })}
|
||
className="w-4 h-4 text-primary-600 border-slate-300 rounded focus:ring-primary-500"
|
||
/>
|
||
<span className="text-sm font-medium text-slate-700">Активна (доступна для бронирования)</span>
|
||
</label>
|
||
<p className="text-xs text-slate-500 mt-1">Неактивные комнаты не отображаются в списке при создании совещания</p>
|
||
</div>
|
||
)}
|
||
|
||
<div className="flex gap-3 pt-4">
|
||
<button
|
||
onClick={handleCreateRoom}
|
||
disabled={!roomFormData.name || !roomFormData.name.trim() || !roomFormData.capacity || roomFormData.capacity <= 0}
|
||
className="flex-1 bg-primary-600 text-white px-4 py-2 rounded-lg font-bold text-sm hover:bg-primary-700 transition-colors disabled:bg-slate-300 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||
>
|
||
<Building2 className="w-4 h-4" />
|
||
{editingRoom ? 'Сохранить' : 'Создать'}
|
||
</button>
|
||
<button
|
||
onClick={() => {
|
||
setShowCreateRoomModal(false);
|
||
setEditingRoom(null);
|
||
}}
|
||
className="px-4 py-2 bg-slate-100 text-slate-600 rounded-lg font-bold text-sm hover:bg-slate-200 transition-colors"
|
||
>
|
||
Отмена
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|