From df3cced995731ee24e10761be0a7144e4c977527 Mon Sep 17 00:00:00 2001 From: poignatov Date: Tue, 3 Feb 2026 18:42:44 +0300 Subject: [PATCH] =?UTF-8?q?4.13.0:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=B7=D0=B0=D0=BF=D0=B8=D1=81=D0=B5=D0=B9=20=D0=B2=20?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D1=82=D0=B8=D1=81=D1=82=D0=B8=D0=BA=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION | 2 +- play-life-backend/main.go | 69 +++++++++++++ play-life-web/package.json | 2 +- .../src/components/FullStatistics.jsx | 1 + .../src/components/TodayEntriesList.jsx | 98 ++++++++++++++++++- .../src/components/WeekProgressChart.jsx | 4 +- 6 files changed, 168 insertions(+), 8 deletions(-) diff --git a/VERSION b/VERSION index 815588e..813b83b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.12.0 +4.13.0 diff --git a/play-life-backend/main.go b/play-life-backend/main.go index 7080971..a38285f 100644 --- a/play-life-backend/main.go +++ b/play-life-backend/main.go @@ -3754,6 +3754,7 @@ func main() { protected.HandleFunc("/project/create", app.createProjectHandler).Methods("POST", "OPTIONS") protected.HandleFunc("/d2dc349a-0d13-49b2-a8f0-1ab094bfba9b", app.getFullStatisticsHandler).Methods("GET", "OPTIONS") protected.HandleFunc("/api/today-entries", app.getTodayEntriesHandler).Methods("GET", "OPTIONS") + protected.HandleFunc("/api/entries/{id}", app.deleteEntryHandler).Methods("DELETE", "OPTIONS") // Integrations protected.HandleFunc("/api/integrations/telegram", app.getTelegramIntegrationHandler).Methods("GET", "OPTIONS") @@ -6321,6 +6322,74 @@ func (a *App) getTodayEntriesHandler(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(entries) } +// deleteEntryHandler удаляет entry и каскадно удаляет связанные nodes +func (a *App) deleteEntryHandler(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 + } + + vars := mux.Vars(r) + entryIDStr := vars["id"] + entryID, err := strconv.Atoi(entryIDStr) + if err != nil { + sendErrorWithCORS(w, "Invalid entry ID", http.StatusBadRequest) + return + } + + // Проверяем, что entry принадлежит пользователю + var entryUserID int + err = a.DB.QueryRow("SELECT user_id FROM entries WHERE id = $1", entryID).Scan(&entryUserID) + if err == sql.ErrNoRows { + sendErrorWithCORS(w, "Entry not found", http.StatusNotFound) + return + } + if err != nil { + log.Printf("Error checking entry ownership: %v", err) + sendErrorWithCORS(w, err.Error(), http.StatusInternalServerError) + return + } + + // Проверяем права доступа + if entryUserID != userID { + sendErrorWithCORS(w, "Forbidden", http.StatusForbidden) + return + } + + // Удаляем entry (nodes удалятся каскадно из-за ON DELETE CASCADE) + result, err := a.DB.Exec("DELETE FROM entries WHERE id = $1 AND user_id = $2", entryID, userID) + if err != nil { + log.Printf("Error deleting entry: %v", err) + sendErrorWithCORS(w, err.Error(), http.StatusInternalServerError) + return + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + log.Printf("Error getting rows affected: %v", err) + sendErrorWithCORS(w, err.Error(), http.StatusInternalServerError) + return + } + + if rowsAffected == 0 { + sendErrorWithCORS(w, "Entry not found", http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "message": "Entry deleted successfully", + }) +} + // getTelegramIntegrationHandler возвращает текущую telegram интеграцию с deep link func (a *App) getTelegramIntegrationHandler(w http.ResponseWriter, r *http.Request) { if r.Method == "OPTIONS" { diff --git a/play-life-web/package.json b/play-life-web/package.json index 5cccd83..ef1a4bb 100644 --- a/play-life-web/package.json +++ b/play-life-web/package.json @@ -1,6 +1,6 @@ { "name": "play-life-web", - "version": "4.12.0", + "version": "4.13.0", "type": "module", "scripts": { "dev": "vite", diff --git a/play-life-web/src/components/FullStatistics.jsx b/play-life-web/src/components/FullStatistics.jsx index 48d5106..1abde28 100644 --- a/play-life-web/src/components/FullStatistics.jsx +++ b/play-life-web/src/components/FullStatistics.jsx @@ -174,6 +174,7 @@ function FullStatistics({ selectedProject, onClearSelection, data, loading, erro loading={todayEntriesLoading} error={todayEntriesError} onRetry={() => fetchTodayEntries && fetchTodayEntries(false, selectedProject, selectedDate)} + onDelete={() => fetchTodayEntries && fetchTodayEntries(false, selectedProject, selectedDate)} /> )} diff --git a/play-life-web/src/components/TodayEntriesList.jsx b/play-life-web/src/components/TodayEntriesList.jsx index 9d42413..4881e73 100644 --- a/play-life-web/src/components/TodayEntriesList.jsx +++ b/play-life-web/src/components/TodayEntriesList.jsx @@ -1,5 +1,6 @@ -import React from 'react' +import React, { useState } from 'react' import LoadingError from './LoadingError' +import { useAuth } from './auth/AuthContext' // Функция для форматирования скорa (аналогично formatScore из TaskDetail) const formatScore = (num) => { @@ -130,7 +131,49 @@ const formatEntryText = (text, nodes) => { return result.length > 0 ? result : currentText } -function TodayEntriesList({ data, loading, error, onRetry }) { +function TodayEntriesList({ data, loading, error, onRetry, onDelete }) { + const { authFetch } = useAuth() + const [deletingIds, setDeletingIds] = useState(new Set()) + + const handleDelete = async (entryId) => { + if (deletingIds.has(entryId)) return + + if (!window.confirm('Вы уверены, что хотите удалить эту запись? Это действие нельзя отменить.')) { + return + } + + setDeletingIds(prev => new Set(prev).add(entryId)) + + try { + const response = await authFetch(`/api/entries/${entryId}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + }) + + if (!response.ok) { + const errorText = await response.text() + console.error('Delete error:', response.status, errorText) + throw new Error(`Ошибка при удалении записи: ${response.status}`) + } + + // Вызываем callback для обновления данных + if (onDelete) { + onDelete() + } + } catch (err) { + console.error('Delete failed:', err) + alert(err.message || 'Не удалось удалить запись') + } finally { + setDeletingIds(prev => { + const next = new Set(prev) + next.delete(entryId) + return next + }) + } + } + if (loading) { return (
@@ -157,9 +200,56 @@ function TodayEntriesList({ data, loading, error, onRetry }) { {data.map((entry) => (
-
+ +
{formatEntryText(entry.text, entry.nodes)}
{entry.created_date && ( diff --git a/play-life-web/src/components/WeekProgressChart.jsx b/play-life-web/src/components/WeekProgressChart.jsx index 63a9aaa..f1737bb 100644 --- a/play-life-web/src/components/WeekProgressChart.jsx +++ b/play-life-web/src/components/WeekProgressChart.jsx @@ -252,8 +252,8 @@ function WeekProgressChart({ data, allProjectsSorted, currentWeekData, selectedP ) return ( -
-

Прогресс недель

+
+

Прогресс недель

{/* Остальные недели (старые сверху, новые снизу) */} {allOtherWeeksData.map((weekData) => (