feat: добавлена возможность создания проектов через UI - версия 2.7.0
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 38s

This commit is contained in:
Play Life Bot
2026-01-02 16:09:16 +03:00
parent ccb365c95c
commit a5ce0de236
3 changed files with 191 additions and 2 deletions

View File

@@ -1 +1 @@
2.6.1 2.7.0

View File

@@ -3569,6 +3569,7 @@ func main() {
protected.HandleFunc("/project/priority", app.setProjectPriorityHandler).Methods("POST", "OPTIONS") protected.HandleFunc("/project/priority", app.setProjectPriorityHandler).Methods("POST", "OPTIONS")
protected.HandleFunc("/project/move", app.moveProjectHandler).Methods("POST", "OPTIONS") protected.HandleFunc("/project/move", app.moveProjectHandler).Methods("POST", "OPTIONS")
protected.HandleFunc("/project/delete", app.deleteProjectHandler).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") protected.HandleFunc("/d2dc349a-0d13-49b2-a8f0-1ab094bfba9b", app.getFullStatisticsHandler).Methods("GET", "OPTIONS")
// Integrations // Integrations
@@ -4974,6 +4975,10 @@ type ProjectDeleteRequest struct {
ID int `json:"id"` ID int `json:"id"`
} }
type ProjectCreateRequest struct {
Name string `json:"name"`
}
func (a *App) moveProjectHandler(w http.ResponseWriter, r *http.Request) { func (a *App) moveProjectHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "OPTIONS" { if r.Method == "OPTIONS" {
setCORSHeaders(w) 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) { func (a *App) todoistWebhookHandler(w http.ResponseWriter, r *http.Request) {
// Логирование входящего запроса // Логирование входящего запроса
log.Printf("=== Todoist Webhook Request ===") log.Printf("=== Todoist Webhook Request ===")

View File

@@ -25,6 +25,104 @@ import { useAuth } from './auth/AuthContext'
const PROJECTS_API_URL = '/projects' const PROJECTS_API_URL = '/projects'
const PRIORITY_UPDATE_API_URL = '/project/priority' const PRIORITY_UPDATE_API_URL = '/project/priority'
const PROJECT_MOVE_API_URL = '/project/move' 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 (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg max-w-md w-90 shadow-lg max-h-[90vh] flex flex-col">
{/* Заголовок с кнопкой закрытия */}
<div className="flex justify-end p-4 border-b border-gray-200">
<button
onClick={onClose}
className="flex items-center justify-center w-10 h-10 rounded-full bg-white hover:bg-gray-100 text-gray-600 hover:text-gray-800 border border-gray-200 hover:border-gray-300 transition-all duration-200 shadow-sm hover:shadow-md"
title="Закрыть"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
{/* Контент */}
<div className="flex-1 overflow-y-auto p-6">
{/* Поле ввода */}
<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
Название проекта
</label>
<input
type="text"
value={projectName}
onChange={(e) => 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 && (
<div className="mt-2 text-sm text-red-600">{error}</div>
)}
</div>
</div>
{/* Кнопка подтверждения (прибита к низу) */}
<div className="p-6 border-t border-gray-200">
<button
onClick={handleSubmit}
disabled={isSubmitting || !projectName.trim()}
className="w-full px-4 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors font-medium disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{isSubmitting ? 'Обработка...' : 'Добавить'}
</button>
</div>
</div>
</div>
)
}
// Компонент экрана переноса проекта // Компонент экрана переноса проекта
function MoveProjectScreen({ project, allProjects, onClose, onSuccess }) { 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 ( return (
<div className="mb-6"> <div className="mb-6">
<div className="text-sm font-semibold text-gray-600 mb-2">{title}</div> <div className="text-sm font-semibold text-gray-600 mb-2">{title}</div>
@@ -258,6 +356,14 @@ function PrioritySlot({ title, projects, allProjects, onMenuClick, maxItems = nu
onMenuClick={onMenuClick} onMenuClick={onMenuClick}
/> />
))} ))}
{onAddClick && containerId === 'low' && (
<button
onClick={onAddClick}
className="w-full bg-white rounded-lg p-3 border-2 border-gray-200 shadow-sm hover:shadow-md transition-all duration-200 hover:border-indigo-400 text-gray-600 hover:text-indigo-600 font-semibold"
>
+ Добавить
</button>
)}
</div> </div>
</div> </div>
) )
@@ -289,6 +395,7 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
const [activeId, setActiveId] = useState(null) const [activeId, setActiveId] = useState(null)
const [selectedProject, setSelectedProject] = useState(null) // Для модального окна const [selectedProject, setSelectedProject] = useState(null) // Для модального окна
const [showMoveScreen, setShowMoveScreen] = useState(false) // Для экрана переноса const [showMoveScreen, setShowMoveScreen] = useState(false) // Для экрана переноса
const [showAddScreen, setShowAddScreen] = useState(false) // Для экрана добавления
const scrollContainerRef = useRef(null) const scrollContainerRef = useRef(null)
@@ -829,6 +936,7 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
allProjects={allProjects} allProjects={allProjects}
onMenuClick={handleMenuClick} onMenuClick={handleMenuClick}
containerId="low" containerId="low"
onAddClick={() => setShowAddScreen(true)}
/> />
</SortableContext> </SortableContext>
@@ -905,6 +1013,17 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
}} }}
/> />
)} )}
{/* Экран добавления проекта */}
{showAddScreen && (
<AddProjectScreen
onClose={() => setShowAddScreen(false)}
onSuccess={() => {
setShowAddScreen(false)
fetchProjects()
}}
/>
)}
</div> </div>
) )
} }