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 (