feat: добавлена возможность создания проектов через UI - версия 2.7.0
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 38s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 38s
This commit is contained in:
@@ -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 ===")
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user