diff --git a/VERSION b/VERSION index 6a6a3d8..24ba9a3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.6.1 +2.7.0 diff --git a/play-life-backend/main.go b/play-life-backend/main.go index 79d9024..b533c96 100644 --- a/play-life-backend/main.go +++ b/play-life-backend/main.go @@ -3569,6 +3569,7 @@ func main() { protected.HandleFunc("/project/priority", app.setProjectPriorityHandler).Methods("POST", "OPTIONS") protected.HandleFunc("/project/move", app.moveProjectHandler).Methods("POST", "OPTIONS") protected.HandleFunc("/project/delete", app.deleteProjectHandler).Methods("POST", "OPTIONS") + protected.HandleFunc("/project/create", app.createProjectHandler).Methods("POST", "OPTIONS") protected.HandleFunc("/d2dc349a-0d13-49b2-a8f0-1ab094bfba9b", app.getFullStatisticsHandler).Methods("GET", "OPTIONS") // Integrations @@ -4974,6 +4975,10 @@ type ProjectDeleteRequest struct { ID int `json:"id"` } +type ProjectCreateRequest struct { + Name string `json:"name"` +} + func (a *App) moveProjectHandler(w http.ResponseWriter, r *http.Request) { if r.Method == "OPTIONS" { setCORSHeaders(w) @@ -5207,6 +5212,71 @@ func (a *App) deleteProjectHandler(w http.ResponseWriter, r *http.Request) { }) } +func (a *App) createProjectHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == "OPTIONS" { + setCORSHeaders(w) + w.WriteHeader(http.StatusOK) + return + } + setCORSHeaders(w) + + userID, ok := getUserIDFromContext(r) + if !ok { + sendErrorWithCORS(w, "Unauthorized", http.StatusUnauthorized) + return + } + + var req ProjectCreateRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + log.Printf("Error decoding create project request: %v", err) + sendErrorWithCORS(w, "Invalid request body", http.StatusBadRequest) + return + } + + if req.Name == "" { + sendErrorWithCORS(w, "name is required", http.StatusBadRequest) + return + } + + // Проверяем, существует ли уже проект с таким именем + var existingID int + err := a.DB.QueryRow(` + SELECT id FROM projects + WHERE name = $1 AND user_id = $2 AND deleted = FALSE + `, req.Name, userID).Scan(&existingID) + + if err == nil { + // Проект уже существует + sendErrorWithCORS(w, "Project with this name already exists", http.StatusConflict) + return + } else if err != sql.ErrNoRows { + log.Printf("Error checking project existence: %v", err) + sendErrorWithCORS(w, fmt.Sprintf("Error checking project existence: %v", err), http.StatusInternalServerError) + return + } + + // Создаем новый проект + var projectID int + err = a.DB.QueryRow(` + INSERT INTO projects (name, deleted, user_id) + VALUES ($1, FALSE, $2) + RETURNING id + `, req.Name, userID).Scan(&projectID) + + if err != nil { + log.Printf("Error creating project: %v", err) + sendErrorWithCORS(w, fmt.Sprintf("Error creating project: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "message": "Project created successfully", + "project_id": projectID, + "project_name": req.Name, + }) +} + func (a *App) todoistWebhookHandler(w http.ResponseWriter, r *http.Request) { // Логирование входящего запроса log.Printf("=== Todoist Webhook Request ===") diff --git a/play-life-web/src/components/ProjectPriorityManager.jsx b/play-life-web/src/components/ProjectPriorityManager.jsx index ab15348..b3b9232 100644 --- a/play-life-web/src/components/ProjectPriorityManager.jsx +++ b/play-life-web/src/components/ProjectPriorityManager.jsx @@ -25,6 +25,104 @@ import { useAuth } from './auth/AuthContext' const PROJECTS_API_URL = '/projects' const PRIORITY_UPDATE_API_URL = '/project/priority' const PROJECT_MOVE_API_URL = '/project/move' +const PROJECT_CREATE_API_URL = '/project/create' + +// Компонент экрана добавления проекта +function AddProjectScreen({ onClose, onSuccess }) { + const { authFetch } = useAuth() + const [projectName, setProjectName] = useState('') + const [isSubmitting, setIsSubmitting] = useState(false) + const [error, setError] = useState(null) + + const handleSubmit = async () => { + if (!projectName.trim()) { + setError('Введите название проекта') + return + } + + setIsSubmitting(true) + setError(null) + + try { + const response = await authFetch(PROJECT_CREATE_API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: projectName.trim(), + }), + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(errorText || 'Ошибка при создании проекта') + } + + onSuccess() + } catch (err) { + console.error('Ошибка создания проекта:', err) + setError(err.message || 'Ошибка при создании проекта') + } finally { + setIsSubmitting(false) + } + } + + return ( +
+
+ {/* Заголовок с кнопкой закрытия */} +
+ +
+ + {/* Контент */} +
+ {/* Поле ввода */} +
+ + setProjectName(e.target.value)} + onKeyPress={(e) => { + if (e.key === 'Enter' && projectName.trim() && !isSubmitting) { + handleSubmit() + } + }} + placeholder="Введите название проекта" + className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" + autoFocus + /> + {error && ( +
{error}
+ )} +
+
+ + {/* Кнопка подтверждения (прибита к низу) */} +
+ +
+
+
+ ) +} // Компонент экрана переноса проекта function MoveProjectScreen({ project, allProjects, onClose, onSuccess }) { @@ -241,7 +339,7 @@ function DroppableSlot({ containerId, isEmpty, maxItems, currentCount }) { } // Компонент для слота приоритета -function PrioritySlot({ title, projects, allProjects, onMenuClick, maxItems = null, containerId }) { +function PrioritySlot({ title, projects, allProjects, onMenuClick, maxItems = null, containerId, onAddClick }) { return (
{title}
@@ -258,6 +356,14 @@ function PrioritySlot({ title, projects, allProjects, onMenuClick, maxItems = nu onMenuClick={onMenuClick} /> ))} + {onAddClick && containerId === 'low' && ( + + )}
) @@ -289,6 +395,7 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad, const [activeId, setActiveId] = useState(null) const [selectedProject, setSelectedProject] = useState(null) // Для модального окна const [showMoveScreen, setShowMoveScreen] = useState(false) // Для экрана переноса + const [showAddScreen, setShowAddScreen] = useState(false) // Для экрана добавления const scrollContainerRef = useRef(null) @@ -829,6 +936,7 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad, allProjects={allProjects} onMenuClick={handleMenuClick} containerId="low" + onAddClick={() => setShowAddScreen(true)} /> @@ -905,6 +1013,17 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad, }} /> )} + + {/* Экран добавления проекта */} + {showAddScreen && ( + setShowAddScreen(false)} + onSuccess={() => { + setShowAddScreen(false) + fetchProjects() + }} + /> + )} ) }