diff --git a/frontend/src/App.css b/frontend/src/App.css index 9546a96..2e95ffd 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -27,6 +27,46 @@ margin: 0 auto; } +/* Notifications */ +.notifications { + position: fixed; + top: 20px; + right: 20px; + z-index: 1000; + display: flex; + flex-direction: column; + gap: 10px; + max-width: 400px; +} + +.notification { + padding: 12px 20px; + border-radius: 8px; + color: white; + font-weight: 500; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + animation: slideIn 0.3s ease-out; +} + +.notification-success { + background: #22c55e; +} + +.notification-error { + background: #ef4444; +} + +@keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + .add-section { background: white; border-radius: 12px; @@ -84,14 +124,38 @@ font-size: 1.25rem; } -.empty-message { +.empty-state { background: rgba(255, 255, 255, 0.2); - padding: 20px; - border-radius: 8px; + padding: 40px 20px; + border-radius: 12px; text-align: center; color: white; } +.empty-icon { + font-size: 3rem; + margin: 0 0 16px; +} + +.empty-message { + font-size: 1.2rem; + margin: 0 0 8px; +} + +.empty-hint { + opacity: 0.8; + margin: 0; +} + +.loading { + background: rgba(255, 255, 255, 0.2); + padding: 40px; + border-radius: 12px; + text-align: center; + color: white; + font-size: 1.2rem; +} + .employees-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); @@ -103,18 +167,23 @@ border-radius: 12px; padding: 20px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - transition: transform 0.2s; + transition: transform 0.2s, box-shadow 0.2s; } .employee-card:hover { transform: translateY(-2px); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15); } .card-header { display: flex; justify-content: space-between; - align-items: center; - margin-bottom: 8px; + align-items: flex-start; + margin-bottom: 12px; +} + +.employee-info { + flex: 1; } .employee-name { @@ -123,6 +192,12 @@ color: #1f2937; } +.employee-position { + margin: 4px 0 0; + color: #6b7280; + font-size: 0.95rem; +} + .btn-delete { background: #fee2e2; color: #ef4444; @@ -136,22 +211,18 @@ align-items: center; justify-content: center; transition: background 0.2s; + flex-shrink: 0; } .btn-delete:hover { background: #fecaca; } -.employee-position { - margin: 0 0 16px; - color: #6b7280; - font-size: 0.95rem; -} - .status-section { display: flex; align-items: center; gap: 12px; + flex-wrap: wrap; } .status-badge { @@ -160,12 +231,12 @@ color: white; font-size: 0.85rem; font-weight: 600; - min-width: 100px; - text-align: center; + white-space: nowrap; } .status-select { flex: 1; + min-width: 150px; padding: 8px 12px; border: 2px solid #e5e7eb; border-radius: 6px; diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 758cc5b..81f0d14 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useCallback } from 'react' import axios from 'axios' import './App.css' @@ -6,38 +6,69 @@ const API_URL = 'http://localhost:8000/api' const STATUS_OPTIONS = ['Online', 'На встрече', 'Offline', 'В отпуске', 'Болеет'] +const STATUS_CONFIG = { + 'Online': { color: '#22c55e', icon: '🟢' }, + 'На встрече': { color: '#f59e0b', icon: '🟡' }, + 'Offline': { color: '#6b7280', icon: '⚫' }, + 'В отпуске': { color: '#3b82f6', icon: '🔵' }, + 'Болеет': { color: '#ef4444', icon: '🔴' } +} + function App() { const [employees, setEmployees] = useState([]) const [newName, setNewName] = useState('') const [newPosition, setNewPosition] = useState('') + const [loading, setLoading] = useState(true) + const [notifications, setNotifications] = useState([]) - const fetchEmployees = async () => { + const addNotification = useCallback((message, type = 'success') => { + const id = Date.now() + setNotifications(prev => [...prev, { id, message, type }]) + setTimeout(() => { + setNotifications(prev => prev.filter(n => n.id !== id)) + }, 3000) + }, []) + + const showError = useCallback((error, context) => { + const message = error.response?.data?.detail || `Ошибка: ${context}` + addNotification(message, 'error') + }, [addNotification]) + + const fetchEmployees = useCallback(async () => { try { const response = await axios.get(`${API_URL}/employees`) setEmployees(response.data) + setLoading(false) } catch (error) { console.error('Error fetching employees:', error) + showError(error, 'Не удалось загрузить список сотрудников') + setLoading(false) } - } + }, [showError]) useEffect(() => { fetchEmployees() - }, []) + }, [fetchEmployees]) const handleAddEmployee = async (e) => { e.preventDefault() - if (!newName.trim() || !newPosition.trim()) return + if (!newName.trim() || !newPosition.trim()) { + addNotification('Заполните все поля', 'error') + return + } try { await axios.post(`${API_URL}/employees`, { - name: newName, - position: newPosition + name: newName.trim(), + position: newPosition.trim() }) setNewName('') setNewPosition('') + addNotification(`Сотрудник ${newName} добавлен`) fetchEmployees() } catch (error) { console.error('Error adding employee:', error) + showError(error, 'Не удалось добавить сотрудника') } } @@ -46,44 +77,48 @@ function App() { await axios.put(`${API_URL}/employees/${employeeId}/status`, { status: newStatus }) + const employee = employees.find(e => e.id === employeeId) + addNotification(`Статус ${employee?.name} изменен на "${newStatus}"`) fetchEmployees() } catch (error) { console.error('Error updating status:', error) + showError(error, 'Не удалось обновить статус') } } const handleDeleteEmployee = async (employeeId) => { - if (!confirm('Вы уверены, что хотите удалить этого сотрудника?')) return + const employee = employees.find(e => e.id === employeeId) + if (!confirm(`Вы уверены, что хотите удалить ${employee?.name}?`)) return try { await axios.delete(`${API_URL}/employees/${employeeId}`) + addNotification(`Сотрудник ${employee?.name} удален`) fetchEmployees() } catch (error) { console.error('Error deleting employee:', error) + showError(error, 'Не удалось удалить сотрудника') } } - const getStatusColor = (status) => { - switch (status) { - case 'Online': return '#22c55e' - case 'На встрече': return '#f59e0b' - case 'Offline': return '#6b7280' - case 'В отпуске': return '#3b82f6' - case 'Болеет': return '#ef4444' - default: return '#6b7280' + const getStatusStyle = (status) => { + const config = STATUS_CONFIG[status] || STATUS_CONFIG['Offline'] + return { + backgroundColor: config.color, } } return (
Панель управления статусами команды