From 479ffb248362a40dc27b21bda1e77dc5859b4b99 Mon Sep 17 00:00:00 2001 From: poignatov Date: Wed, 4 Feb 2026 17:12:21 +0300 Subject: [PATCH] =?UTF-8?q?4.16.0:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=B2=D1=8B=D0=B1=D0=BE=D1=80=20=D1=86=D0=B2?= =?UTF-8?q?=D0=B5=D1=82=D0=B0=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BA=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION | 2 +- nginx-unified.conf | 15 +- play-life-backend/main.go | 166 ++++++++++++++++-- .../000011_add_color_to_projects.down.sql | 9 + .../000011_add_color_to_projects.up.sql | 45 +++++ play-life-web/nginx.conf | 15 +- play-life-web/package.json | 2 +- .../src/components/ColorPickerModal.jsx | 63 +++++++ play-life-web/src/components/CurrentWeek.jsx | 2 +- .../src/components/ProjectPriorityManager.jsx | 78 +++++++- .../src/components/WeekProgressChart.jsx | 9 +- play-life-web/src/utils/projectUtils.js | 48 ++++- 12 files changed, 416 insertions(+), 38 deletions(-) create mode 100644 play-life-backend/migrations/000011_add_color_to_projects.down.sql create mode 100644 play-life-backend/migrations/000011_add_color_to_projects.up.sql create mode 100644 play-life-web/src/components/ColorPickerModal.jsx diff --git a/VERSION b/VERSION index 5c517bf..ecbc3b0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.15.0 +4.16.0 diff --git a/nginx-unified.conf b/nginx-unified.conf index cc09d7c..138a2a4 100644 --- a/nginx-unified.conf +++ b/nginx-unified.conf @@ -62,8 +62,21 @@ server { proxy_cache_bypass $http_upgrade; } + # Proxy project endpoints to backend (must be before location /) + location ^~ /project/ { + proxy_pass http://localhost:8080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } + # Proxy other API endpoints to backend - location ~ ^/(playlife-feed|d2dc349a-0d13-49b2-a8f0-1ab094bfba9b|projects|project/priority|project/move|project/delete|project/create|message/post|weekly_goals/setup)$ { + location ~ ^/(playlife-feed|d2dc349a-0d13-49b2-a8f0-1ab094bfba9b|projects|message/post|weekly_goals/setup)$ { proxy_pass http://localhost:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; diff --git a/play-life-backend/main.go b/play-life-backend/main.go index 3493979..38f69d1 100644 --- a/play-life-backend/main.go +++ b/play-life-backend/main.go @@ -28,6 +28,8 @@ import ( "image/jpeg" + mathrand "math/rand" + "github.com/chromedp/chromedp" "github.com/disintegration/imaging" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" @@ -43,6 +45,42 @@ import ( "golang.org/x/crypto/bcrypt" ) +// Палитра из 30 контрастных цветов для проектов (HEX формат) +// Используется для генерации случайного цвета при создании проекта +// Должна быть синхронизирована с frontend (projectUtils.js) +var projectColorsPalette = []string{ + "#EF4444", // Красный + "#F97316", // Оранжевый + "#F59E0B", // Янтарный + "#EAB308", // Желтый + "#84CC16", // Лайм + "#22C55E", // Зеленый + "#10B981", // Изумрудный + "#14B8A6", // Бирюзовый + "#06B6D4", // Голубой + "#0EA5E9", // Небесный + "#3B82F6", // Синий + "#6366F1", // Индиго + "#8B5CF6", // Фиолетовый + "#A855F7", // Пурпурный + "#D946EF", // Фуксия + "#EC4899", // Розовый + "#F43F5E", // Розово-красный + "#DC2626", // Темно-красный + "#EA580C", // Темно-оранжевый + "#CA8A04", // Темно-желтый + "#65A30D", // Темно-лайм + "#16A34A", // Темно-зеленый + "#059669", // Темно-изумрудный + "#0D9488", // Темно-бирюзовый + "#0891B2", // Темно-голубой + "#0284C7", // Темно-небесный + "#2563EB", // Темно-синий + "#4F46E5", // Темно-индиго + "#7C3AED", // Темно-фиолетовый + "#9333EA", // Темно-пурпурный +} + type Word struct { ID int `json:"id"` Name string `json:"name"` @@ -113,6 +151,7 @@ type WeeklyProjectStats struct { Priority *int `json:"priority,omitempty"` CalculatedScore float64 `json:"calculated_score"` TodayChange *float64 `json:"today_change,omitempty"` + Color string `json:"color"` } type GroupsProgress struct { @@ -158,6 +197,7 @@ type Project struct { ProjectID int `json:"project_id"` ProjectName string `json:"project_name"` Priority *int `json:"priority,omitempty"` + Color string `json:"color"` } type ProjectPriorityUpdate struct { @@ -176,6 +216,7 @@ type FullStatisticsItem struct { TotalScore float64 `json:"total_score"` MinGoalScore float64 `json:"min_goal_score"` MaxGoalScore float64 `json:"max_goal_score"` + Color string `json:"color"` } type TodayEntryNode struct { @@ -377,9 +418,9 @@ type WishlistItem struct { LockedConditionsCount int `json:"locked_conditions_count,omitempty"` // Общее количество заблокированных условий UnlockConditions []UnlockConditionDisplay `json:"unlock_conditions,omitempty"` LinkedTask *LinkedTask `json:"linked_task,omitempty"` - TasksCount int `json:"tasks_count,omitempty"` // Количество задач для этого желания - ProjectID *int `json:"project_id,omitempty"` // ID проекта, к которому принадлежит желание - ProjectName *string `json:"project_name,omitempty"` // Название проекта + TasksCount int `json:"tasks_count,omitempty"` // Количество задач для этого желания + ProjectID *int `json:"project_id,omitempty"` // ID проекта, к которому принадлежит желание + ProjectName *string `json:"project_name,omitempty"` // Название проекта } type UnlockConditionDisplay struct { @@ -729,6 +770,14 @@ func generateWebhookToken() (string, error) { return base64.URLEncoding.EncodeToString(b), nil } +// generateRandomProjectColor возвращает случайный цвет из предопределенной палитры +func generateRandomProjectColor() string { + if len(projectColorsPalette) == 0 { + return "#3B82F6" // Fallback цвет + } + return projectColorsPalette[mathrand.Intn(len(projectColorsPalette))] +} + func (a *App) generateAccessToken(userID int) (string, error) { claims := JWTClaims{ UserID: userID, @@ -2556,7 +2605,8 @@ func (a *App) getWeeklyStatsHandler(w http.ResponseWriter, r *http.Request) { COALESCE(wr.total_score, 0.0000) AS total_score, wg.min_goal_score, wg.max_goal_score, - wg.priority AS priority + wg.priority AS priority, + p.color FROM projects p LEFT JOIN @@ -2600,6 +2650,7 @@ func (a *App) getWeeklyStatsHandler(w http.ResponseWriter, r *http.Request) { &minGoalScore, &maxGoalScore, &priority, + &project.Color, ) if err != nil { log.Printf("Error scanning weekly stats row: %v", err) @@ -3091,7 +3142,8 @@ func (a *App) getWeeklyStatsData() (*WeeklyStatsResponse, error) { COALESCE(wr.total_score, 0.0000) AS total_score, wg.min_goal_score, wg.max_goal_score, - wg.priority AS priority + wg.priority AS priority, + p.color FROM projects p LEFT JOIN @@ -3252,7 +3304,8 @@ func (a *App) getWeeklyStatsDataForUser(userID int) (*WeeklyStatsResponse, error COALESCE(wr.total_score, 0.0000) AS total_score, wg.min_goal_score, wg.max_goal_score, - wg.priority AS priority + wg.priority AS priority, + p.color FROM projects p LEFT JOIN @@ -3293,6 +3346,7 @@ func (a *App) getWeeklyStatsDataForUser(userID int) (*WeeklyStatsResponse, error &minGoalScore, &maxGoalScore, &priority, + &project.Color, ) if err != nil { return nil, fmt.Errorf("error scanning weekly stats row: %w", err) @@ -3864,6 +3918,7 @@ func main() { // Note: /message/post, /weekly_goals/setup, /daily-report/trigger moved to adminAPIRoutes protected.HandleFunc("/projects", app.getProjectsHandler).Methods("GET", "OPTIONS") protected.HandleFunc("/project/priority", app.setProjectPriorityHandler).Methods("POST", "OPTIONS") + protected.HandleFunc("/project/color", app.setProjectColorHandler).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") @@ -4656,10 +4711,11 @@ func (a *App) insertMessageData(entryText string, createdDate string, nodes []Pr if err == sql.ErrNoRows { // Проект не существует, создаем новый + randomColor := generateRandomProjectColor() _, err = tx.Exec(` - INSERT INTO projects (name, deleted, user_id) - VALUES ($1, FALSE, $2) - `, projectName, *userID) + INSERT INTO projects (name, deleted, user_id, color) + VALUES ($1, FALSE, $2, $3) + `, projectName, *userID, randomColor) if err != nil { // Если ошибка из-за уникальности, пробуем обновить существующий _, err = tx.Exec(` @@ -4685,10 +4741,11 @@ func (a *App) insertMessageData(entryText string, createdDate string, nodes []Pr if err == sql.ErrNoRows { // Проект не существует, создаем новый + randomColor := generateRandomProjectColor() _, err = tx.Exec(` - INSERT INTO projects (name, deleted) - VALUES ($1, FALSE) - `, projectName) + INSERT INTO projects (name, deleted, color) + VALUES ($1, FALSE, $2) + `, projectName, randomColor) if err != nil { return fmt.Errorf("failed to insert project %s: %w", projectName, err) } @@ -5211,7 +5268,8 @@ func (a *App) getProjectsHandler(w http.ResponseWriter, r *http.Request) { SELECT id AS project_id, name AS project_name, - priority + priority, + color FROM projects WHERE @@ -5238,6 +5296,7 @@ func (a *App) getProjectsHandler(w http.ResponseWriter, r *http.Request) { &project.ProjectID, &project.ProjectName, &priority, + &project.Color, ) if err != nil { log.Printf("Error scanning project row: %v", err) @@ -5433,6 +5492,69 @@ func (a *App) setProjectPriorityHandler(w http.ResponseWriter, r *http.Request) }) } +type ProjectColorRequest struct { + ID int `json:"id"` + Color string `json:"color"` +} + +func (a *App) setProjectColorHandler(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 ProjectColorRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + log.Printf("Error decoding project color request: %v", err) + sendErrorWithCORS(w, "Invalid request body", http.StatusBadRequest) + return + } + + if req.ID == 0 { + sendErrorWithCORS(w, "id is required", http.StatusBadRequest) + return + } + + if req.Color == "" { + sendErrorWithCORS(w, "color is required", http.StatusBadRequest) + return + } + + // Проверяем, что цвет в правильном формате HEX + if !strings.HasPrefix(req.Color, "#") || len(req.Color) != 7 { + sendErrorWithCORS(w, "color must be in HEX format (e.g., #FF5733)", http.StatusBadRequest) + return + } + + // Обновляем цвет проекта + _, err := a.DB.Exec(` + UPDATE projects + SET color = $1 + WHERE id = $2 AND user_id = $3 + `, req.Color, req.ID, userID) + + if err != nil { + log.Printf("Error updating project color: %v", err) + sendErrorWithCORS(w, fmt.Sprintf("Error updating project color: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "message": "Project color updated successfully", + "id": req.ID, + "color": req.Color, + }) +} + type ProjectMoveRequest struct { ID int `json:"id"` NewName string `json:"new_name"` @@ -5720,12 +5842,13 @@ func (a *App) createProjectHandler(w http.ResponseWriter, r *http.Request) { } // Создаем новый проект + randomColor := generateRandomProjectColor() var projectID int err = a.DB.QueryRow(` - INSERT INTO projects (name, deleted, user_id) - VALUES ($1, FALSE, $2) + INSERT INTO projects (name, deleted, user_id, color) + VALUES ($1, FALSE, $2, $3) RETURNING id - `, req.Name, userID).Scan(&projectID) + `, req.Name, userID, randomColor).Scan(&projectID) if err != nil { log.Printf("Error creating project: %v", err) @@ -6188,7 +6311,8 @@ func (a *App) getFullStatisticsHandler(w http.ResponseWriter, r *http.Request) { -- Максимальная цель: COALESCE(NULL, 0.0000) COALESCE(wg.max_goal_score, 0.0000) AS max_goal_score, - p.id AS project_id + p.id AS project_id, + p.color FROM weekly_report_mv wr FULL OUTER JOIN @@ -6232,6 +6356,7 @@ func (a *App) getFullStatisticsHandler(w http.ResponseWriter, r *http.Request) { &item.MinGoalScore, &item.MaxGoalScore, &projectID, + &item.Color, ) if err != nil { log.Printf("Error scanning full statistics row: %v", err) @@ -6256,7 +6381,8 @@ func (a *App) getFullStatisticsHandler(w http.ResponseWriter, r *http.Request) { p.id AS project_id, p.name AS project_name, COALESCE(wg.min_goal_score, 0.0000) AS min_goal_score, - COALESCE(wg.max_goal_score, 0.0000) AS max_goal_score + COALESCE(wg.max_goal_score, 0.0000) AS max_goal_score, + p.color FROM projects p LEFT JOIN weekly_goals wg ON wg.project_id = p.id AND wg.goal_year = EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER @@ -6288,7 +6414,8 @@ func (a *App) getFullStatisticsHandler(w http.ResponseWriter, r *http.Request) { var projectID int var projectName string var minGoalScore, maxGoalScore float64 - if err := goalsRows.Scan(&projectID, &projectName, &minGoalScore, &maxGoalScore); err == nil { + var projectColor string + if err := goalsRows.Scan(&projectID, &projectName, &minGoalScore, &maxGoalScore, &projectColor); err == nil { // Добавляем только если проекта еще нет в статистике if !existingProjects[projectID] { totalScore := 0.0 @@ -6304,6 +6431,7 @@ func (a *App) getFullStatisticsHandler(w http.ResponseWriter, r *http.Request) { TotalScore: totalScore, MinGoalScore: minGoalScore, MaxGoalScore: maxGoalScore, + Color: projectColor, } statistics = append(statistics, item) } diff --git a/play-life-backend/migrations/000011_add_color_to_projects.down.sql b/play-life-backend/migrations/000011_add_color_to_projects.down.sql new file mode 100644 index 0000000..52c341b --- /dev/null +++ b/play-life-backend/migrations/000011_add_color_to_projects.down.sql @@ -0,0 +1,9 @@ +-- Migration: Remove color field from projects table +-- Date: 2026-02-02 +-- +-- This migration removes the color field from projects table. + +DROP INDEX IF EXISTS idx_projects_color; + +ALTER TABLE projects +DROP COLUMN IF EXISTS color; diff --git a/play-life-backend/migrations/000011_add_color_to_projects.up.sql b/play-life-backend/migrations/000011_add_color_to_projects.up.sql new file mode 100644 index 0000000..2301b24 --- /dev/null +++ b/play-life-backend/migrations/000011_add_color_to_projects.up.sql @@ -0,0 +1,45 @@ +-- Migration: Add color field to projects table +-- Date: 2026-02-02 +-- +-- This migration adds color field to projects table to allow +-- custom color selection for projects. The field is NOT NULL, +-- and existing projects will be assigned colors from a predefined palette. + +-- Добавляем поле color +ALTER TABLE projects +ADD COLUMN color VARCHAR(7) NOT NULL DEFAULT '#3B82F6'; + +-- Палитра из 30 контрастных цветов (синхронизирована с backend и frontend) +-- Заполняем существующие проекты цветами из палитры +DO $$ +DECLARE + colors TEXT[] := ARRAY[ + '#EF4444', '#F97316', '#F59E0B', '#EAB308', '#84CC16', + '#22C55E', '#10B981', '#14B8A6', '#06B6D4', '#0EA5E9', + '#3B82F6', '#6366F1', '#8B5CF6', '#A855F7', '#D946EF', + '#EC4899', '#F43F5E', '#DC2626', '#EA580C', '#CA8A04', + '#65A30D', '#16A34A', '#059669', '#0D9488', '#0891B2', + '#0284C7', '#2563EB', '#4F46E5', '#7C3AED', '#9333EA' + ]; + project_record RECORD; + color_index INTEGER := 0; +BEGIN + -- Обновляем существующие проекты, присваивая им цвета из палитры + FOR project_record IN + SELECT id FROM projects ORDER BY id + LOOP + UPDATE projects + SET color = colors[1 + (color_index % array_length(colors, 1))] + WHERE id = project_record.id; + + color_index := color_index + 1; + END LOOP; +END $$; + +-- Убираем DEFAULT, так как теперь все проекты имеют цвет +ALTER TABLE projects +ALTER COLUMN color DROP DEFAULT; + +CREATE INDEX IF NOT EXISTS idx_projects_color ON projects(color); + +COMMENT ON COLUMN projects.color IS 'Project color in HEX format (e.g., #FF5733)'; diff --git a/play-life-web/nginx.conf b/play-life-web/nginx.conf index 9f10aaa..258de2e 100644 --- a/play-life-web/nginx.conf +++ b/play-life-web/nginx.conf @@ -36,8 +36,21 @@ server { proxy_cache_bypass $http_upgrade; } + # Proxy project endpoints to backend (must be before location /) + location ^~ /project/ { + proxy_pass http://backend:8080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } + # Proxy other API endpoints to backend - location ~ ^/(playlife-feed|d2dc349a-0d13-49b2-a8f0-1ab094bfba9b|projects|project/priority|project/move|project/delete|project/create|message/post|webhook/|weekly_goals/setup)$ { + location ~ ^/(playlife-feed|d2dc349a-0d13-49b2-a8f0-1ab094bfba9b|projects|message/post|webhook/|weekly_goals/setup)$ { proxy_pass http://backend:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; diff --git a/play-life-web/package.json b/play-life-web/package.json index 8f5a2d4..5740cbe 100644 --- a/play-life-web/package.json +++ b/play-life-web/package.json @@ -1,6 +1,6 @@ { "name": "play-life-web", - "version": "4.15.0", + "version": "4.16.0", "type": "module", "scripts": { "dev": "vite", diff --git a/play-life-web/src/components/ColorPickerModal.jsx b/play-life-web/src/components/ColorPickerModal.jsx new file mode 100644 index 0000000..21a0037 --- /dev/null +++ b/play-life-web/src/components/ColorPickerModal.jsx @@ -0,0 +1,63 @@ +import { PROJECT_COLORS_PALETTE } from '../utils/projectUtils' +import './Integrations.css' + +function ColorPickerModal({ onClose, onColorSelect, currentColor }) { + const handleColorClick = (color) => { + onColorSelect(color) + onClose() + } + + return ( +
+
e.stopPropagation()}> + {/* Заголовок с кнопкой закрытия */} +
+

Выберите цвет проекта

+ +
+ + {/* Контент - сетка цветов */} +
+
+ {PROJECT_COLORS_PALETTE.map((color, index) => ( + + ))} +
+
+
+
+ ) +} + +export default ColorPickerModal diff --git a/play-life-web/src/components/CurrentWeek.jsx b/play-life-web/src/components/CurrentWeek.jsx index 5c73ec7..60bbdc8 100644 --- a/play-life-web/src/components/CurrentWeek.jsx +++ b/play-life-web/src/components/CurrentWeek.jsx @@ -234,7 +234,7 @@ function PriorityGroup({ title, subtitle, projects, allProjects, onProjectClick {projects.map((project, index) => { if (!project || !project.project_name) return null - const projectColor = getProjectColor(project.project_name, allProjects) + const projectColor = getProjectColor(project.project_name, allProjects, project.color) return ( -
{ + e.stopPropagation() + if (onColorClick) { + onColorClick(project, e) + } + }} + className="w-3 h-3 rounded-full flex-shrink-0 hover:scale-125 transition-transform cursor-pointer border border-gray-300 hover:border-gray-500" style={{ backgroundColor: projectColor }} - >
+ title="Выбрать цвет" + > {project.name} {onMenuClick && ( @@ -347,7 +356,7 @@ function DroppableSlot({ containerId, isEmpty, maxItems, currentCount }) { } // Компонент для слота приоритета -function PrioritySlot({ title, projects, allProjects, onMenuClick, maxItems = null, containerId, onAddClick }) { +function PrioritySlot({ title, projects, allProjects, onMenuClick, maxItems = null, containerId, onAddClick, onColorClick }) { return (
{title}
@@ -362,6 +371,7 @@ function PrioritySlot({ title, projects, allProjects, onMenuClick, maxItems = nu index={index} allProjects={allProjects} onMenuClick={onMenuClick} + onColorClick={onColorClick} /> ))} {onAddClick && containerId === 'low' && ( @@ -405,6 +415,8 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad, const [selectedProject, setSelectedProject] = useState(null) // Для модального окна const [showMoveScreen, setShowMoveScreen] = useState(false) // Для экрана переноса const [showAddScreen, setShowAddScreen] = useState(false) // Для экрана добавления + const [showColorPicker, setShowColorPicker] = useState(false) // Для модального окна выбора цвета + const [selectedProjectForColor, setSelectedProjectForColor] = useState(null) // Проект для выбора цвета const scrollContainerRef = useRef(null) @@ -438,7 +450,8 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad, const id = item?.project_id ?? item?.id ?? item?.projectId ?? null const priorityValue = item?.priority ?? item?.priority_value ?? item?.priority_level ?? null - return { id, name, priority: priorityValue } + const color = item?.color ?? null + return { id, name, priority: priorityValue, color } }) .filter(Boolean) @@ -458,7 +471,7 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad, const low = [] uniqueProjects.forEach(item => { - const projectEntry = { name: item.name, id: item.id } + const projectEntry = { name: item.name, id: item.id, color: item.color } if (item.priority === 1) { max.push(projectEntry) } else if (item.priority === 2) { @@ -860,6 +873,38 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad, } } + const handleColorClick = (project, e) => { + e.stopPropagation() + setSelectedProjectForColor(project) + setShowColorPicker(true) + } + + const handleColorSelect = async (color) => { + if (!selectedProjectForColor) return + + try { + const projectId = selectedProjectForColor.id ?? selectedProjectForColor.name + const response = await authFetch(PROJECT_COLOR_API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: projectId, color: color }), + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(errorText || 'Ошибка при сохранении цвета') + } + + setShowColorPicker(false) + setSelectedProjectForColor(null) + // Обновляем список проектов + fetchProjects() + } catch (error) { + console.error('Ошибка сохранения цвета:', error) + setToastMessage({ text: error.message || 'Ошибка сохранения цвета', type: 'error' }) + } + } + const closeModal = () => { setSelectedProject(null) } @@ -912,6 +957,7 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad, projects={maxPriority} allProjects={allProjects} onMenuClick={handleMenuClick} + onColorClick={handleColorClick} maxItems={1} containerId="max" /> @@ -923,6 +969,7 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad, projects={mediumPriority} allProjects={allProjects} onMenuClick={handleMenuClick} + onColorClick={handleColorClick} maxItems={2} containerId="medium" /> @@ -934,6 +981,7 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad, projects={lowPriority} allProjects={allProjects} onMenuClick={handleMenuClick} + onColorClick={handleColorClick} containerId="low" onAddClick={() => setShowAddScreen(true)} /> @@ -954,7 +1002,7 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
{activeProject.name}
@@ -1030,6 +1078,18 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad, /> )} + {/* Модальное окно выбора цвета */} + {showColorPicker && selectedProjectForColor && ( + { + setShowColorPicker(false) + setSelectedProjectForColor(null) + }} + onColorSelect={handleColorSelect} + currentColor={selectedProjectForColor.color || getProjectColor(selectedProjectForColor.name, allProjects)} + /> + )} + {toastMessage && ( { - const color = getProjectColor(project.projectName, allProjects) + // Ищем цвет проекта в данных full-statistics + const projectData = data?.find(item => item.project_name === project.projectName) + const projectColor = projectData?.color + ? getProjectColor(project.projectName, allProjects, projectData.color) + : getProjectColor(project.projectName, allProjects) return { ...project, - color + color: projectColor } }) diff --git a/play-life-web/src/utils/projectUtils.js b/play-life-web/src/utils/projectUtils.js index 7b1335a..05f865b 100644 --- a/play-life-web/src/utils/projectUtils.js +++ b/play-life-web/src/utils/projectUtils.js @@ -1,5 +1,40 @@ // Утилиты для работы с проектами - обеспечивают единую сортировку и цвета +// Палитра из 30 контрастных цветов для проектов (HEX формат) +// Должна быть синхронизирована с backend (main.go) +export const PROJECT_COLORS_PALETTE = [ + '#EF4444', // Красный + '#F97316', // Оранжевый + '#F59E0B', // Янтарный + '#EAB308', // Желтый + '#84CC16', // Лайм + '#22C55E', // Зеленый + '#10B981', // Изумрудный + '#14B8A6', // Бирюзовый + '#06B6D4', // Голубой + '#0EA5E9', // Небесный + '#3B82F6', // Синий + '#6366F1', // Индиго + '#8B5CF6', // Фиолетовый + '#A855F7', // Пурпурный + '#D946EF', // Фуксия + '#EC4899', // Розовый + '#F43F5E', // Розово-красный + '#DC2626', // Темно-красный + '#EA580C', // Темно-оранжевый + '#CA8A04', // Темно-желтый + '#65A30D', // Темно-лайм + '#16A34A', // Темно-зеленый + '#059669', // Темно-изумрудный + '#0D9488', // Темно-бирюзовый + '#0891B2', // Темно-голубой + '#0284C7', // Темно-небесный + '#2563EB', // Темно-синий + '#4F46E5', // Темно-индиго + '#7C3AED', // Темно-фиолетовый + '#9333EA', // Темно-пурпурный +] + // Функция для генерации цвета проекта на основе его индекса в отсортированном списке export function getProjectColorByIndex(index) { const hue = (index * 137.508) % 360 // Золотой угол для равномерного распределения цветов @@ -39,13 +74,20 @@ export function getAllProjectsSorted(allProjectsData, currentWeekData = null) { } /** - * Получает цвет проекта на основе его названия + * Получает цвет проекта на основе его названия или цвета из БД * * @param {string} projectName - название проекта * @param {Array} allProjectsSorted - отсортированный список всех проектов - * @returns {string} цвет в формате HSL + * @param {string|null} projectColorFromDB - цвет проекта из базы данных (HEX формат) + * @returns {string} цвет в формате HEX или HSL (fallback) */ -export function getProjectColor(projectName, allProjectsSorted) { +export function getProjectColor(projectName, allProjectsSorted, projectColorFromDB = null) { + // Если передан цвет из БД и он не пустой - использовать его + if (projectColorFromDB && projectColorFromDB.trim() !== '') { + return projectColorFromDB + } + + // Иначе использовать вычисляемый цвет (текущая логика) - это fallback для обратной совместимости const projectIndex = allProjectsSorted.indexOf(projectName) return projectIndex >= 0 ? getProjectColorByIndex(projectIndex) : '#9CA3AF' }