import React, { useState, useEffect } from 'react'; import { authFetch } from '../../services/apiClient'; import { OfficeRequest, OfficeInventoryItem, Employee } from '../../types'; import { CURRENT_USER_MOCK } from '../../constants'; import { ShoppingCart, Package, AlertTriangle, Search, Plus, CheckCircle2, X, Edit, Warehouse, Monitor, Save, FileText, Trash2, Copy, ClipboardList, Bell, CheckSquare, Receipt } from 'lucide-react'; interface Props { requests: OfficeRequest[]; } export const SupplyRegistry: React.FC = ({ requests: initialRequests }) => { const [view, setView] = useState<'requests' | 'orders' | 'inventory'>('requests'); const [search, setSearch] = useState(''); const [requests, setRequests] = useState(initialRequests); const [orders, setOrders] = useState([]); const [inventory, setInventory] = useState([]); const [equipment, setEquipment] = useState([]); const [employees, setEmployees] = useState<{ id: string; name: string; position?: string }[]>([]); const [equipmentAssignedDropdownOpen, setEquipmentAssignedDropdownOpen] = useState(false); const [equipmentAssignedFilter, setEquipmentAssignedFilter] = useState(''); const [loading, setLoading] = useState(true); // Функция для проверки, является ли товар оборудованием const isEquipment = (request: OfficeRequest): boolean => { const itemName = (request.itemName || '').toLowerCase(); return request.category === 'equipment' || itemName.includes('компьютер') || itemName.includes('ноутбук') || itemName.includes('микроволновка') || itemName.includes('принтер') || itemName.includes('монитор') || itemName.includes('кондиционер') || itemName.includes('кондер') || itemName.includes('телефон') || itemName.includes('сканер'); }; const [showCreateRequestModal, setShowCreateRequestModal] = useState(false); const [showCreateOrderModal, setShowCreateOrderModal] = useState(false); const [selectedCategory, setSelectedCategory] = useState(null); const [selectedRequestsForOrder, setSelectedRequestsForOrder] = useState>(new Set()); const [showEditModal, setShowEditModal] = useState(false); const [showMoveToInventoryModal, setShowMoveToInventoryModal] = useState(false); const [showAddToEquipmentModal, setShowAddToEquipmentModal] = useState(false); const [showIssueFromInventoryModal, setShowIssueFromInventoryModal] = useState(false); const [showWriteOffModal, setShowWriteOffModal] = useState(false); const [writeOffSource, setWriteOffSource] = useState<'inventory' | 'equipment'>('inventory'); const [selectedRequest, setSelectedRequest] = useState(null); const [selectedInventoryItem, setSelectedInventoryItem] = useState(null); const [selectedEquipmentForWriteOff, setSelectedEquipmentForWriteOff] = useState(null); // Форма редактирования заявки const [editFormData, setEditFormData] = useState({ amount: 0, status: 'new' as 'new' | 'approved' | 'ordered' | 'received' | 'canceled', priority: 'medium' as 'low' | 'medium' | 'high' | 'urgent', notes: '' }); // Форма переноса на склад const [inventoryFormData, setInventoryFormData] = useState({ quantity: 0, location: '', notes: '' }); // Форма выдачи со склада const [issueFormData, setIssueFormData] = useState({ quantity: 0, requestId: '', notes: '' }); // Форма списания со склада const [writeOffFormData, setWriteOffFormData] = useState({ quantity: 0, reason: '' }); // Форма добавления в имущество const [equipmentFormData, setEquipmentFormData] = useState({ type: 'other' as 'pc' | 'laptop' | 'air_conditioner' | 'printer' | 'other', brand: '', model: '', serialNumber: '', assignedTo: '', purchaseDate: '', warrantyUntil: '', condition: 'good' as 'good' | 'fair' | 'poor', notes: '' }); // Форма создания заявки (таблица для нескольких позиций) const [requestItems, setRequestItems] = useState>([{ id: '1', category: 'stationery', itemName: '', quantity: 1, unit: 'шт.', priority: 'medium', notes: '' }]); useEffect(() => { fetchRequests(); fetchInventory(); fetchOrders(); fetchEquipmentForWriteOff(); fetchEmployees(); // Используем начальные данные, если API еще не загрузил if (initialRequests.length > 0 && requests.length === 0) { setRequests(initialRequests); } }, []); const fetchEmployees = async () => { try { const response = await authFetch('/api/employees'); if (response.ok) { const data = await response.json(); const activeEmployees = data .filter((emp: Employee) => !emp.status || emp.status === 'active') .map((emp: Employee) => ({ id: emp.id, name: emp.name, position: emp.position || '' })) .sort((a: { name: string }, b: { name: string }) => a.name.localeCompare(b.name)); setEmployees(activeEmployees); } } catch (error) { console.error('Ошибка загрузки сотрудников:', error); } }; const fetchOrders = async () => { try { const response = await authFetch('/api/office/orders'); if (response.ok) { const data = await response.json(); setOrders(data); } } catch (error) { console.error('Ошибка загрузки заказов:', error); } }; const fetchEquipmentForWriteOff = async () => { try { const data = await fetchEquipment(); setEquipment(data); } catch (error) { console.error('Ошибка загрузки оборудования:', error); } }; const fetchRequests = async () => { try { const response = await authFetch('/api/office/supply-requests'); if (response.ok) { const data = await response.json(); // Преобразуем snake_case в camelCase const normalizedData = data.map((req: any) => ({ id: req.id, requesterName: req.requester_name || req.requesterName || '', category: req.category || '', itemName: req.item_name || req.itemName || '', quantity: req.quantity || 1, issuedQuantity: req.issued_quantity || req.issuedQuantity || 0, unit: req.unit || 'шт.', amount: parseFloat(req.amount) || 0, priority: req.priority || 'medium', status: req.status || 'new', approvedBy: req.approved_by || req.approvedBy, approvedAt: req.approved_at || req.approvedAt, orderedAt: req.ordered_at || req.orderedAt, receivedAt: req.received_at || req.receivedAt, notes: req.notes, date: req.created_at || req.date, createdAt: req.created_at, updatedAt: req.updated_at })); setRequests(normalizedData); } } catch (error) { console.error('Ошибка загрузки заявок:', error); } finally { setLoading(false); } }; const fetchInventory = async () => { try { const response = await authFetch('/api/office/inventory'); if (response.ok) { const data = await response.json(); // Преобразуем snake_case в camelCase const normalizedData = data.map((item: any) => ({ id: item.id, name: item.name || '', category: item.category, quantity: parseFloat(item.quantity) || 0, unit: item.unit || 'шт.', minThreshold: parseFloat(item.min_threshold) || parseFloat(item.minThreshold) || 0, lastRestock: item.last_restock || item.lastRestock, lastRestockBy: item.last_restock_by || item.lastRestockBy, location: item.location, notes: item.notes, createdAt: item.created_at, updatedAt: item.updated_at })); setInventory(normalizedData); } } catch (error) { console.error('Ошибка загрузки склада:', error); } }; const fetchEquipment = async () => { try { const response = await authFetch('/api/office/equipment'); if (response.ok) { const data = await response.json(); return data.map((eq: any) => ({ id: eq.id, name: eq.name || '', type: eq.type || 'other', brand: eq.brand, model: eq.model, serialNumber: eq.serial_number || eq.serialNumber || '', assignedTo: eq.assigned_to || eq.assignedTo, condition: eq.condition || 'good', notes: eq.notes })); } return []; } catch (error) { console.error('Ошибка загрузки оборудования:', error); return []; } }; const handleToggleRequestSelection = (requestId: number | string) => { const newSelected = new Set(selectedRequestsForOrder); if (newSelected.has(requestId)) { newSelected.delete(requestId); } else { newSelected.add(requestId); } setSelectedRequestsForOrder(newSelected); }; const handleSelectCategoryRequests = (category: string) => { const categoryRequests = requests .filter(req => req.status === 'new' || req.status === 'approved') .filter(req => req.category === category) .map(req => req.id); const newSelected = new Set(selectedRequestsForOrder); categoryRequests.forEach(id => newSelected.add(id)); setSelectedRequestsForOrder(newSelected); setSelectedCategory(category); }; const handleCreateOrder = async () => { if (selectedRequestsForOrder.size === 0) { alert('Выберите хотя бы одну заявку для заказа'); return; } try { const selectedRequests = requests.filter(req => selectedRequestsForOrder.has(req.id)); // Цены теперь удобно проставлять прямо в окне заказа, // поэтому на этапе создания заказа не блокируем процесс по отсутствию суммы. const totalAmount = selectedRequests.reduce((sum, req) => sum + (req.amount || 0), 0); // Создаем заказ (пока просто обновляем статусы заявок) // В будущем можно создать отдельную таблицу orders const orderNumber = `ORD-${Date.now()}`; // Обновляем статусы заявок на "ordered" и добавляем order_id for (const req of selectedRequests) { await authFetch(`/api/office/supply-requests/${req.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'ordered', ordered_at: new Date().toISOString(), notes: (req.notes || '') + `\n[Заказ: ${orderNumber}]` }) }); } // Создаем счет на оплату (через API финансов) const invoiceData = { buildingId: 'office', address: 'Офис', contractorName: 'Поставщик', serviceName: `Заказ офисных товаров ${orderNumber}`, amount: totalAmount, date: new Date().toISOString().split('T')[0], status: 'pending_approval', priority: 'medium', closingDocsReceived: false }; // Здесь можно вызвать API для создания счета // await authFetch('/api/finance/invoices', { method: 'POST', ... }); const requestIds = selectedRequests.map(req => req.id); // Создаем заказ через API const response = await authFetch('/api/office/orders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: `Заказ от ${new Date().toLocaleDateString('ru-RU')}`, description: `Заказ включает ${selectedRequests.length} позиций`, requestIds: requestIds, createdBy: CURRENT_USER_MOCK.name || 'Система' }) }); if (response.ok) { const newOrder = await response.json(); alert(`Заказ ${newOrder.order_number} успешно создан`); setSelectedRequestsForOrder(new Set()); setSelectedCategory(null); setShowCreateOrderModal(false); fetchOrders(); setView('orders'); // Переключаемся на вкладку заказов } else { const errorData = await response.json().catch(() => ({ error: 'Неизвестная ошибка' })); alert(`Ошибка создания заказа: ${errorData.error || 'Неизвестная ошибка'}`); } } catch (error: any) { console.error('Ошибка создания заказа:', error); alert(`Ошибка создания заказа: ${error.message || 'Неизвестная ошибка'}`); } }; const handleMarkAsReceived = async (requestId: number | string) => { try { const request = requests.find(r => r.id === requestId); if (!request) { alert('Заявка не найдена'); return; } // Обновляем статус заявки const response = await authFetch(`/api/office/supply-requests/${requestId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'received', received_at: new Date().toISOString() }) }); if (response.ok) { fetchRequests(); // Если это оборудование, открываем модальное окно для заполнения данных имущества if (isEquipment(request)) { handleOpenAddToEquipmentModal(request); alert('Товар получен. Заполните данные для постановки на учет в имущество.'); } else { alert('Товар получен. Сотрудник будет уведомлен.'); } } } catch (error) { console.error('Ошибка обновления статуса:', error); alert('Ошибка обновления статуса'); } }; const handleAddRequestRow = () => { setRequestItems([...requestItems, { id: Date.now().toString(), category: 'stationery', itemName: '', quantity: 1, unit: 'шт.', priority: 'medium', notes: '' }]); }; const handleRemoveRequestRow = (id: string) => { if (requestItems.length > 1) { setRequestItems(requestItems.filter(item => item.id !== id)); } }; const handleUpdateRequestItem = (id: string, field: string, value: any) => { setRequestItems(requestItems.map(item => item.id === id ? { ...item, [field]: value } : item )); }; const handleMarkAsCollected = async (requestId: number | string) => { try { const request = requests.find(r => r.id === requestId); if (!request) { alert('Заявка не найдена'); return; } // Если еще не выдано, выдаем полное количество из заявки const issuedQty = request.issuedQuantity || 0; const requestedQty = request.quantity || 0; const toIssue = requestedQty - issuedQty; // Обновляем статус заявки и количество выданного const response = await authFetch(`/api/office/supply-requests/${requestId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'collected', issued_quantity: requestedQty // Выдаем все, что было в заявке }) }); if (response.ok) { // Если это оборудование, обновляем карточку имущества - закрепляем за сотрудником if (isEquipment(request)) { try { // Извлекаем ID оборудования из примечаний const equipmentIdMatch = request.notes?.match(/\[Equipment ID: (\d+)\]/); if (equipmentIdMatch) { const equipmentId = equipmentIdMatch[1]; const equipmentResponse = await authFetch(`/api/office/equipment/${equipmentId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ assignedTo: request.requesterName }) }); if (equipmentResponse.ok) { alert('Заявка отмечена как "Забрал сотрудник". Оборудование закреплено за ' + request.requesterName); } else { alert('Заявка отмечена, но не удалось обновить карточку имущества'); } } else { // Если ID не найден, ищем по названию const equipmentListResponse = await authFetch('/api/office/equipment'); if (equipmentListResponse.ok) { const equipmentList = await equipmentListResponse.json(); const equipment = equipmentList.find((eq: any) => (eq.name || '').toLowerCase() === (request.itemName || '').toLowerCase() ); if (equipment) { await authFetch(`/api/office/equipment/${equipment.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ assignedTo: request.requesterName }) }); alert('Заявка отмечена как "Забрал сотрудник". Оборудование закреплено за ' + request.requesterName); } else { alert('Заявка отмечена как "Забрал сотрудник"'); } } else { alert('Заявка отмечена как "Забрал сотрудник"'); } } } catch (equipmentError) { console.error('Ошибка обновления карточки имущества:', equipmentError); alert('Заявка отмечена как "Забрал сотрудник"'); } } else { alert('Заявка отмечена как "Забрал сотрудник"'); } fetchRequests(); } } catch (error) { console.error('Ошибка обновления статуса:', error); alert('Ошибка обновления статуса'); } }; const handleArchiveRequest = async (requestId: number | string) => { try { const response = await authFetch(`/api/office/supply-requests/${requestId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'archived' }) }); if (response.ok) { fetchRequests(); alert('Заявка перемещена в архив'); } } catch (error) { console.error('Ошибка архивирования:', error); } }; const handleCreateRequest = async () => { try { // Проверяем, что все обязательные поля заполнены const invalidItems = requestItems.filter(item => !item.itemName || !item.itemName.trim()); if (invalidItems.length > 0) { alert('Пожалуйста, заполните наименование товара во всех строках'); return; } // Создаем все заявки const createdRequests = []; for (const item of requestItems) { if (!item.itemName || !item.itemName.trim()) continue; const response = await authFetch('/api/office/supply-requests', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ requesterName: CURRENT_USER_MOCK.name, category: item.category, itemName: item.itemName, quantity: item.quantity || 1, unit: item.unit || 'шт.', amount: 0, priority: item.priority || 'medium', notes: item.notes || null }) }); if (response.ok) { const newRequest = await response.json(); // Преобразуем snake_case в camelCase const normalizedRequest = { id: newRequest.id, requesterName: newRequest.requester_name || CURRENT_USER_MOCK.name, category: newRequest.category || item.category, itemName: newRequest.item_name || item.itemName, quantity: newRequest.quantity || item.quantity || 1, issuedQuantity: newRequest.issued_quantity || newRequest.issuedQuantity || 0, unit: newRequest.unit || item.unit || 'шт.', amount: parseFloat(newRequest.amount) || 0, priority: newRequest.priority || item.priority || 'medium', status: newRequest.status || 'new', approvedBy: newRequest.approved_by, approvedAt: newRequest.approved_at, orderedAt: newRequest.ordered_at, receivedAt: newRequest.received_at, notes: newRequest.notes, date: newRequest.created_at ? new Date(newRequest.created_at).toISOString().split('T')[0] : new Date().toISOString().split('T')[0], createdAt: newRequest.created_at, updatedAt: newRequest.updated_at }; createdRequests.push(normalizedRequest); } else { const errorData = await response.json().catch(() => ({ error: 'Неизвестная ошибка' })); alert(`Ошибка создания заявки: ${errorData.error || 'Неизвестная ошибка'}`); return; } } if (createdRequests.length > 0) { setRequests([...createdRequests, ...requests]); setShowCreateRequestModal(false); setRequestItems([{ id: '1', category: 'stationery', itemName: '', quantity: 1, unit: 'шт.', priority: 'medium', notes: '' }]); alert(`Создано заявок: ${createdRequests.length}`); } } catch (error: any) { console.error('Ошибка создания заявки:', error); alert(`Ошибка создания заявки: ${error.message || 'Неизвестная ошибка'}`); } }; const handleCheckRequest = async (request: OfficeRequest) => { // Проверка наличия на складе и возможность одобрения const requestItemName = (request.itemName || '').toLowerCase(); const inventoryItem = inventory.find(item => (item.name || '').toLowerCase().includes(requestItemName) ); if (inventoryItem) { const available = inventoryItem.quantity >= (request.quantity || 1); if (available) { alert(`Товар "${request.itemName}" есть на складе (${inventoryItem.quantity} ${inventoryItem.unit}). Можно одобрить заявку.`); } else { alert(`Товар "${request.itemName}" есть на складе, но недостаточно (требуется: ${request.quantity || 1}, есть: ${inventoryItem.quantity} ${inventoryItem.unit}).`); } } else { alert(`Товар "${request.itemName}" не найден на складе. Требуется заказ.`); } }; const handleApproveRequest = async (requestId: number | string) => { try { const response = await authFetch(`/api/office/supply-requests/${requestId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'approved', approved_by: CURRENT_USER_MOCK.name }) }); if (response.ok) { fetchRequests(); } } catch (error) { console.error('Ошибка одобрения заявки:', error); } }; const handleOpenEditModal = (request: OfficeRequest) => { setSelectedRequest(request); setEditFormData({ amount: request.amount || 0, status: request.status || 'new', priority: request.priority || 'medium', notes: request.notes || '' }); setShowEditModal(true); }; const handleSaveEdit = async () => { if (!selectedRequest) return; try { const response = await authFetch(`/api/office/supply-requests/${selectedRequest.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: editFormData.amount, status: editFormData.status, priority: editFormData.priority, notes: editFormData.notes, approved_by: editFormData.status === 'approved' ? CURRENT_USER_MOCK.name : undefined }) }); if (response.ok) { fetchRequests(); setShowEditModal(false); setSelectedRequest(null); } else { const errorData = await response.json().catch(() => ({ error: 'Неизвестная ошибка' })); alert(`Ошибка сохранения: ${errorData.error || 'Неизвестная ошибка'}`); } } catch (error: any) { console.error('Ошибка сохранения заявки:', error); alert(`Ошибка сохранения: ${error.message || 'Неизвестная ошибка'}`); } }; const handleOpenMoveToInventoryModal = (request: OfficeRequest) => { setSelectedRequest(request); const requestedQty = request.quantity || 1; const issuedQty = request.issuedQuantity || 0; const availableForWarehouse = requestedQty - issuedQty; setInventoryFormData({ quantity: Math.max(0, availableForWarehouse), location: '', notes: '' }); setShowMoveToInventoryModal(true); }; const handleMoveToInventory = async () => { if (!selectedRequest) return; try { // Создаем или обновляем запись на складе const existingItem = inventory.find(item => (item.name || '').toLowerCase() === (selectedRequest.itemName || '').toLowerCase() ); if (existingItem) { // Обновляем существующий товар const response = await authFetch(`/api/office/inventory/${existingItem.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ quantity: (existingItem.quantity || 0) + inventoryFormData.quantity, last_restock: new Date().toISOString().split('T')[0], last_restock_by: CURRENT_USER_MOCK.name, location: inventoryFormData.location || existingItem.location || null, notes: inventoryFormData.notes || existingItem.notes || null }) }); if (response.ok) { // Обновляем статус заявки на "received" await authFetch(`/api/office/supply-requests/${selectedRequest.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'received', received_at: new Date().toISOString() }) }); // Если это оборудование, создаем карточку в имуществе с закреплением за офис-менеджером if (isEquipment(selectedRequest)) { try { const equipmentResponse = await authFetch('/api/office/equipment', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: selectedRequest.itemName, type: 'other', assignedTo: CURRENT_USER_MOCK.name, purchaseDate: new Date().toISOString().split('T')[0], condition: 'good', notes: `На складе. Создано из заявки #${selectedRequest.id}. Заказчик: ${selectedRequest.requesterName}` }) }); if (equipmentResponse.ok) { const equipment = await equipmentResponse.json(); // Сохраняем ID оборудования в примечаниях заявки await authFetch(`/api/office/supply-requests/${selectedRequest.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ notes: `${selectedRequest.notes || ''}\n[Equipment ID: ${equipment.id}]`.trim() }) }); } } catch (equipmentError) { console.error('Ошибка создания карточки имущества:', equipmentError); } } fetchRequests(); fetchInventory(); setShowMoveToInventoryModal(false); setSelectedRequest(null); alert('Товар успешно добавлен на склад' + (isEquipment(selectedRequest) ? '. Карточка имущества создана с закреплением за офис-менеджером.' : '')); } } else { // Создаем новый товар const response = await authFetch('/api/office/inventory', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: selectedRequest.itemName, category: selectedRequest.category || 'other', quantity: inventoryFormData.quantity, unit: selectedRequest.unit || 'шт.', minThreshold: 0, location: inventoryFormData.location || null, notes: inventoryFormData.notes || null }) }); if (response.ok) { // Обновляем статус заявки на "received" await authFetch(`/api/office/supply-requests/${selectedRequest.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'received', received_at: new Date().toISOString() }) }); // Если это оборудование, создаем карточку в имуществе с закреплением за офис-менеджером if (isEquipment(selectedRequest)) { try { const equipmentResponse = await authFetch('/api/office/equipment', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: selectedRequest.itemName, type: 'other', assignedTo: CURRENT_USER_MOCK.name, purchaseDate: new Date().toISOString().split('T')[0], condition: 'good', notes: `На складе. Создано из заявки #${selectedRequest.id}. Заказчик: ${selectedRequest.requesterName}` }) }); if (equipmentResponse.ok) { const equipment = await equipmentResponse.json(); // Сохраняем ID оборудования в примечаниях заявки await authFetch(`/api/office/supply-requests/${selectedRequest.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ notes: `${selectedRequest.notes || ''}\n[Equipment ID: ${equipment.id}]`.trim() }) }); } } catch (equipmentError) { console.error('Ошибка создания карточки имущества:', equipmentError); } } fetchRequests(); fetchInventory(); setShowMoveToInventoryModal(false); setSelectedRequest(null); alert('Товар успешно добавлен на склад' + (isEquipment(selectedRequest) ? '. Карточка имущества создана с закреплением за офис-менеджером.' : '')); } } } catch (error: any) { console.error('Ошибка переноса на склад:', error); alert(`Ошибка: ${error.message || 'Неизвестная ошибка'}`); } }; const handleOpenAddToEquipmentModal = (request: OfficeRequest) => { setSelectedRequest(request); setEquipmentFormData({ type: 'other', brand: '', model: '', serialNumber: '', assignedTo: request.requesterName || '', // Предзаполняем заказчика purchaseDate: new Date().toISOString().split('T')[0], warrantyUntil: '', condition: 'good', notes: `Создано из заявки #${request.id}. Заказчик: ${request.requesterName}` }); setShowAddToEquipmentModal(true); }; const handleAddToEquipment = async () => { if (!selectedRequest) return; // Проверяем обязательные поля if (!equipmentFormData.serialNumber || !equipmentFormData.serialNumber.trim()) { alert('Пожалуйста, укажите серийный номер'); return; } try { const response = await authFetch('/api/office/equipment', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: selectedRequest.itemName, type: equipmentFormData.type, brand: equipmentFormData.brand || null, model: equipmentFormData.model || null, serialNumber: equipmentFormData.serialNumber.trim(), assignedTo: equipmentFormData.assignedTo || null, purchaseDate: equipmentFormData.purchaseDate || null, warrantyUntil: equipmentFormData.warrantyUntil || null, condition: equipmentFormData.condition, notes: equipmentFormData.notes || null }) }); if (response.ok) { const equipment = await response.json(); // Сохраняем ID оборудования в примечаниях заявки await authFetch(`/api/office/supply-requests/${selectedRequest.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ notes: `${selectedRequest.notes || ''}\n[Equipment ID: ${equipment.id}]`.trim() }) }); fetchRequests(); setShowAddToEquipmentModal(false); setSelectedRequest(null); alert('Оборудование успешно добавлено в имущество'); } else { const errorData = await response.json().catch(() => ({ error: 'Неизвестная ошибка' })); alert(`Ошибка добавления: ${errorData.error || 'Неизвестная ошибка'}`); } } catch (error: any) { console.error('Ошибка добавления в имущество:', error); alert(`Ошибка: ${error.message || 'Неизвестная ошибка'}`); } }; const filteredRequests = requests.filter(req => { // Исключаем архивные заявки из основного списка if (req.status === 'archived' || req.status === 'collected') { return false; } const itemName = (req.itemName || '').toLowerCase(); const requesterName = (req.requesterName || '').toLowerCase(); const searchLower = search.toLowerCase(); return itemName.includes(searchLower) || requesterName.includes(searchLower); }); const filteredInventory = inventory.filter(item => item.name.toLowerCase().includes(search.toLowerCase()) ); const getStatusLabel = (status: string) => { const labels = { new: 'Ожидание', approved: 'Одобрено', ordered: 'Заказано', received: 'На месте', canceled: 'Отменено', archived: 'В архиве', collected: 'Забрал сотрудник' }; return labels[status as keyof typeof labels] || status; }; return (
{/* Sub-tabs toggle */}
{/* Header with Create button */}
setSearch(e.target.value)} className="w-full pl-10 pr-4 py-2.5 rounded-xl border border-slate-200 focus:ring-2 focus:ring-primary-500 outline-none text-sm shadow-sm" />
{view === 'requests' && (
)} {view === 'orders' && (
)}
{view === 'orders' ? (
{loading ? (
Загрузка...
) : orders && orders.length > 0 ? ( orders .filter((order: any) => { const searchLower = search.toLowerCase(); return (order.order_number || '').toLowerCase().includes(searchLower) || (order.title || '').toLowerCase().includes(searchLower) || (order.supplier_name || '').toLowerCase().includes(searchLower); }) .map((order: any) => ( )) ) : (
Нет заказов
)}
) : view === 'requests' ? (
{/* Фильтры по категориям для оформления заказа */} {showCreateOrderModal && (

Выберите категорию для быстрого выбора:

{['stationery', 'household', 'food', 'equipment', 'other'].map(cat => { const labels: Record = { stationery: 'Канцтовары', household: 'Хоз. нужды', food: 'Продукты', equipment: 'Оборудование', other: 'Другое' }; const count = requests.filter(r => (r.status === 'new' || r.status === 'approved') && r.category === cat).length; return ( ); })}
)} {loading ? (
Загрузка...
) : filteredRequests && filteredRequests.length > 0 ? ( filteredRequests.map(req => { if (!req || !req.id) return null; const isSelected = selectedRequestsForOrder.has(req.id); const canSelect = showCreateOrderModal && (req.status === 'new' || req.status === 'approved'); return (
{canSelect && (
)}
{req.priority === 'urgent' ? 'Срочно' : req.priority === 'high' ? 'Высокий' : req.priority === 'medium' ? 'Средний' : 'Низкий'} {req.createdAt ? new Date(req.createdAt).toLocaleDateString('ru-RU') : req.date}

{req.itemName}

Заказчик: {req.requesterName}

{req.quantity && req.unit && (

Количество: {req.quantity} {req.unit}

)}

{req.amount.toLocaleString()} ₽

{getStatusLabel(req.status)}
{req.status === 'new' && ( )} {req.status === 'ordered' && ( )} {req.status === 'received' && ( <> )} {(req.status === 'collected' || req.status === 'archived') && ( )} {(req.status === 'ordered' || req.status === 'approved' || req.status === 'received') && ( <> {(req.category === 'equipment' || req.itemName?.toLowerCase().includes('компьютер') || req.itemName?.toLowerCase().includes('ноутбук') || req.itemName?.toLowerCase().includes('микроволновка') || req.itemName?.toLowerCase().includes('принтер') || req.itemName?.toLowerCase().includes('монитор') || req.itemName?.toLowerCase().includes('кондиционер') || req.itemName?.toLowerCase().includes('телефон') || req.itemName?.toLowerCase().includes('сканер') || req.itemName?.toLowerCase().includes('кондер')) && ( )} )}
); }).filter(Boolean) ) : (
Нет заявок
)}
) : (
{filteredInventory && filteredInventory.length > 0 ? filteredInventory.map(item => { if (!item || !item.id) return null; const isLow = (item.quantity || 0) <= (item.minThreshold || 0); // Находим заявки, которые можно выдать со склада const availableRequests = requests.filter(req => (req.status === 'approved' || req.status === 'ordered') && req.itemName?.toLowerCase() === item.name?.toLowerCase() && (req.issuedQuantity || 0) < (req.quantity || 0) ); return ( ); }).filter(Boolean) : ( )}
Наименование Наличие Действия

{item.name}

{item.lastRestock ? `Обновлено: ${item.lastRestock}` : 'Не обновлялось'}

{item.quantity} {item.unit} {isLow && Критично}
{availableRequests.length > 0 && ( )}
Нет товаров на складе
)} {/* Create Order Modal - Select Requests */} {showCreateOrderModal && (

Оформить заказ

Выберите заявки для объединения в заказ

{/* Quick category selection */}

Быстрый выбор по категориям:

{['stationery', 'household', 'food', 'equipment', 'other'].map(cat => { const labels: Record = { stationery: 'Канцтовары', household: 'Хоз. нужды', food: 'Продукты', equipment: 'Оборудование', other: 'Другое' }; const availableRequests = requests.filter(r => (r.status === 'new' || r.status === 'approved') && r.category === cat); const selectedCount = availableRequests.filter(r => selectedRequestsForOrder.has(r.id)).length; return ( ); })}
{/* Selected requests list */}
{requests .filter(req => req.status === 'new' || req.status === 'approved') .map(req => { const isSelected = selectedRequestsForOrder.has(req.id); const categoryLabels: Record = { stationery: 'Канцтовары', household: 'Хоз. нужды', food: 'Продукты', equipment: 'Оборудование', other: 'Другое' }; return (
handleToggleRequestSelection(req.id)} className={`p-4 cursor-pointer transition-colors ${ isSelected ? 'bg-primary-50 border-l-4 border-primary-500' : 'hover:bg-slate-50' }`} >
{categoryLabels[req.category] || req.category} {req.requesterName}

{req.itemName}

{req.quantity} {req.unit} {req.amount.toLocaleString()} ₽
); })} {requests.filter(req => req.status === 'new' || req.status === 'approved').length === 0 && (

Нет доступных заявок для оформления заказа

)}
{/* Summary and actions */}

Выбрано заявок: {selectedRequestsForOrder.size}

Итого: {requests .filter(req => selectedRequestsForOrder.has(req.id)) .reduce((sum, req) => sum + (req.amount || 0), 0) .toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ₽

)} {/* Create Request Modal - Table */} {showCreateRequestModal && (

Оставить заявку на ТМЦ

Добавьте позиции и оставьте заявку (цены укажет офис-менеджер)

{requestItems.map((item) => ( ))}
Категория Наименование Кол-во Ед. Приоритет Примечания
handleUpdateRequestItem(item.id, 'itemName', e.target.value)} className="w-full px-2 py-1.5 border border-slate-300 rounded text-xs focus:ring-2 focus:ring-primary-500 outline-none" placeholder="Наименование товара" /> { const qty = parseInt(e.target.value) || 1; handleUpdateRequestItem(item.id, 'quantity', qty); }} className="w-20 px-2 py-1.5 border border-slate-300 rounded text-xs text-center focus:ring-2 focus:ring-primary-500 outline-none" min="1" /> handleUpdateRequestItem(item.id, 'unit', e.target.value)} className="w-16 px-2 py-1.5 border border-slate-300 rounded text-xs text-center focus:ring-2 focus:ring-primary-500 outline-none" placeholder="шт." /> handleUpdateRequestItem(item.id, 'notes', e.target.value)} className="w-full px-2 py-1.5 border border-slate-300 rounded text-xs focus:ring-2 focus:ring-primary-500 outline-none" placeholder="Доп. информация" /> {requestItems.length > 1 && ( )}
Всего позиций: {requestItems.length}
)} {/* Edit Request Modal */} {showEditModal && selectedRequest && (

Редактировать заявку

setEditFormData({ ...editFormData, amount: parseFloat(e.target.value) || 0 })} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" min="0" step="0.01" />