Bladeren bron

feat: restore endpoint + кнопка восстановления в админке

- PlaceRepo.Restore — UPDATE status='published', deleted_at=NULL
- PlaceService.Restore — ModerationLog + вызов репозитория
- PlaceHandler.Restore — POST /places/{id}/restore
- Роут в moderator group в main.go
- Кнопка 'Восстановить' во вкладке 'Удалённые' админки
neyrogovnarik 1 maand geleden
bovenliggende
commit
32d73d5eb0

+ 1 - 0
backend/cmd/api/main.go

@@ -253,6 +253,7 @@ func main() {
 				r.Use(adminLimiter.Middleware())
 			}
 			r.Post("/places/{id}/moderate", placeHandler.Moderate)
+			r.Post("/places/{id}/restore", placeHandler.Restore)
 			r.Get("/admin/users", userHandler.AdminListUsers)
 			r.Post("/admin/users", userHandler.AdminCreateUser)
 			r.Patch("/admin/users/{id}", userHandler.AdminUpdateUser)

+ 12 - 0
backend/internal/handlers/places.go

@@ -394,3 +394,15 @@ func (h *PlaceHandler) HardDelete(w http.ResponseWriter, r *http.Request) {
 
 	w.WriteHeader(http.StatusNoContent)
 }
+
+func (h *PlaceHandler) Restore(w http.ResponseWriter, r *http.Request) {
+	id := chi.URLParam(r, "id")
+	userID := middleware.GetUserID(r.Context())
+
+	if err := h.placeSvc.Restore(r.Context(), id, userID); err != nil {
+		writeError(w, http.StatusInternalServerError, "failed to restore place", err)
+		return
+	}
+
+	writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
+}

+ 7 - 0
backend/internal/repository/places.go

@@ -178,6 +178,13 @@ func (r *PlaceRepo) HardDelete(ctx context.Context, id string) error {
 	return err
 }
 
+func (r *PlaceRepo) Restore(ctx context.Context, id string) error {
+	_, err := r.pool.Exec(ctx,
+		`UPDATE places SET status='published', deleted_at=NULL, published_at=now(), updated_at=now()
+		 WHERE id=$1 AND deleted_at IS NOT NULL`, id)
+	return err
+}
+
 func (r *PlaceRepo) GetByIDRaw(ctx context.Context, id string) (*models.Place, error) {
 	row := r.pool.QueryRow(ctx,
 		`SELECT id, type, owner_id, title, description, address,

+ 25 - 0
backend/internal/services/places.go

@@ -61,6 +61,7 @@ type PlaceRepo interface {
 	UpdateStatus(ctx context.Context, id, status, comment string) error
 	SoftDelete(ctx context.Context, id string) error
 	HardDelete(ctx context.Context, id string) error
+	Restore(ctx context.Context, id string) error
 	GetPlaceImages(ctx context.Context, placeID string) ([]models.PlaceImage, error)
 	SetTags(ctx context.Context, placeID string, tags []models.Tag) error
 	SetFeatures(ctx context.Context, placeID string, features []models.Feature) error
@@ -376,3 +377,27 @@ func (s *PlaceService) HardDelete(ctx context.Context, id, moderatorID string) e
 
 	return s.placeRepo.HardDelete(ctx, id)
 }
+
+func (s *PlaceService) Restore(ctx context.Context, id, moderatorID string) error {
+	place, err := s.placeRepo.GetByIDRaw(ctx, id)
+	if err != nil {
+		return err
+	}
+	if place == nil {
+		return nil
+	}
+
+	entry := &models.ModerationLog{
+		ModeratorID: moderatorID,
+		TargetType:  "place",
+		TargetID:    id,
+		Action:      "restore",
+		OldStatus:   &place.Status,
+		NewStatus:   pointer.Str("published"),
+	}
+	if err := s.moderationLogRepo.Create(ctx, entry); err != nil {
+		return fmt.Errorf("log restore: %w", err)
+	}
+
+	return s.placeRepo.Restore(ctx, id)
+}

+ 18 - 3
frontend/src/app/admin/page.tsx

@@ -92,6 +92,16 @@ export default function AdminModerationPage() {
     }
   }
 
+  const restore = async (id: string) => {
+    try {
+      await api.post(`/places/${id}/restore`)
+      setPlaces((prev) => prev.filter((p) => p.id !== id))
+      setActionMsg('Место восстановлено')
+    } catch (err: any) {
+      setActionMsg(err.message || 'Ошибка при восстановлении')
+    }
+  }
+
   return (
     <div>
       <h1 className="mb-6 text-2xl font-bold text-white">Модерация</h1>
@@ -162,9 +172,14 @@ export default function AdminModerationPage() {
                   </>
                 )}
                 {filter === 'deleted' && (
-                  <button onClick={() => setHardDeletePlace(place)}
-                    className="rounded-lg bg-red-700 px-4 py-2 text-sm text-white hover:bg-red-600 transition"
-                  >Полностью удалить</button>
+                  <>
+                    <button onClick={() => restore(place.id)}
+                      className="rounded-lg bg-green-700 px-4 py-2 text-sm text-white hover:bg-green-600 transition"
+                    >Восстановить</button>
+                    <button onClick={() => setHardDeletePlace(place)}
+                      className="rounded-lg bg-red-700 px-4 py-2 text-sm text-white hover:bg-red-600 transition"
+                    >Полностью удалить</button>
+                  </>
                 )}
               </div>
             </div>