4.16.0: Добавлен выбор цвета для проектов
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 2m9s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 2m9s
This commit is contained in:
@@ -62,8 +62,21 @@ server {
|
|||||||
proxy_cache_bypass $http_upgrade;
|
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
|
# 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_pass http://localhost:8080;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ import (
|
|||||||
|
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
|
|
||||||
|
mathrand "math/rand"
|
||||||
|
|
||||||
"github.com/chromedp/chromedp"
|
"github.com/chromedp/chromedp"
|
||||||
"github.com/disintegration/imaging"
|
"github.com/disintegration/imaging"
|
||||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||||
@@ -43,6 +45,42 @@ import (
|
|||||||
"golang.org/x/crypto/bcrypt"
|
"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 {
|
type Word struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -113,6 +151,7 @@ type WeeklyProjectStats struct {
|
|||||||
Priority *int `json:"priority,omitempty"`
|
Priority *int `json:"priority,omitempty"`
|
||||||
CalculatedScore float64 `json:"calculated_score"`
|
CalculatedScore float64 `json:"calculated_score"`
|
||||||
TodayChange *float64 `json:"today_change,omitempty"`
|
TodayChange *float64 `json:"today_change,omitempty"`
|
||||||
|
Color string `json:"color"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GroupsProgress struct {
|
type GroupsProgress struct {
|
||||||
@@ -158,6 +197,7 @@ type Project struct {
|
|||||||
ProjectID int `json:"project_id"`
|
ProjectID int `json:"project_id"`
|
||||||
ProjectName string `json:"project_name"`
|
ProjectName string `json:"project_name"`
|
||||||
Priority *int `json:"priority,omitempty"`
|
Priority *int `json:"priority,omitempty"`
|
||||||
|
Color string `json:"color"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProjectPriorityUpdate struct {
|
type ProjectPriorityUpdate struct {
|
||||||
@@ -176,6 +216,7 @@ type FullStatisticsItem struct {
|
|||||||
TotalScore float64 `json:"total_score"`
|
TotalScore float64 `json:"total_score"`
|
||||||
MinGoalScore float64 `json:"min_goal_score"`
|
MinGoalScore float64 `json:"min_goal_score"`
|
||||||
MaxGoalScore float64 `json:"max_goal_score"`
|
MaxGoalScore float64 `json:"max_goal_score"`
|
||||||
|
Color string `json:"color"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TodayEntryNode struct {
|
type TodayEntryNode struct {
|
||||||
@@ -729,6 +770,14 @@ func generateWebhookToken() (string, error) {
|
|||||||
return base64.URLEncoding.EncodeToString(b), nil
|
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) {
|
func (a *App) generateAccessToken(userID int) (string, error) {
|
||||||
claims := JWTClaims{
|
claims := JWTClaims{
|
||||||
UserID: userID,
|
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,
|
COALESCE(wr.total_score, 0.0000) AS total_score,
|
||||||
wg.min_goal_score,
|
wg.min_goal_score,
|
||||||
wg.max_goal_score,
|
wg.max_goal_score,
|
||||||
wg.priority AS priority
|
wg.priority AS priority,
|
||||||
|
p.color
|
||||||
FROM
|
FROM
|
||||||
projects p
|
projects p
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
@@ -2600,6 +2650,7 @@ func (a *App) getWeeklyStatsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
&minGoalScore,
|
&minGoalScore,
|
||||||
&maxGoalScore,
|
&maxGoalScore,
|
||||||
&priority,
|
&priority,
|
||||||
|
&project.Color,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error scanning weekly stats row: %v", err)
|
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,
|
COALESCE(wr.total_score, 0.0000) AS total_score,
|
||||||
wg.min_goal_score,
|
wg.min_goal_score,
|
||||||
wg.max_goal_score,
|
wg.max_goal_score,
|
||||||
wg.priority AS priority
|
wg.priority AS priority,
|
||||||
|
p.color
|
||||||
FROM
|
FROM
|
||||||
projects p
|
projects p
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
@@ -3252,7 +3304,8 @@ func (a *App) getWeeklyStatsDataForUser(userID int) (*WeeklyStatsResponse, error
|
|||||||
COALESCE(wr.total_score, 0.0000) AS total_score,
|
COALESCE(wr.total_score, 0.0000) AS total_score,
|
||||||
wg.min_goal_score,
|
wg.min_goal_score,
|
||||||
wg.max_goal_score,
|
wg.max_goal_score,
|
||||||
wg.priority AS priority
|
wg.priority AS priority,
|
||||||
|
p.color
|
||||||
FROM
|
FROM
|
||||||
projects p
|
projects p
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
@@ -3293,6 +3346,7 @@ func (a *App) getWeeklyStatsDataForUser(userID int) (*WeeklyStatsResponse, error
|
|||||||
&minGoalScore,
|
&minGoalScore,
|
||||||
&maxGoalScore,
|
&maxGoalScore,
|
||||||
&priority,
|
&priority,
|
||||||
|
&project.Color,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error scanning weekly stats row: %w", err)
|
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
|
// Note: /message/post, /weekly_goals/setup, /daily-report/trigger moved to adminAPIRoutes
|
||||||
protected.HandleFunc("/projects", app.getProjectsHandler).Methods("GET", "OPTIONS")
|
protected.HandleFunc("/projects", app.getProjectsHandler).Methods("GET", "OPTIONS")
|
||||||
protected.HandleFunc("/project/priority", app.setProjectPriorityHandler).Methods("POST", "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/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("/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 {
|
if err == sql.ErrNoRows {
|
||||||
// Проект не существует, создаем новый
|
// Проект не существует, создаем новый
|
||||||
|
randomColor := generateRandomProjectColor()
|
||||||
_, err = tx.Exec(`
|
_, err = tx.Exec(`
|
||||||
INSERT INTO projects (name, deleted, user_id)
|
INSERT INTO projects (name, deleted, user_id, color)
|
||||||
VALUES ($1, FALSE, $2)
|
VALUES ($1, FALSE, $2, $3)
|
||||||
`, projectName, *userID)
|
`, projectName, *userID, randomColor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Если ошибка из-за уникальности, пробуем обновить существующий
|
// Если ошибка из-за уникальности, пробуем обновить существующий
|
||||||
_, err = tx.Exec(`
|
_, err = tx.Exec(`
|
||||||
@@ -4685,10 +4741,11 @@ func (a *App) insertMessageData(entryText string, createdDate string, nodes []Pr
|
|||||||
|
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
// Проект не существует, создаем новый
|
// Проект не существует, создаем новый
|
||||||
|
randomColor := generateRandomProjectColor()
|
||||||
_, err = tx.Exec(`
|
_, err = tx.Exec(`
|
||||||
INSERT INTO projects (name, deleted)
|
INSERT INTO projects (name, deleted, color)
|
||||||
VALUES ($1, FALSE)
|
VALUES ($1, FALSE, $2)
|
||||||
`, projectName)
|
`, projectName, randomColor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to insert project %s: %w", projectName, err)
|
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
|
SELECT
|
||||||
id AS project_id,
|
id AS project_id,
|
||||||
name AS project_name,
|
name AS project_name,
|
||||||
priority
|
priority,
|
||||||
|
color
|
||||||
FROM
|
FROM
|
||||||
projects
|
projects
|
||||||
WHERE
|
WHERE
|
||||||
@@ -5238,6 +5296,7 @@ func (a *App) getProjectsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
&project.ProjectID,
|
&project.ProjectID,
|
||||||
&project.ProjectName,
|
&project.ProjectName,
|
||||||
&priority,
|
&priority,
|
||||||
|
&project.Color,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error scanning project row: %v", err)
|
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 {
|
type ProjectMoveRequest struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
NewName string `json:"new_name"`
|
NewName string `json:"new_name"`
|
||||||
@@ -5720,12 +5842,13 @@ func (a *App) createProjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Создаем новый проект
|
// Создаем новый проект
|
||||||
|
randomColor := generateRandomProjectColor()
|
||||||
var projectID int
|
var projectID int
|
||||||
err = a.DB.QueryRow(`
|
err = a.DB.QueryRow(`
|
||||||
INSERT INTO projects (name, deleted, user_id)
|
INSERT INTO projects (name, deleted, user_id, color)
|
||||||
VALUES ($1, FALSE, $2)
|
VALUES ($1, FALSE, $2, $3)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`, req.Name, userID).Scan(&projectID)
|
`, req.Name, userID, randomColor).Scan(&projectID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error creating project: %v", err)
|
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(NULL, 0.0000)
|
||||||
COALESCE(wg.max_goal_score, 0.0000) AS max_goal_score,
|
COALESCE(wg.max_goal_score, 0.0000) AS max_goal_score,
|
||||||
p.id AS project_id
|
p.id AS project_id,
|
||||||
|
p.color
|
||||||
FROM
|
FROM
|
||||||
weekly_report_mv wr
|
weekly_report_mv wr
|
||||||
FULL OUTER JOIN
|
FULL OUTER JOIN
|
||||||
@@ -6232,6 +6356,7 @@ func (a *App) getFullStatisticsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
&item.MinGoalScore,
|
&item.MinGoalScore,
|
||||||
&item.MaxGoalScore,
|
&item.MaxGoalScore,
|
||||||
&projectID,
|
&projectID,
|
||||||
|
&item.Color,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error scanning full statistics row: %v", err)
|
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.id AS project_id,
|
||||||
p.name AS project_name,
|
p.name AS project_name,
|
||||||
COALESCE(wg.min_goal_score, 0.0000) AS min_goal_score,
|
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
|
FROM projects p
|
||||||
LEFT JOIN weekly_goals wg ON wg.project_id = p.id
|
LEFT JOIN weekly_goals wg ON wg.project_id = p.id
|
||||||
AND wg.goal_year = EXTRACT(ISOYEAR FROM CURRENT_DATE)::INTEGER
|
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 projectID int
|
||||||
var projectName string
|
var projectName string
|
||||||
var minGoalScore, maxGoalScore float64
|
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] {
|
if !existingProjects[projectID] {
|
||||||
totalScore := 0.0
|
totalScore := 0.0
|
||||||
@@ -6304,6 +6431,7 @@ func (a *App) getFullStatisticsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
TotalScore: totalScore,
|
TotalScore: totalScore,
|
||||||
MinGoalScore: minGoalScore,
|
MinGoalScore: minGoalScore,
|
||||||
MaxGoalScore: maxGoalScore,
|
MaxGoalScore: maxGoalScore,
|
||||||
|
Color: projectColor,
|
||||||
}
|
}
|
||||||
statistics = append(statistics, item)
|
statistics = append(statistics, item)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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)';
|
||||||
@@ -36,8 +36,21 @@ server {
|
|||||||
proxy_cache_bypass $http_upgrade;
|
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
|
# 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_pass http://backend:8080;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "play-life-web",
|
"name": "play-life-web",
|
||||||
"version": "4.15.0",
|
"version": "4.16.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
63
play-life-web/src/components/ColorPickerModal.jsx
Normal file
63
play-life-web/src/components/ColorPickerModal.jsx
Normal file
@@ -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 (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" onClick={onClose}>
|
||||||
|
<div className="bg-white rounded-lg max-w-md w-90 shadow-lg max-h-[90vh] flex flex-col" onClick={(e) => e.stopPropagation()}>
|
||||||
|
{/* Заголовок с кнопкой закрытия */}
|
||||||
|
<div className="flex justify-between items-center p-4 border-b border-gray-200">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-800">Выберите цвет проекта</h3>
|
||||||
|
<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="grid grid-cols-6 gap-3">
|
||||||
|
{PROJECT_COLORS_PALETTE.map((color, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => handleColorClick(color)}
|
||||||
|
className={`
|
||||||
|
w-12 h-12 rounded-full
|
||||||
|
border-2 transition-all duration-200
|
||||||
|
hover:scale-110 hover:shadow-lg
|
||||||
|
${currentColor === color
|
||||||
|
? 'border-gray-800 shadow-md ring-2 ring-offset-2 ring-gray-400'
|
||||||
|
: 'border-gray-300 hover:border-gray-500'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
style={{ backgroundColor: color }}
|
||||||
|
title={color}
|
||||||
|
>
|
||||||
|
{currentColor === color && (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<polyline points="20 6 9 17 4 12"></polyline>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ColorPickerModal
|
||||||
@@ -234,7 +234,7 @@ function PriorityGroup({ title, subtitle, projects, allProjects, onProjectClick
|
|||||||
{projects.map((project, index) => {
|
{projects.map((project, index) => {
|
||||||
if (!project || !project.project_name) return null
|
if (!project || !project.project_name) return null
|
||||||
|
|
||||||
const projectColor = getProjectColor(project.project_name, allProjects)
|
const projectColor = getProjectColor(project.project_name, allProjects, project.color)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProjectCard
|
<ProjectCard
|
||||||
|
|||||||
@@ -22,11 +22,13 @@ import { getAllProjectsSorted, getProjectColor } from '../utils/projectUtils'
|
|||||||
import { useAuth } from './auth/AuthContext'
|
import { useAuth } from './auth/AuthContext'
|
||||||
import LoadingError from './LoadingError'
|
import LoadingError from './LoadingError'
|
||||||
import Toast from './Toast'
|
import Toast from './Toast'
|
||||||
|
import ColorPickerModal from './ColorPickerModal'
|
||||||
import './Integrations.css'
|
import './Integrations.css'
|
||||||
|
|
||||||
// API endpoints (используем относительные пути, проксирование настроено в nginx/vite)
|
// API endpoints (используем относительные пути, проксирование настроено в nginx/vite)
|
||||||
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_COLOR_API_URL = '/project/color'
|
||||||
const PROJECT_MOVE_API_URL = '/project/move'
|
const PROJECT_MOVE_API_URL = '/project/move'
|
||||||
const PROJECT_CREATE_API_URL = '/project/create'
|
const PROJECT_CREATE_API_URL = '/project/create'
|
||||||
|
|
||||||
@@ -257,7 +259,7 @@ function MoveProjectScreen({ project, allProjects, onClose, onSuccess, onError }
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Компонент для сортируемого элемента проекта
|
// Компонент для сортируемого элемента проекта
|
||||||
function SortableProjectItem({ project, index, allProjects, onMenuClick }) {
|
function SortableProjectItem({ project, index, allProjects, onMenuClick, onColorClick }) {
|
||||||
const {
|
const {
|
||||||
attributes,
|
attributes,
|
||||||
listeners,
|
listeners,
|
||||||
@@ -274,7 +276,7 @@ function SortableProjectItem({ project, index, allProjects, onMenuClick }) {
|
|||||||
opacity: isDragging ? 0.5 : 1,
|
opacity: isDragging ? 0.5 : 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectColor = getProjectColor(project.name, allProjects)
|
const projectColor = getProjectColor(project.name, allProjects, project.color)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -301,10 +303,17 @@ function SortableProjectItem({ project, index, allProjects, onMenuClick }) {
|
|||||||
<circle cx="13" cy="13" r="1.5" />
|
<circle cx="13" cy="13" r="1.5" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<button
|
||||||
className="w-3 h-3 rounded-full flex-shrink-0"
|
onClick={(e) => {
|
||||||
|
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 }}
|
style={{ backgroundColor: projectColor }}
|
||||||
></div>
|
title="Выбрать цвет"
|
||||||
|
></button>
|
||||||
<span className="font-semibold text-gray-800">{project.name}</span>
|
<span className="font-semibold text-gray-800">{project.name}</span>
|
||||||
</div>
|
</div>
|
||||||
{onMenuClick && (
|
{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 (
|
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>
|
||||||
@@ -362,6 +371,7 @@ function PrioritySlot({ title, projects, allProjects, onMenuClick, maxItems = nu
|
|||||||
index={index}
|
index={index}
|
||||||
allProjects={allProjects}
|
allProjects={allProjects}
|
||||||
onMenuClick={onMenuClick}
|
onMenuClick={onMenuClick}
|
||||||
|
onColorClick={onColorClick}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{onAddClick && containerId === 'low' && (
|
{onAddClick && containerId === 'low' && (
|
||||||
@@ -405,6 +415,8 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
|
|||||||
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 [showAddScreen, setShowAddScreen] = useState(false) // Для экрана добавления
|
||||||
|
const [showColorPicker, setShowColorPicker] = useState(false) // Для модального окна выбора цвета
|
||||||
|
const [selectedProjectForColor, setSelectedProjectForColor] = useState(null) // Проект для выбора цвета
|
||||||
|
|
||||||
|
|
||||||
const scrollContainerRef = useRef(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 id = item?.project_id ?? item?.id ?? item?.projectId ?? null
|
||||||
const priorityValue = item?.priority ?? item?.priority_value ?? item?.priority_level ?? 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)
|
.filter(Boolean)
|
||||||
|
|
||||||
@@ -458,7 +471,7 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
|
|||||||
const low = []
|
const low = []
|
||||||
|
|
||||||
uniqueProjects.forEach(item => {
|
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) {
|
if (item.priority === 1) {
|
||||||
max.push(projectEntry)
|
max.push(projectEntry)
|
||||||
} else if (item.priority === 2) {
|
} 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 = () => {
|
const closeModal = () => {
|
||||||
setSelectedProject(null)
|
setSelectedProject(null)
|
||||||
}
|
}
|
||||||
@@ -912,6 +957,7 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
|
|||||||
projects={maxPriority}
|
projects={maxPriority}
|
||||||
allProjects={allProjects}
|
allProjects={allProjects}
|
||||||
onMenuClick={handleMenuClick}
|
onMenuClick={handleMenuClick}
|
||||||
|
onColorClick={handleColorClick}
|
||||||
maxItems={1}
|
maxItems={1}
|
||||||
containerId="max"
|
containerId="max"
|
||||||
/>
|
/>
|
||||||
@@ -923,6 +969,7 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
|
|||||||
projects={mediumPriority}
|
projects={mediumPriority}
|
||||||
allProjects={allProjects}
|
allProjects={allProjects}
|
||||||
onMenuClick={handleMenuClick}
|
onMenuClick={handleMenuClick}
|
||||||
|
onColorClick={handleColorClick}
|
||||||
maxItems={2}
|
maxItems={2}
|
||||||
containerId="medium"
|
containerId="medium"
|
||||||
/>
|
/>
|
||||||
@@ -934,6 +981,7 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
|
|||||||
projects={lowPriority}
|
projects={lowPriority}
|
||||||
allProjects={allProjects}
|
allProjects={allProjects}
|
||||||
onMenuClick={handleMenuClick}
|
onMenuClick={handleMenuClick}
|
||||||
|
onColorClick={handleColorClick}
|
||||||
containerId="low"
|
containerId="low"
|
||||||
onAddClick={() => setShowAddScreen(true)}
|
onAddClick={() => setShowAddScreen(true)}
|
||||||
/>
|
/>
|
||||||
@@ -954,7 +1002,7 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div
|
<div
|
||||||
className="w-3 h-3 rounded-full"
|
className="w-3 h-3 rounded-full"
|
||||||
style={{ backgroundColor: getProjectColor(activeProject.name, allProjects) }}
|
style={{ backgroundColor: getProjectColor(activeProject.name, allProjects, activeProject.color) }}
|
||||||
></div>
|
></div>
|
||||||
<span className="font-semibold text-gray-800">{activeProject.name}</span>
|
<span className="font-semibold text-gray-800">{activeProject.name}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1030,6 +1078,18 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Модальное окно выбора цвета */}
|
||||||
|
{showColorPicker && selectedProjectForColor && (
|
||||||
|
<ColorPickerModal
|
||||||
|
onClose={() => {
|
||||||
|
setShowColorPicker(false)
|
||||||
|
setSelectedProjectForColor(null)
|
||||||
|
}}
|
||||||
|
onColorSelect={handleColorSelect}
|
||||||
|
currentColor={selectedProjectForColor.color || getProjectColor(selectedProjectForColor.name, allProjects)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{toastMessage && (
|
{toastMessage && (
|
||||||
<Toast
|
<Toast
|
||||||
message={toastMessage.text}
|
message={toastMessage.text}
|
||||||
|
|||||||
@@ -152,12 +152,17 @@ function WeekProgressChart({ data, allProjectsSorted, currentWeekData, selectedP
|
|||||||
|
|
||||||
// Используем абсолютные значения (баллы)
|
// Используем абсолютные значения (баллы)
|
||||||
// Сортируем проекты так же, как в полной статистике (по priority и min_goal_score)
|
// Сортируем проекты так же, как в полной статистике (по priority и min_goal_score)
|
||||||
|
// Получаем цвет проекта из данных full-statistics, если доступен
|
||||||
const projectsWithData = weekProjects.map(project => {
|
const projectsWithData = weekProjects.map(project => {
|
||||||
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 {
|
return {
|
||||||
...project,
|
...project,
|
||||||
color
|
color: projectColor
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
export function getProjectColorByIndex(index) {
|
||||||
const hue = (index * 137.508) % 360 // Золотой угол для равномерного распределения цветов
|
const hue = (index * 137.508) % 360 // Золотой угол для равномерного распределения цветов
|
||||||
@@ -39,13 +74,20 @@ export function getAllProjectsSorted(allProjectsData, currentWeekData = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Получает цвет проекта на основе его названия
|
* Получает цвет проекта на основе его названия или цвета из БД
|
||||||
*
|
*
|
||||||
* @param {string} projectName - название проекта
|
* @param {string} projectName - название проекта
|
||||||
* @param {Array} allProjectsSorted - отсортированный список всех проектов
|
* @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)
|
const projectIndex = allProjectsSorted.indexOf(projectName)
|
||||||
return projectIndex >= 0 ? getProjectColorByIndex(projectIndex) : '#9CA3AF'
|
return projectIndex >= 0 ? getProjectColorByIndex(projectIndex) : '#9CA3AF'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user