From d3a9f9360b48aee9a292739dd450d20db8e39a9d Mon Sep 17 00:00:00 2001 From: Damir Date: Thu, 2 Apr 2026 03:22:17 +0300 Subject: [PATCH] Initial commit: Team Status Board with FastAPI + React --- .gitignore | 44 ++++++++++ .qwen/settings.json | 12 +++ .qwen/settings.json.orig | 7 ++ README.md | 60 +++++++++++++ backend/main.py | 133 +++++++++++++++++++++++++++++ backend/requirements.txt | 4 + frontend/index.html | 12 +++ frontend/package.json | 22 +++++ frontend/src/App.css | 180 +++++++++++++++++++++++++++++++++++++++ frontend/src/App.jsx | 154 +++++++++++++++++++++++++++++++++ frontend/src/index.css | 11 +++ frontend/src/main.jsx | 10 +++ frontend/vite.config.js | 10 +++ start.sh | 85 ++++++++++++++++++ 14 files changed, 744 insertions(+) create mode 100644 .gitignore create mode 100644 .qwen/settings.json create mode 100644 .qwen/settings.json.orig create mode 100644 README.md create mode 100644 backend/main.py create mode 100644 backend/requirements.txt create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.jsx create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.jsx create mode 100644 frontend/vite.config.js create mode 100644 start.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..232c2ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# Python +venv/ +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +env.bak/ +.venv/ + +# Node +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Database +*.db +*.sqlite +*.sqlite3 + +# Environment +.env +.env.local +.env.*.local + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Build +dist/ +build/ +*.egg-info/ + +# Logs +*.log diff --git a/.qwen/settings.json b/.qwen/settings.json new file mode 100644 index 0000000..1129bff --- /dev/null +++ b/.qwen/settings.json @@ -0,0 +1,12 @@ +{ + "permissions": { + "allow": [ + "Bash(mkdir *)", + "Bash(powershell *)", + "Bash(chmod *)", + "Bash(git init)", + "Bash(git add *)" + ] + }, + "$version": 3 +} \ No newline at end of file diff --git a/.qwen/settings.json.orig b/.qwen/settings.json.orig new file mode 100644 index 0000000..9a9e4cc --- /dev/null +++ b/.qwen/settings.json.orig @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(mkdir *)" + ] + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..548cc01 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# Team Status Board + +Панель управления статусами команды. + +## Стек + +- **Бэкенд:** Python, FastAPI, SQLAlchemy, SQLite +- **Фронтенд:** React, Vite +- **API:** REST + +## Быстрый старт + +```bash +git clone +cd "Team status board" +./start.sh +``` + +Приложение будет доступно: +- Фронтенд: http://localhost:3000 +- Бэкенд API: http://localhost:8000 + +## API Endpoints + +| Метод | Endpoint | Описание | +|-------|----------|----------| +| GET | `/api/employees` | Получить всех сотрудников | +| POST | `/api/employees` | Добавить сотрудника | +| PUT | `/api/employees/{id}/status` | Обновить статус | +| DELETE | `/api/employees/{id}` | Удалить сотрудника | + +## Структура проекта + +``` +. +├── backend/ +│ ├── main.py # FastAPI приложение +│ ├── requirements.txt # Python зависимости +│ └── team_status.db # SQLite база данных (создается автоматически) +├── frontend/ +│ ├── src/ +│ │ ├── App.jsx # Основной компонент React +│ │ ├── App.css # Стили +│ │ ├── main.jsx # Точка входа +│ │ └── index.css # Глобальные стили +│ ├── index.html +│ ├── package.json +│ └── vite.config.js +├── .gitignore +├── start.sh # Скрипт автозапуска +└── README.md +``` + +## Статусы + +- Online +- На встрече +- Offline +- В отпуске +- Болеет diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..66ebdff --- /dev/null +++ b/backend/main.py @@ -0,0 +1,133 @@ +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +from sqlalchemy import create_engine, Column, Integer, String +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker, Session +from typing import List, Optional + +# Database setup +SQLALCHEMY_DATABASE_URL = "sqlite:///./team_status.db" +engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() + + +# SQLAlchemy Model +class Employee(Base): + __tablename__ = "employees" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String, nullable=False) + position = Column(String, nullable=False) + status = Column(String, default="Online") + + +# Create tables +Base.metadata.create_all(bind=engine) + + +# Pydantic Models +class EmployeeBase(BaseModel): + name: str + position: str + + +class EmployeeCreate(EmployeeBase): + pass + + +class EmployeeResponse(EmployeeBase): + id: int + status: str + + class Config: + from_attributes = True + + +class StatusUpdate(BaseModel): + status: str + + +# FastAPI app +app = FastAPI(title="Team Status Board API") + +# CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +# Dependency +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + + +# CRUD Operations +@app.get("/api/employees", response_model=List[EmployeeResponse]) +def read_employees(): + """Get all employees""" + db = SessionLocal() + try: + employees = db.query(Employee).all() + return employees + finally: + db.close() + + +@app.post("/api/employees", response_model=EmployeeResponse) +def create_employee(employee: EmployeeCreate): + """Create a new employee""" + db = SessionLocal() + try: + db_employee = Employee(**employee.model_dump(), status="Online") + db.add(db_employee) + db.commit() + db.refresh(db_employee) + return db_employee + finally: + db.close() + + +@app.put("/api/employees/{employee_id}/status", response_model=EmployeeResponse) +def update_employee_status(employee_id: int, status_update: StatusUpdate): + """Update employee status""" + db = SessionLocal() + try: + employee = db.query(Employee).filter(Employee.id == employee_id).first() + if not employee: + raise HTTPException(status_code=404, detail="Employee not found") + employee.status = status_update.status + db.commit() + db.refresh(employee) + return employee + finally: + db.close() + + +@app.delete("/api/employees/{employee_id}") +def delete_employee(employee_id: int): + """Delete an employee""" + db = SessionLocal() + try: + employee = db.query(Employee).filter(Employee.id == employee_id).first() + if not employee: + raise HTTPException(status_code=404, detail="Employee not found") + db.delete(employee) + db.commit() + return {"message": "Employee deleted successfully"} + finally: + db.close() + + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..b71204e --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,4 @@ +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +sqlalchemy==2.0.23 +pydantic==2.5.2 diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..e703507 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + Team Status Board + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..6815940 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,22 @@ +{ + "name": "team-status-board-frontend", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "axios": "^1.6.2" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "vite": "^5.0.8" + } +} diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..9546a96 --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,180 @@ +.app { + min-height: 100vh; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 20px; +} + +.header { + text-align: center; + color: white; + margin-bottom: 30px; +} + +.header h1 { + font-size: 2.5rem; + margin: 0; + font-weight: 700; +} + +.subtitle { + margin: 10px 0 0; + opacity: 0.9; + font-size: 1.1rem; +} + +.main { + max-width: 1200px; + margin: 0 auto; +} + +.add-section { + background: white; + border-radius: 12px; + padding: 24px; + margin-bottom: 30px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.add-section h2 { + margin: 0 0 16px; + color: #1f2937; + font-size: 1.25rem; +} + +.add-form { + display: flex; + gap: 12px; + flex-wrap: wrap; +} + +.input { + flex: 1; + min-width: 200px; + padding: 12px 16px; + border: 2px solid #e5e7eb; + border-radius: 8px; + font-size: 1rem; + transition: border-color 0.2s; +} + +.input:focus { + outline: none; + border-color: #667eea; +} + +.btn-add { + padding: 12px 24px; + background: #667eea; + color: white; + border: none; + border-radius: 8px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: background 0.2s; +} + +.btn-add:hover { + background: #5568d3; +} + +.employees-section h2 { + color: white; + margin: 0 0 16px; + font-size: 1.25rem; +} + +.empty-message { + background: rgba(255, 255, 255, 0.2); + padding: 20px; + border-radius: 8px; + text-align: center; + color: white; +} + +.employees-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 16px; +} + +.employee-card { + background: white; + border-radius: 12px; + padding: 20px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + transition: transform 0.2s; +} + +.employee-card:hover { + transform: translateY(-2px); +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.employee-name { + margin: 0; + font-size: 1.25rem; + color: #1f2937; +} + +.btn-delete { + background: #fee2e2; + color: #ef4444; + border: none; + border-radius: 6px; + width: 28px; + height: 28px; + cursor: pointer; + font-size: 1rem; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s; +} + +.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; +} + +.status-badge { + padding: 6px 12px; + border-radius: 20px; + color: white; + font-size: 0.85rem; + font-weight: 600; + min-width: 100px; + text-align: center; +} + +.status-select { + flex: 1; + padding: 8px 12px; + border: 2px solid #e5e7eb; + border-radius: 6px; + font-size: 0.9rem; + cursor: pointer; + background: white; +} + +.status-select:focus { + outline: none; + border-color: #667eea; +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..758cc5b --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,154 @@ +import { useState, useEffect } from 'react' +import axios from 'axios' +import './App.css' + +const API_URL = 'http://localhost:8000/api' + +const STATUS_OPTIONS = ['Online', 'На встрече', 'Offline', 'В отпуске', 'Болеет'] + +function App() { + const [employees, setEmployees] = useState([]) + const [newName, setNewName] = useState('') + const [newPosition, setNewPosition] = useState('') + + const fetchEmployees = async () => { + try { + const response = await axios.get(`${API_URL}/employees`) + setEmployees(response.data) + } catch (error) { + console.error('Error fetching employees:', error) + } + } + + useEffect(() => { + fetchEmployees() + }, []) + + const handleAddEmployee = async (e) => { + e.preventDefault() + if (!newName.trim() || !newPosition.trim()) return + + try { + await axios.post(`${API_URL}/employees`, { + name: newName, + position: newPosition + }) + setNewName('') + setNewPosition('') + fetchEmployees() + } catch (error) { + console.error('Error adding employee:', error) + } + } + + const handleStatusChange = async (employeeId, newStatus) => { + try { + await axios.put(`${API_URL}/employees/${employeeId}/status`, { + status: newStatus + }) + fetchEmployees() + } catch (error) { + console.error('Error updating status:', error) + } + } + + const handleDeleteEmployee = async (employeeId) => { + if (!confirm('Вы уверены, что хотите удалить этого сотрудника?')) return + + try { + await axios.delete(`${API_URL}/employees/${employeeId}`) + fetchEmployees() + } catch (error) { + console.error('Error deleting employee:', 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' + } + } + + return ( +
+
+

Team Status Board

+

Панель управления статусами команды

+
+ +
+
+

Добавить сотрудника

+
+ setNewName(e.target.value)} + className="input" + /> + setNewPosition(e.target.value)} + className="input" + /> + +
+
+ +
+

Сотрудники ({employees.length})

+ {employees.length === 0 ? ( +

Список сотрудников пуст

+ ) : ( +
+ {employees.map((employee) => ( +
+
+

{employee.name}

+ +
+

{employee.position}

+
+ + {employee.status} + + +
+
+ ))} +
+ )} +
+
+
+ ) +} + +export default App diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..00f6d57 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,11 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..54b39dd --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.jsx' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + , +) diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..80d8467 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + server: { + port: 3000, + host: true + } +}) diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..ac7dc02 --- /dev/null +++ b/start.sh @@ -0,0 +1,85 @@ +#!/bin/bash + +set -e + +echo "🚀 Team Status Board - Запуск приложения" +echo "=========================================" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# Функция проверки наличия команды +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Проверка Python +if ! command_exists python3; then + echo "❌ Python3 не найден. Установите Python 3.8+" + exit 1 +fi +echo "✓ Python найден: $(python3 --version)" + +# Проверка Node.js +if ! command_exists node; then + echo "❌ Node.js не найден. Установите Node.js 18+" + exit 1 +fi +echo "✓ Node.js найден: $(node --version)" + +# Создание и активация venv для бэкенда +if [ ! -d "backend/venv" ]; then + echo "📦 Создание виртуального окружения Python..." + python3 -m venv backend/venv +fi +echo "✓ Виртуальное окружение готово" + +# Установка зависимостей Python +echo "📦 Установка Python зависимостей..." +source backend/venv/bin/activate +pip install --quiet -r backend/requirements.txt +echo "✓ Python зависимости установлены" + +# Установка зависимостей Node.js +echo "📦 Установка Node.js зависимостей..." +cd frontend +if [ ! -d "node_modules" ]; then + npm install --silent +fi +echo "✓ Node.js зависимости установлены" +cd .. + +# Запуск бэкенда в фоне +echo "🔙 Запуск бэкенда (FastAPI)..." +source backend/venv/bin/activate +cd backend +python main.py & +BACKEND_PID=$! +cd .. + +# Ожидание запуска бэкенда +echo "⏳ Ожидание запуска бэкенда..." +sleep 3 + +# Запуск фронтенда +echo "🎨 Запуск фронтенда (React + Vite)..." +cd frontend +npm run dev & +FRONTEND_PID=$! +cd .. + +echo "" +echo "=========================================" +echo "✅ Приложение запущено!" +echo "" +echo "Бэкенд API: http://localhost:8000" +echo "Frontend: http://localhost:3000" +echo "" +echo "PID бэкенда: $BACKEND_PID" +echo "PID фронтенда: $FRONTEND_PID" +echo "" +echo "Для остановки нажмите Ctrl+C" +echo "=========================================" + +# Ожидание завершения +wait