4.15.0: Добавлена принадлежность желаний к проектам
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m33s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m33s
This commit is contained in:
@@ -378,6 +378,8 @@ type WishlistItem struct {
|
|||||||
UnlockConditions []UnlockConditionDisplay `json:"unlock_conditions,omitempty"`
|
UnlockConditions []UnlockConditionDisplay `json:"unlock_conditions,omitempty"`
|
||||||
LinkedTask *LinkedTask `json:"linked_task,omitempty"`
|
LinkedTask *LinkedTask `json:"linked_task,omitempty"`
|
||||||
TasksCount int `json:"tasks_count,omitempty"` // Количество задач для этого желания
|
TasksCount int `json:"tasks_count,omitempty"` // Количество задач для этого желания
|
||||||
|
ProjectID *int `json:"project_id,omitempty"` // ID проекта, к которому принадлежит желание
|
||||||
|
ProjectName *string `json:"project_name,omitempty"` // Название проекта
|
||||||
}
|
}
|
||||||
|
|
||||||
type UnlockConditionDisplay struct {
|
type UnlockConditionDisplay struct {
|
||||||
@@ -404,6 +406,7 @@ type WishlistRequest struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Price *float64 `json:"price,omitempty"`
|
Price *float64 `json:"price,omitempty"`
|
||||||
Link *string `json:"link,omitempty"`
|
Link *string `json:"link,omitempty"`
|
||||||
|
ProjectID *int `json:"project_id,omitempty"` // ID проекта, к которому принадлежит желание
|
||||||
UnlockConditions []UnlockConditionRequest `json:"unlock_conditions,omitempty"`
|
UnlockConditions []UnlockConditionRequest `json:"unlock_conditions,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9938,6 +9941,8 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
|
|||||||
wi.image_path,
|
wi.image_path,
|
||||||
wi.link,
|
wi.link,
|
||||||
wi.completed,
|
wi.completed,
|
||||||
|
wi.project_id AS item_project_id,
|
||||||
|
wp.name AS item_project_name,
|
||||||
wc.id AS condition_id,
|
wc.id AS condition_id,
|
||||||
wc.display_order,
|
wc.display_order,
|
||||||
wc.task_condition_id,
|
wc.task_condition_id,
|
||||||
@@ -9950,6 +9955,7 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
|
|||||||
sc.required_points,
|
sc.required_points,
|
||||||
sc.start_date
|
sc.start_date
|
||||||
FROM wishlist_items wi
|
FROM wishlist_items wi
|
||||||
|
LEFT JOIN projects wp ON wi.project_id = wp.id AND wp.deleted = FALSE
|
||||||
LEFT JOIN wishlist_conditions wc ON wi.id = wc.wishlist_item_id
|
LEFT JOIN wishlist_conditions wc ON wi.id = wc.wishlist_item_id
|
||||||
LEFT JOIN task_conditions tc ON wc.task_condition_id = tc.id
|
LEFT JOIN task_conditions tc ON wc.task_condition_id = tc.id
|
||||||
LEFT JOIN tasks t ON tc.task_id = t.id AND t.deleted = FALSE
|
LEFT JOIN tasks t ON tc.task_id = t.id AND t.deleted = FALSE
|
||||||
@@ -9976,6 +9982,8 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
|
|||||||
var price sql.NullFloat64
|
var price sql.NullFloat64
|
||||||
var imagePath, link sql.NullString
|
var imagePath, link sql.NullString
|
||||||
var completed bool
|
var completed bool
|
||||||
|
var itemProjectID sql.NullInt64
|
||||||
|
var itemProjectName sql.NullString
|
||||||
|
|
||||||
var conditionID, displayOrder sql.NullInt64
|
var conditionID, displayOrder sql.NullInt64
|
||||||
var taskConditionID, scoreConditionID sql.NullInt64
|
var taskConditionID, scoreConditionID sql.NullInt64
|
||||||
@@ -9989,6 +9997,7 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
|
|||||||
|
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&itemID, &name, &price, &imagePath, &link, &completed,
|
&itemID, &name, &price, &imagePath, &link, &completed,
|
||||||
|
&itemProjectID, &itemProjectName,
|
||||||
&conditionID, &displayOrder,
|
&conditionID, &displayOrder,
|
||||||
&taskConditionID, &scoreConditionID, &conditionUserID,
|
&taskConditionID, &scoreConditionID, &conditionUserID,
|
||||||
&taskID, &taskName,
|
&taskID, &taskName,
|
||||||
@@ -10019,6 +10028,14 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
|
|||||||
l := link.String
|
l := link.String
|
||||||
item.Link = &l
|
item.Link = &l
|
||||||
}
|
}
|
||||||
|
if itemProjectID.Valid {
|
||||||
|
projectIDVal := int(itemProjectID.Int64)
|
||||||
|
item.ProjectID = &projectIDVal
|
||||||
|
}
|
||||||
|
if itemProjectName.Valid {
|
||||||
|
projectNameVal := itemProjectName.String
|
||||||
|
item.ProjectName = &projectNameVal
|
||||||
|
}
|
||||||
itemsMap[itemID] = item
|
itemsMap[itemID] = item
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10681,10 +10698,10 @@ func (a *App) createWishlistHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var wishlistID int
|
var wishlistID int
|
||||||
err = tx.QueryRow(`
|
err = tx.QueryRow(`
|
||||||
INSERT INTO wishlist_items (user_id, author_id, name, price, link, completed, deleted)
|
INSERT INTO wishlist_items (user_id, author_id, name, price, link, project_id, completed, deleted)
|
||||||
VALUES ($1, $1, $2, $3, $4, FALSE, FALSE)
|
VALUES ($1, $1, $2, $3, $4, $5, FALSE, FALSE)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`, userID, strings.TrimSpace(req.Name), req.Price, req.Link).Scan(&wishlistID)
|
`, userID, strings.TrimSpace(req.Name), req.Price, req.Link, req.ProjectID).Scan(&wishlistID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error creating wishlist item: %v", err)
|
log.Printf("Error creating wishlist item: %v", err)
|
||||||
@@ -10895,6 +10912,8 @@ func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
wi.image_path,
|
wi.image_path,
|
||||||
wi.link,
|
wi.link,
|
||||||
wi.completed,
|
wi.completed,
|
||||||
|
wi.project_id AS item_project_id,
|
||||||
|
wp.name AS item_project_name,
|
||||||
wc.id AS condition_id,
|
wc.id AS condition_id,
|
||||||
wc.display_order,
|
wc.display_order,
|
||||||
wc.task_condition_id,
|
wc.task_condition_id,
|
||||||
@@ -10907,6 +10926,7 @@ func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
sc.required_points,
|
sc.required_points,
|
||||||
sc.start_date
|
sc.start_date
|
||||||
FROM wishlist_items wi
|
FROM wishlist_items wi
|
||||||
|
LEFT JOIN projects wp ON wi.project_id = wp.id AND wp.deleted = FALSE
|
||||||
LEFT JOIN wishlist_conditions wc ON wi.id = wc.wishlist_item_id
|
LEFT JOIN wishlist_conditions wc ON wi.id = wc.wishlist_item_id
|
||||||
LEFT JOIN task_conditions tc ON wc.task_condition_id = tc.id
|
LEFT JOIN task_conditions tc ON wc.task_condition_id = tc.id
|
||||||
LEFT JOIN tasks t ON tc.task_id = t.id AND t.deleted = FALSE
|
LEFT JOIN tasks t ON tc.task_id = t.id AND t.deleted = FALSE
|
||||||
@@ -10933,6 +10953,8 @@ func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
var imagePath sql.NullString
|
var imagePath sql.NullString
|
||||||
var link sql.NullString
|
var link sql.NullString
|
||||||
var completed bool
|
var completed bool
|
||||||
|
var itemProjectID sql.NullInt64
|
||||||
|
var itemProjectName sql.NullString
|
||||||
var conditionID sql.NullInt64
|
var conditionID sql.NullInt64
|
||||||
var displayOrder sql.NullInt64
|
var displayOrder sql.NullInt64
|
||||||
var taskConditionID sql.NullInt64
|
var taskConditionID sql.NullInt64
|
||||||
@@ -10946,7 +10968,7 @@ func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
var startDate sql.NullTime
|
var startDate sql.NullTime
|
||||||
|
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&itemID, &name, &price, &imagePath, &link, &completed,
|
&itemID, &name, &price, &imagePath, &link, &completed, &itemProjectID, &itemProjectName,
|
||||||
&conditionID, &displayOrder, &taskConditionID, &scoreConditionID, &conditionUserID,
|
&conditionID, &displayOrder, &taskConditionID, &scoreConditionID, &conditionUserID,
|
||||||
&taskID, &taskName, &projectID, &projectName, &requiredPoints, &startDate,
|
&taskID, &taskName, &projectID, &projectName, &requiredPoints, &startDate,
|
||||||
)
|
)
|
||||||
@@ -10976,6 +10998,14 @@ func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if link.Valid {
|
if link.Valid {
|
||||||
item.Link = &link.String
|
item.Link = &link.String
|
||||||
}
|
}
|
||||||
|
if itemProjectID.Valid {
|
||||||
|
projectIDVal := int(itemProjectID.Int64)
|
||||||
|
item.ProjectID = &projectIDVal
|
||||||
|
}
|
||||||
|
if itemProjectName.Valid {
|
||||||
|
projectNameVal := itemProjectName.String
|
||||||
|
item.ProjectName = &projectNameVal
|
||||||
|
}
|
||||||
itemsMap[itemID] = item
|
itemsMap[itemID] = item
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11231,9 +11261,9 @@ func (a *App) updateWishlistHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Обновляем желание (не проверяем user_id в WHERE, так как доступ уже проверен выше)
|
// Обновляем желание (не проверяем user_id в WHERE, так как доступ уже проверен выше)
|
||||||
_, err = tx.Exec(`
|
_, err = tx.Exec(`
|
||||||
UPDATE wishlist_items
|
UPDATE wishlist_items
|
||||||
SET name = $1, price = $2, link = $3, updated_at = NOW()
|
SET name = $1, price = $2, link = $3, project_id = $4, updated_at = NOW()
|
||||||
WHERE id = $4
|
WHERE id = $5
|
||||||
`, strings.TrimSpace(req.Name), req.Price, req.Link, itemID)
|
`, strings.TrimSpace(req.Name), req.Price, req.Link, req.ProjectID, itemID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error updating wishlist item: %v", err)
|
log.Printf("Error updating wishlist item: %v", err)
|
||||||
@@ -13014,6 +13044,8 @@ func (a *App) getBoardCompletedHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
wi.image_path,
|
wi.image_path,
|
||||||
wi.link,
|
wi.link,
|
||||||
wi.completed,
|
wi.completed,
|
||||||
|
wi.project_id AS item_project_id,
|
||||||
|
wp.name AS item_project_name,
|
||||||
wc.id AS condition_id,
|
wc.id AS condition_id,
|
||||||
wc.display_order,
|
wc.display_order,
|
||||||
wc.task_condition_id,
|
wc.task_condition_id,
|
||||||
@@ -13027,6 +13059,7 @@ func (a *App) getBoardCompletedHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
sc.start_date,
|
sc.start_date,
|
||||||
COALESCE(u.name, u.email) AS user_name
|
COALESCE(u.name, u.email) AS user_name
|
||||||
FROM wishlist_items wi
|
FROM wishlist_items wi
|
||||||
|
LEFT JOIN projects wp ON wi.project_id = wp.id AND wp.deleted = FALSE
|
||||||
LEFT JOIN wishlist_conditions wc ON wi.id = wc.wishlist_item_id
|
LEFT JOIN wishlist_conditions wc ON wi.id = wc.wishlist_item_id
|
||||||
LEFT JOIN task_conditions tc ON wc.task_condition_id = tc.id
|
LEFT JOIN task_conditions tc ON wc.task_condition_id = tc.id
|
||||||
LEFT JOIN tasks t ON tc.task_id = t.id AND t.deleted = FALSE
|
LEFT JOIN tasks t ON tc.task_id = t.id AND t.deleted = FALSE
|
||||||
@@ -13056,6 +13089,8 @@ func (a *App) getBoardCompletedHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
var imagePath sql.NullString
|
var imagePath sql.NullString
|
||||||
var link sql.NullString
|
var link sql.NullString
|
||||||
var completed bool
|
var completed bool
|
||||||
|
var itemProjectID sql.NullInt64
|
||||||
|
var itemProjectName sql.NullString
|
||||||
var conditionID sql.NullInt64
|
var conditionID sql.NullInt64
|
||||||
var displayOrder sql.NullInt64
|
var displayOrder sql.NullInt64
|
||||||
var taskConditionID sql.NullInt64
|
var taskConditionID sql.NullInt64
|
||||||
@@ -13070,7 +13105,7 @@ func (a *App) getBoardCompletedHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
var userName sql.NullString
|
var userName sql.NullString
|
||||||
|
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&itemID, &name, &price, &imagePath, &link, &completed,
|
&itemID, &name, &price, &imagePath, &link, &completed, &itemProjectID, &itemProjectName,
|
||||||
&conditionID, &displayOrder, &taskConditionID, &scoreConditionID, &userIDCond,
|
&conditionID, &displayOrder, &taskConditionID, &scoreConditionID, &userIDCond,
|
||||||
&taskID, &taskName, &projectID, &projectName, &requiredPoints, &startDate, &userName,
|
&taskID, &taskName, &projectID, &projectName, &requiredPoints, &startDate, &userName,
|
||||||
)
|
)
|
||||||
@@ -13100,6 +13135,8 @@ func (a *App) getBoardCompletedHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if link.Valid {
|
if link.Valid {
|
||||||
item.Link = &link.String
|
item.Link = &link.String
|
||||||
}
|
}
|
||||||
|
// Для завершённых желаний не устанавливаем project_id и project_name
|
||||||
|
// Они отображаются отдельно без группировки по проектам
|
||||||
itemsMap[itemID] = item
|
itemsMap[itemID] = item
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13305,6 +13342,8 @@ func (a *App) getWishlistItemsByBoard(boardID int, userID int) ([]WishlistItem,
|
|||||||
wi.image_path,
|
wi.image_path,
|
||||||
wi.link,
|
wi.link,
|
||||||
wi.completed,
|
wi.completed,
|
||||||
|
wi.project_id AS item_project_id,
|
||||||
|
wp.name AS item_project_name,
|
||||||
COALESCE(wi.author_id, wi.user_id) AS item_owner_id,
|
COALESCE(wi.author_id, wi.user_id) AS item_owner_id,
|
||||||
wc.id AS condition_id,
|
wc.id AS condition_id,
|
||||||
wc.display_order,
|
wc.display_order,
|
||||||
@@ -13318,6 +13357,7 @@ func (a *App) getWishlistItemsByBoard(boardID int, userID int) ([]WishlistItem,
|
|||||||
sc.required_points,
|
sc.required_points,
|
||||||
sc.start_date
|
sc.start_date
|
||||||
FROM wishlist_items wi
|
FROM wishlist_items wi
|
||||||
|
LEFT JOIN projects wp ON wi.project_id = wp.id AND wp.deleted = FALSE
|
||||||
LEFT JOIN wishlist_conditions wc ON wi.id = wc.wishlist_item_id
|
LEFT JOIN wishlist_conditions wc ON wi.id = wc.wishlist_item_id
|
||||||
LEFT JOIN task_conditions tc ON wc.task_condition_id = tc.id
|
LEFT JOIN task_conditions tc ON wc.task_condition_id = tc.id
|
||||||
LEFT JOIN tasks t ON tc.task_id = t.id AND t.deleted = FALSE
|
LEFT JOIN tasks t ON tc.task_id = t.id AND t.deleted = FALSE
|
||||||
@@ -13344,6 +13384,8 @@ func (a *App) getWishlistItemsByBoard(boardID int, userID int) ([]WishlistItem,
|
|||||||
var imagePath sql.NullString
|
var imagePath sql.NullString
|
||||||
var link sql.NullString
|
var link sql.NullString
|
||||||
var completed bool
|
var completed bool
|
||||||
|
var itemProjectID sql.NullInt64
|
||||||
|
var itemProjectName sql.NullString
|
||||||
var itemOwnerID sql.NullInt64
|
var itemOwnerID sql.NullInt64
|
||||||
var conditionID sql.NullInt64
|
var conditionID sql.NullInt64
|
||||||
var displayOrder sql.NullInt64
|
var displayOrder sql.NullInt64
|
||||||
@@ -13358,7 +13400,7 @@ func (a *App) getWishlistItemsByBoard(boardID int, userID int) ([]WishlistItem,
|
|||||||
var startDate sql.NullTime
|
var startDate sql.NullTime
|
||||||
|
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&itemID, &name, &price, &imagePath, &link, &completed, &itemOwnerID,
|
&itemID, &name, &price, &imagePath, &link, &completed, &itemProjectID, &itemProjectName, &itemOwnerID,
|
||||||
&conditionID, &displayOrder, &taskConditionID, &scoreConditionID, &conditionUserID,
|
&conditionID, &displayOrder, &taskConditionID, &scoreConditionID, &conditionUserID,
|
||||||
&taskID, &taskName, &projectID, &projectName, &requiredPoints, &startDate,
|
&taskID, &taskName, &projectID, &projectName, &requiredPoints, &startDate,
|
||||||
)
|
)
|
||||||
@@ -13388,6 +13430,14 @@ func (a *App) getWishlistItemsByBoard(boardID int, userID int) ([]WishlistItem,
|
|||||||
if link.Valid {
|
if link.Valid {
|
||||||
item.Link = &link.String
|
item.Link = &link.String
|
||||||
}
|
}
|
||||||
|
if itemProjectID.Valid {
|
||||||
|
projectIDVal := int(itemProjectID.Int64)
|
||||||
|
item.ProjectID = &projectIDVal
|
||||||
|
}
|
||||||
|
if itemProjectName.Valid {
|
||||||
|
projectNameVal := itemProjectName.String
|
||||||
|
item.ProjectName = &projectNameVal
|
||||||
|
}
|
||||||
itemsMap[itemID] = item
|
itemsMap[itemID] = item
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13657,10 +13707,10 @@ func (a *App) createBoardItemHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var itemID int
|
var itemID int
|
||||||
err = tx.QueryRow(`
|
err = tx.QueryRow(`
|
||||||
INSERT INTO wishlist_items (user_id, board_id, author_id, name, price, link, completed, deleted)
|
INSERT INTO wishlist_items (user_id, board_id, author_id, name, price, link, project_id, completed, deleted)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, FALSE, FALSE)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, FALSE, FALSE)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`, ownerID, boardID, userID, strings.TrimSpace(req.Name), req.Price, req.Link).Scan(&itemID)
|
`, ownerID, boardID, userID, strings.TrimSpace(req.Name), req.Price, req.Link, req.ProjectID).Scan(&itemID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("createBoardItemHandler: Error creating board item: %v", err)
|
log.Printf("createBoardItemHandler: Error creating board item: %v", err)
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- Migration: Remove project_id field from wishlist_items table
|
||||||
|
-- Date: 2026-02-02
|
||||||
|
--
|
||||||
|
-- This migration reverts the addition of project_id field.
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_wishlist_items_project_id;
|
||||||
|
|
||||||
|
ALTER TABLE wishlist_items
|
||||||
|
DROP COLUMN IF EXISTS project_id;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
-- Migration: Add project_id field to wishlist_items table
|
||||||
|
-- Date: 2026-02-02
|
||||||
|
--
|
||||||
|
-- This migration adds project_id field to wishlist_items table to allow
|
||||||
|
-- grouping wishlist items by project. The field is nullable, so existing
|
||||||
|
-- items without a project will remain valid.
|
||||||
|
|
||||||
|
ALTER TABLE wishlist_items
|
||||||
|
ADD COLUMN project_id INTEGER REFERENCES projects(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
CREATE INDEX idx_wishlist_items_project_id ON wishlist_items(project_id);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN wishlist_items.project_id IS 'Project this wishlist item belongs to (optional)';
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "play-life-web",
|
"name": "play-life-web",
|
||||||
"version": "4.14.1",
|
"version": "4.15.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -93,6 +93,53 @@
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wishlist-project-group {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wishlist-project-group-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wishlist-project-group-items {
|
||||||
|
display: flex;
|
||||||
|
overflow-x: auto;
|
||||||
|
gap: 1rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wishlist-project-group-items::-webkit-scrollbar {
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wishlist-project-group-items::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wishlist-project-group-items::-webkit-scrollbar-thumb {
|
||||||
|
background: #888;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wishlist-project-group-items::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wishlist-project-group-items .wishlist-card {
|
||||||
|
flex: 0 0 150px;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wishlist-no-project {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.wishlist-card {
|
.wishlist-card {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react'
|
import React, { useState, useEffect, useRef, useMemo } from 'react'
|
||||||
import { useAuth } from './auth/AuthContext'
|
import { useAuth } from './auth/AuthContext'
|
||||||
import BoardSelector from './BoardSelector'
|
import BoardSelector from './BoardSelector'
|
||||||
import LoadingError from './LoadingError'
|
import LoadingError from './LoadingError'
|
||||||
import WishlistDetail from './WishlistDetail'
|
import WishlistDetail from './WishlistDetail'
|
||||||
|
import { sortProjectsLikeCurrentWeek } from '../utils/projectUtils'
|
||||||
import './Wishlist.css'
|
import './Wishlist.css'
|
||||||
|
|
||||||
const API_URL = '/api/wishlist'
|
const API_URL = '/api/wishlist'
|
||||||
@@ -45,6 +46,7 @@ function Wishlist({ onNavigate, refreshTrigger = 0, isActive = false, initialBoa
|
|||||||
const [completedLoading, setCompletedLoading] = useState(false)
|
const [completedLoading, setCompletedLoading] = useState(false)
|
||||||
const [selectedItem, setSelectedItem] = useState(null)
|
const [selectedItem, setSelectedItem] = useState(null)
|
||||||
const [selectedWishlistForDetail, setSelectedWishlistForDetail] = useState(null)
|
const [selectedWishlistForDetail, setSelectedWishlistForDetail] = useState(null)
|
||||||
|
const [currentWeekData, setCurrentWeekData] = useState(null)
|
||||||
const fetchingRef = useRef(false)
|
const fetchingRef = useRef(false)
|
||||||
const fetchingCompletedRef = useRef(false)
|
const fetchingCompletedRef = useRef(false)
|
||||||
const initialFetchDoneRef = useRef(false)
|
const initialFetchDoneRef = useRef(false)
|
||||||
@@ -234,6 +236,24 @@ function Wishlist({ onNavigate, refreshTrigger = 0, isActive = false, initialBoa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Загрузка данных текущей недели для сортировки проектов
|
||||||
|
const fetchCurrentWeek = async () => {
|
||||||
|
try {
|
||||||
|
const response = await authFetch('/api/current-week')
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
// Обрабатываем ответ: приходит массив с одним объектом [{total: ..., projects: [...]}]
|
||||||
|
if (Array.isArray(data) && data.length > 0) {
|
||||||
|
setCurrentWeekData(data[0])
|
||||||
|
} else if (data && typeof data === 'object') {
|
||||||
|
setCurrentWeekData(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading current week data:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Первая инициализация
|
// Первая инициализация
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!initialFetchDoneRef.current) {
|
if (!initialFetchDoneRef.current) {
|
||||||
@@ -247,6 +267,9 @@ function Wishlist({ onNavigate, refreshTrigger = 0, isActive = false, initialBoa
|
|||||||
|
|
||||||
// Загружаем доски с сервера
|
// Загружаем доски с сервера
|
||||||
fetchBoards()
|
fetchBoards()
|
||||||
|
|
||||||
|
// Загружаем данные текущей недели для сортировки проектов
|
||||||
|
fetchCurrentWeek()
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -557,6 +580,60 @@ function Wishlist({ onNavigate, refreshTrigger = 0, isActive = false, initialBoa
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Группируем желания по проектам
|
||||||
|
const groupedItems = useMemo(() => {
|
||||||
|
const groups = {}
|
||||||
|
const noProjectItems = []
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
if (item.project_id && item.project_name) {
|
||||||
|
const projectId = item.project_id
|
||||||
|
if (!groups[projectId]) {
|
||||||
|
groups[projectId] = {
|
||||||
|
projectId: projectId,
|
||||||
|
projectName: item.project_name,
|
||||||
|
items: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
groups[projectId].items.push(item)
|
||||||
|
} else {
|
||||||
|
noProjectItems.push(item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Сортируем группы проектов
|
||||||
|
const projectIds = Object.keys(groups)
|
||||||
|
if (currentWeekData && projectIds.length > 0) {
|
||||||
|
const projectNames = projectIds.map(id => groups[id].projectName)
|
||||||
|
const sortedProjectNames = sortProjectsLikeCurrentWeek(projectNames, currentWeekData)
|
||||||
|
|
||||||
|
// Создаем отсортированный массив групп
|
||||||
|
const sortedGroups = []
|
||||||
|
sortedProjectNames.forEach(projectName => {
|
||||||
|
const group = Object.values(groups).find(g => g.projectName === projectName)
|
||||||
|
if (group) {
|
||||||
|
sortedGroups.push(group)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Добавляем группы, которых нет в currentWeekData (если есть)
|
||||||
|
Object.values(groups).forEach(group => {
|
||||||
|
if (!sortedProjectNames.includes(group.projectName)) {
|
||||||
|
sortedGroups.push(group)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return { groups: sortedGroups, noProjectItems }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если нет данных текущей недели, сортируем по алфавиту
|
||||||
|
const sortedGroups = Object.values(groups).sort((a, b) =>
|
||||||
|
a.projectName.localeCompare(b.projectName)
|
||||||
|
)
|
||||||
|
|
||||||
|
return { groups: sortedGroups, noProjectItems }
|
||||||
|
}, [items, currentWeekData])
|
||||||
|
|
||||||
const renderItem = (item) => {
|
const renderItem = (item) => {
|
||||||
const isFaded = (!item.unlocked && !item.completed) || item.completed
|
const isFaded = (!item.unlocked && !item.completed) || item.completed
|
||||||
|
|
||||||
@@ -646,9 +723,24 @@ function Wishlist({ onNavigate, refreshTrigger = 0, isActive = false, initialBoa
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="wishlist-grid">
|
{/* Группы проектов */}
|
||||||
{items.map(renderItem)}
|
{groupedItems.groups.map(group => (
|
||||||
</div>
|
<div key={group.projectId} className="wishlist-project-group">
|
||||||
|
<div className="wishlist-project-group-title">{group.projectName}</div>
|
||||||
|
<div className="wishlist-project-group-items">
|
||||||
|
{group.items.map(renderItem)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Желания без проекта */}
|
||||||
|
{groupedItems.noProjectItems.length > 0 && (
|
||||||
|
<div className="wishlist-no-project">
|
||||||
|
<div className="wishlist-grid">
|
||||||
|
{groupedItems.noProjectItems.map(renderItem)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Завершённые */}
|
{/* Завершённые */}
|
||||||
{completedCount > 0 && (
|
{completedCount > 0 && (
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
|
|||||||
const [editingConditionIndex, setEditingConditionIndex] = useState(null)
|
const [editingConditionIndex, setEditingConditionIndex] = useState(null)
|
||||||
const [tasks, setTasks] = useState([])
|
const [tasks, setTasks] = useState([])
|
||||||
const [projects, setProjects] = useState([])
|
const [projects, setProjects] = useState([])
|
||||||
|
const [selectedProjectId, setSelectedProjectId] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [toastMessage, setToastMessage] = useState(null)
|
const [toastMessage, setToastMessage] = useState(null)
|
||||||
@@ -86,6 +87,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
|
|||||||
setPrice(data.price ? String(data.price) : '')
|
setPrice(data.price ? String(data.price) : '')
|
||||||
setLink(data.link || '')
|
setLink(data.link || '')
|
||||||
setImageUrl(data.image_url || null)
|
setImageUrl(data.image_url || null)
|
||||||
|
setSelectedProjectId(data.project_id ? String(data.project_id) : '')
|
||||||
if (data.unlock_conditions) {
|
if (data.unlock_conditions) {
|
||||||
setUnlockConditions(data.unlock_conditions.map((cond, idx) => ({
|
setUnlockConditions(data.unlock_conditions.map((cond, idx) => ({
|
||||||
id: cond.id || null,
|
id: cond.id || null,
|
||||||
@@ -242,6 +244,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
|
|||||||
setLink(data.link || '')
|
setLink(data.link || '')
|
||||||
setImageUrl(data.image_url || null)
|
setImageUrl(data.image_url || null)
|
||||||
setImageFile(null) // Сбрасываем imageFile при загрузке существующего желания
|
setImageFile(null) // Сбрасываем imageFile при загрузке существующего желания
|
||||||
|
setSelectedProjectId(data.project_id ? String(data.project_id) : '')
|
||||||
if (data.unlock_conditions) {
|
if (data.unlock_conditions) {
|
||||||
setUnlockConditions(data.unlock_conditions.map((cond, idx) => ({
|
setUnlockConditions(data.unlock_conditions.map((cond, idx) => ({
|
||||||
id: cond.id || null,
|
id: cond.id || null,
|
||||||
@@ -268,6 +271,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
|
|||||||
setLink(data.link || '')
|
setLink(data.link || '')
|
||||||
setImageUrl(data.image_url || null)
|
setImageUrl(data.image_url || null)
|
||||||
setImageFile(null)
|
setImageFile(null)
|
||||||
|
setSelectedProjectId(data.project_id ? String(data.project_id) : '')
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
@@ -283,6 +287,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
|
|||||||
setImageUrl(null)
|
setImageUrl(null)
|
||||||
setImageFile(null)
|
setImageFile(null)
|
||||||
setUnlockConditions([])
|
setUnlockConditions([])
|
||||||
|
setSelectedProjectId('')
|
||||||
setError('')
|
setError('')
|
||||||
setShowCropper(false)
|
setShowCropper(false)
|
||||||
setCrop({ x: 0, y: 0 })
|
setCrop({ x: 0, y: 0 })
|
||||||
@@ -553,6 +558,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
|
|||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
price: price ? parseFloat(price) : null,
|
price: price ? parseFloat(price) : null,
|
||||||
link: link.trim() || null,
|
link: link.trim() || null,
|
||||||
|
project_id: selectedProjectId ? parseInt(selectedProjectId, 10) : null,
|
||||||
unlock_conditions: unlockConditions.map(cond => ({
|
unlock_conditions: unlockConditions.map(cond => ({
|
||||||
id: cond.id || null,
|
id: cond.id || null,
|
||||||
type: cond.type,
|
type: cond.type,
|
||||||
@@ -841,6 +847,23 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="project">Принадлежность к проекту</label>
|
||||||
|
<select
|
||||||
|
id="project"
|
||||||
|
value={selectedProjectId}
|
||||||
|
onChange={(e) => setSelectedProjectId(e.target.value)}
|
||||||
|
className="form-input"
|
||||||
|
>
|
||||||
|
<option value="">Не выбран</option>
|
||||||
|
{projects.map(project => (
|
||||||
|
<option key={project.project_id} value={project.project_id}>
|
||||||
|
{project.project_name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
{error && <div className="error-message">{error}</div>}
|
{error && <div className="error-message">{error}</div>}
|
||||||
|
|
||||||
<div className="form-actions">
|
<div className="form-actions">
|
||||||
|
|||||||
Reference in New Issue
Block a user