Files
mkd/components/office/BookMeetingRoomModal.tsx
2026-02-04 00:17:04 +05:00

198 lines
7.5 KiB
TypeScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { authFetch } from '../../services/apiClient';
import { X, Building2 } from 'lucide-react';
import { MeetingRoom } from '../../types';
import { CURRENT_USER_MOCK } from '../../constants';
interface BookMeetingRoomModalProps {
purpose: string;
defaultDate?: string;
onBooked: (roomName?: string) => void;
onClose: () => void;
}
export const BookMeetingRoomModal: React.FC<BookMeetingRoomModalProps> = ({
purpose,
defaultDate = new Date().toISOString().split('T')[0],
onBooked,
onClose
}) => {
const [rooms, setRooms] = useState<MeetingRoom[]>([]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [date, setDate] = useState(defaultDate);
const [startTime, setStartTime] = useState('10:00');
const [endTime, setEndTime] = useState('11:00');
const [roomId, setRoomId] = useState('');
useEffect(() => {
const fetchRooms = async () => {
try {
const res = await authFetch('/api/office/meeting-rooms');
if (res.ok) {
const data = await res.json();
const normalized = (Array.isArray(data) ? data : []).map((r: any) => ({
id: r.id,
name: r.name || '',
capacity: r.capacity || 0,
location: r.location,
isActive: r.is_active !== false
}));
setRooms(normalized.filter((r: MeetingRoom) => r.isActive !== false));
if (normalized.length > 0 && !roomId) setRoomId(String(normalized[0].id));
}
} catch (e) {
console.error(e);
setError('Не удалось загрузить переговорные');
} finally {
setLoading(false);
}
};
fetchRooms();
}, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!roomId || !date || !startTime || !endTime) {
setError('Заполните дату, время и выберите переговорную');
return;
}
const startDateTime = new Date(`${date}T${startTime}`);
const endDateTime = new Date(`${date}T${endTime}`);
if (endDateTime <= startDateTime) {
setError('Время окончания должно быть позже начала');
return;
}
setError(null);
setSaving(true);
try {
const res = await authFetch('/api/office/meeting-bookings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
roomId: parseInt(roomId),
bookedBy: CURRENT_USER_MOCK.name,
startTime: startDateTime.toISOString(),
endTime: endDateTime.toISOString(),
purpose
})
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || 'Ошибка бронирования');
}
const room = rooms.find(r => r.id === parseInt(roomId));
onBooked(room?.name);
onClose();
} catch (err) {
setError(err instanceof Error ? err.message : 'Ошибка бронирования');
} finally {
setSaving(false);
}
};
const modalContent = (
<div
className="fixed inset-0 z-[110] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm"
onClick={onClose}
role="dialog"
aria-modal="true"
>
<div
className="bg-white rounded-2xl w-full max-w-md shadow-2xl"
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
>
<div className="flex justify-between items-center p-4 border-b border-slate-200">
<h3 className="text-lg font-bold text-slate-800">Забронировать переговорную</h3>
<button type="button" onClick={onClose} className="p-2 hover:bg-slate-100 rounded-lg">
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleSubmit} className="p-4 space-y-4">
{error && (
<div className="p-3 bg-red-50 text-red-700 rounded-lg text-sm">{error}</div>
)}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Цель</label>
<p className="text-sm text-slate-600 bg-slate-50 px-3 py-2 rounded-lg">{purpose}</p>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Дата *</label>
<input
type="date"
value={date}
onChange={e => setDate(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-3">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Начало *</label>
<input
type="time"
value={startTime}
onChange={e => setStartTime(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={endTime}
onChange={e => setEndTime(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>
{loading ? (
<p className="text-sm text-slate-500">Загрузка...</p>
) : (
<select
value={roomId}
onChange={e => setRoomId(e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
required
>
<option value="">Выберите переговорную</option>
{rooms.map(r => (
<option key={r.id} value={r.id}>{r.name} (до {r.capacity} чел.)</option>
))}
</select>
)}
</div>
<div className="flex gap-3 pt-2">
<button
type="button"
onClick={onClose}
className="flex-1 px-4 py-2 bg-slate-100 text-slate-700 rounded-lg font-medium text-sm hover:bg-slate-200"
>
Отмена
</button>
<button
type="submit"
disabled={saving || loading || !roomId}
className="flex-1 px-4 py-2 bg-primary-600 text-white rounded-lg font-medium text-sm hover:bg-primary-700 disabled:opacity-50 flex items-center justify-center gap-2"
>
<Building2 className="w-4 h-4" />
{saving ? 'Бронируем...' : 'Забронировать'}
</button>
</div>
</form>
</div>
</div>
);
return typeof document !== 'undefined' && document.body
? createPortal(modalContent, document.body)
: modalContent;
};