diff --git a/app/components/ModeratorRequestDetailsModal.jsx b/app/components/ModeratorRequestDetailsModal.jsx
index e38119b..1716da5 100644
--- a/app/components/ModeratorRequestDetailsModal.jsx
+++ b/app/components/ModeratorRequestDetailsModal.jsx
@@ -13,7 +13,7 @@ const ModeratorRequestModal = ({ request, onClose, onModerated }) => {
// request.status: "pending_moderation" | "approved" | "rejected"
const isApproved = request.status === "approved";
const isRejected = request.status === "rejected";
- const isPending = request.status === "pending_moderation";
+ const isPending = request.status === "На модерации";
const getAccessToken = () => {
if (typeof window === "undefined") return null;
@@ -43,171 +43,170 @@ const ModeratorRequestModal = ({ request, onClose, onModerated }) => {
const deadlineTime = request.deadlineTime || request.time || "";
const handleApprove = async () => {
- if (!API_BASE || submitting) return;
- const token = getAccessToken();
- if (!token) {
- setError("Вы не авторизованы");
- return;
- }
+ if (!API_BASE || submitting) return;
+ const token = getAccessToken();
+ if (!token) {
+ setError("Вы не авторизованы");
+ return;
+ }
- console.log("[MODERATION] APPROVE start", {
- requestId: request.id,
- statusBefore: request.status,
- });
+ console.log("[MODERATION] APPROVE start", {
+ requestId: request.id,
+ statusBefore: request.status,
+ });
- try {
- setSubmitting(true);
- setError("");
+ try {
+ setSubmitting(true);
+ setError("");
- const res = await fetch(
- `${API_BASE}/moderation/requests/${request.id}/approve`,
- {
- method: "POST",
- headers: {
- Accept: "application/json",
- Authorization: `Bearer ${token}`,
- "Content-Type": "application/json",
- },
- body: JSON.stringify({ comment: null }),
- }
- );
+ const res = await fetch(
+ `${API_BASE}/moderation/requests/${request.id}/approve`,
+ {
+ method: "POST",
+ headers: {
+ Accept: "application/json",
+ Authorization: `Bearer ${token}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ comment: null }),
+ }
+ );
- console.log("[MODERATION] APPROVE response status", res.status);
+ console.log("[MODERATION] APPROVE response status", res.status);
- const text = await res.text();
- let data = null;
- if (text) {
- try {
- data = JSON.parse(text);
- } catch {
- data = null;
- }
- }
+ const text = await res.text();
+ let data = null;
+ if (text) {
+ try {
+ data = JSON.parse(text);
+ } catch {
+ data = null;
+ }
+ }
- console.log("[MODERATION] APPROVE response body", data || text);
+ console.log("[MODERATION] APPROVE response body", data || text);
- if (!res.ok) {
- let msg = "Не удалось одобрить заявку";
- if (data && typeof data === "object" && data.error) {
- msg = data.error;
- } else if (text) {
- msg = text;
- }
- console.log("[MODERATION] APPROVE error", msg);
- setError(msg);
- setSubmitting(false);
- return;
- }
+ if (!res.ok) {
+ let msg = "Не удалось одобрить заявку";
+ if (data && typeof data === "object" && data.error) {
+ msg = data.error;
+ } else if (text) {
+ msg = text;
+ }
+ console.log("[MODERATION] APPROVE error", msg);
+ setError(msg);
+ setSubmitting(false);
+ return;
+ }
- onModerated?.({
- ...request,
- status: "approved",
- moderationResult: data,
- });
+ onModerated?.({
+ ...request,
+ status: "approved",
+ moderationResult: data,
+ });
- console.log("[MODERATION] APPROVE success", {
- requestId: request.id,
- newStatus: "approved",
- });
+ console.log("[MODERATION] APPROVE success", {
+ requestId: request.id,
+ newStatus: "approved",
+ });
- setSubmitting(false);
- onClose();
- } catch (e) {
- console.log("[MODERATION] APPROVE exception", e);
- setError(e.message || "Ошибка сети");
- setSubmitting(false);
- }
-};
+ setSubmitting(false);
+ onClose();
+ } catch (e) {
+ console.log("[MODERATION] APPROVE exception", e);
+ setError(e.message || "Ошибка сети");
+ setSubmitting(false);
+ }
+ };
-const handleRejectConfirm = async () => {
- if (!API_BASE || submitting) return;
- const token = getAccessToken();
- if (!token) {
- setError("Вы не авторизованы");
- return;
- }
- if (!rejectReason.trim()) {
- setError("Укажите причину отклонения");
- return;
- }
+ const handleRejectConfirm = async () => {
+ if (!API_BASE || submitting) return;
+ const token = getAccessToken();
+ if (!token) {
+ setError("Вы не авторизованы");
+ return;
+ }
+ if (!rejectReason.trim()) {
+ setError("Укажите причину отклонения");
+ return;
+ }
- console.log("[MODERATION] REJECT start", {
- requestId: request.id,
- statusBefore: request.status,
- reason: rejectReason,
- });
+ console.log("[MODERATION] REJECT start", {
+ requestId: request.id,
+ statusBefore: request.status,
+ reason: rejectReason,
+ });
- try {
- setSubmitting(true);
- setError("");
+ try {
+ setSubmitting(true);
+ setError("");
- const res = await fetch(
- `${API_BASE}/moderation/requests/${request.id}/reject`,
- {
- method: "POST",
- headers: {
- Accept: "application/json",
- Authorization: `Bearer ${token}`,
- "Content-Type": "application/json",
- },
- body: JSON.stringify({ comment: rejectReason }),
- }
- );
+ const res = await fetch(
+ `${API_BASE}/moderation/requests/${request.id}/reject`,
+ {
+ method: "POST",
+ headers: {
+ Accept: "application/json",
+ Authorization: `Bearer ${token}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ comment: rejectReason }),
+ }
+ );
- console.log("[MODERATION] REJECT response status", res.status);
+ console.log("[MODERATION] REJECT response status", res.status);
- const text = await res.text();
- let data = null;
- if (text) {
- try {
- data = JSON.parse(text);
- } catch {
- data = null;
- }
- }
+ const text = await res.text();
+ let data = null;
+ if (text) {
+ try {
+ data = JSON.parse(text);
+ } catch {
+ data = null;
+ }
+ }
- console.log("[MODERATION] REJECT response body", data || text);
+ console.log("[MODERATION] REJECT response body", data || text);
- if (!res.ok) {
- let msg = "Не удалось отклонить заявку";
- if (data && typeof data === "object" && data.error) {
- msg = data.error;
- } else if (text) {
- msg = text;
- }
- console.log("[MODERATION] REJECT error", msg);
- setError(msg);
- setSubmitting(false);
- return;
- }
+ if (!res.ok) {
+ let msg = "Не удалось отклонить заявку";
+ if (data && typeof data === "object" && data.error) {
+ msg = data.error;
+ } else if (text) {
+ msg = text;
+ }
+ console.log("[MODERATION] REJECT error", msg);
+ setError(msg);
+ setSubmitting(false);
+ return;
+ }
- onModerated?.({
- ...request,
- status: "rejected",
- rejectReason,
- moderationResult: data,
- });
+ onModerated?.({
+ ...request,
+ status: "rejected",
+ rejectReason: (data && data.moderation_comment) || rejectReason,
+ moderationResult: data,
+ });
- console.log("[MODERATION] REJECT success", {
- requestId: request.id,
- newStatus: "rejected",
- });
-
- setShowRejectPopup(false);
- setSubmitting(false);
- onClose();
- } catch (e) {
- console.log("[MODERATION] REJECT exception", e);
- setError(e.message || "Ошибка сети");
- setSubmitting(false);
- }
-};
+ console.log("[MODERATION] REJECT success", {
+ requestId: request.id,
+ newStatus: "rejected",
+ });
+ setShowRejectPopup(false);
+ setSubmitting(false);
+ onClose();
+ } catch (e) {
+ console.log("[MODERATION] REJECT exception", e);
+ setError(e.message || "Ошибка сети");
+ setSubmitting(false);
+ }
+ };
return (
<>
-
+
-
+ {/* Кнопки показываем только для pending_moderation */}
+ {isPending && (
-
+ )}
{showRejectPopup && (
@@ -368,7 +368,7 @@ const handleRejectConfirm = async () => {
disabled={submitting}
className="w-full h-10 bg-[#E06767] rounded-[10px] flex items-center justify-center disabled:opacity-60"
>
-
+
{submitting ? "Сохранение..." : "Подтвердить"}
diff --git a/app/components/RequestDetailsModal.jsx b/app/components/RequestDetailsModal.jsx
index b8070c4..25c252e 100644
--- a/app/components/RequestDetailsModal.jsx
+++ b/app/components/RequestDetailsModal.jsx
@@ -6,7 +6,7 @@ import { FaStar } from "react-icons/fa";
const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL;
const RequestDetailsModal = ({ request, onClose }) => {
- const [details, setDetails] = useState(null); // полная заявка из API
+ const [details, setDetails] = useState(null); // RequestDetail
const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState("");
@@ -16,8 +16,14 @@ const RequestDetailsModal = ({ request, onClose }) => {
const [rating, setRating] = useState(0);
const [review, setReview] = useState("");
const [rejectFeedback, setRejectFeedback] = useState("");
+
- // подгружаем детальную заявку /requests/{id}[file:519]
+ // для приёма отклика
+ const [acceptLoading, setAcceptLoading] = useState(false);
+ const [acceptError, setAcceptError] = useState("");
+ const [acceptSuccess, setAcceptSuccess] = useState("");
+
+ // подгружаем детальную заявку /requests/{id}
useEffect(() => {
const fetchDetails = async () => {
if (!API_BASE) {
@@ -61,7 +67,7 @@ const RequestDetailsModal = ({ request, onClose }) => {
return;
}
- const data = await res.json(); // RequestDetail[file:519]
+ const data = await res.json(); // RequestDetail
setDetails(data);
setLoading(false);
} catch (e) {
@@ -88,7 +94,64 @@ const RequestDetailsModal = ({ request, onClose }) => {
onClose();
};
- // подготовка текстов из details (без изменения верстки)
+ // приём отклика волонтёра: POST /requests/{id}/responses/{response_id}/accept
+ const handleAcceptResponse = async (responseId) => {
+ if (!API_BASE || !request.id || !responseId) {
+ setAcceptError("Некорректные данные для приёма отклика");
+ return;
+ }
+
+ const saved =
+ typeof window !== "undefined"
+ ? localStorage.getItem("authUser")
+ : null;
+ const authUser = saved ? JSON.parse(saved) : null;
+ const accessToken = authUser?.accessToken;
+
+ if (!accessToken) {
+ setAcceptError("Вы не авторизованы");
+ return;
+ }
+
+ try {
+ setAcceptLoading(true);
+ setAcceptError("");
+ setAcceptSuccess("");
+
+ const res = await fetch(
+ `${API_BASE}/requests/${request.id}/responses/${responseId}/accept`,
+ {
+ method: "POST",
+ headers: {
+ Accept: "application/json",
+ Authorization: `Bearer ${accessToken}`,
+ },
+ }
+ );
+
+ if (!res.ok) {
+ let msg = "Не удалось принять отклик";
+ try {
+ const data = await res.json();
+ if (data.error) msg = data.error;
+ } catch {
+ const text = await res.text();
+ if (text) msg = text;
+ }
+ setAcceptError(msg);
+ setAcceptLoading(false);
+ return;
+ }
+
+ await res.json(); // { success, message, ... }
+ setAcceptSuccess("Волонтёр принят на заявку");
+ setAcceptLoading(false);
+ } catch (e) {
+ setAcceptError(e.message || "Ошибка сети");
+ setAcceptLoading(false);
+ }
+ };
+
const fullDescription =
details?.description || request.description || "Описание отсутствует";
@@ -102,10 +165,14 @@ const RequestDetailsModal = ({ request, onClose }) => {
const requestTypeName = details?.request_type?.name;
+ // здесь предполагаем, что детали заявки содержат массив responses,
+ // либо ты добавишь его на бэке к RequestDetail
+ const responses = details?.responses || [];
+
return (
{/* Заголовок */}
-
+
+ {/* Блок откликов волонтёров: принять/отклонить */}
+ {!loading && !loadError && responses.length > 0 && (
+
+
+ Отклики волонтёров
+
+ {responses.map((resp) => (
+
+
+ Волонтёр:{" "}
+ {resp.volunteer_name || resp.volunteername || resp.volunteer?.name}
+
+ {resp.message && (
+
+ Сообщение:{" "}
+ {resp.message}
+
+ )}
+
+
+ {/* Если позже добавишь отклонение отклика — вторая кнопка здесь */}
+ {/* */}
+
+
+ ))}
+
+ {acceptError && (
+
+ {acceptError}
+
+ )}
+ {acceptSuccess && (
+
+ {acceptSuccess}
+
+ )}
+
+ )}
+
{/* Выполнена: блок отзыва */}
{isDone && (
@@ -218,7 +335,7 @@ const RequestDetailsModal = ({ request, onClose }) => {
value={review}
onChange={(e) => setReview(e.target.value)}
rows={4}
- className="w-full bg-[#72B8E2] rounded-2xl px-3 py-2 text-sm font-montserrat text-white placeholder:text-white/70 outline-none resize-none border border-white/20"
+ className="w-full.bg-[#72B8E2] rounded-2xl px-3 py-2 text-sm font-montserrat text-white placeholder:text-white/70 outline-none resize-none border border.white/20"
placeholder="Напишите, как прошла помощь"
/>
@@ -246,7 +363,7 @@ const RequestDetailsModal = ({ request, onClose }) => {
value={rejectFeedback}
onChange={(e) => setRejectFeedback(e.target.value)}
rows={4}
- className="w-full rounded-2xl px-3 py-2 text-sm font-montserrat text-black.placeholder:text-black/40 outline-none resize-none border border-[#FF8282]"
+ className="w-full rounded-2xl px-3 py-2 text-sm font-montserrat text-black placeholder:text-black/40 outline-none resize-none border border-[#FF8282]"
placeholder="Расскажите, что можно улучшить"
/>
@@ -286,9 +403,9 @@ const RequestDetailsModal = ({ request, onClose }) => {
diff --git a/app/components/ValounterRequestDetailsModal.jsx b/app/components/ValounterRequestDetailsModal.jsx
index cb3d55c..df3af73 100644
--- a/app/components/ValounterRequestDetailsModal.jsx
+++ b/app/components/ValounterRequestDetailsModal.jsx
@@ -4,7 +4,8 @@ import React, { useState } from "react";
import { FaStar } from "react-icons/fa";
const RequestDetailsModal = ({ request, onClose }) => {
- const isDone = request.rawStatus === "completed" || request.status === "Выполнена";
+ const isDone =
+ request.rawStatus === "completed" || request.status === "Выполнена";
const isInProgress =
request.rawStatus === "in_progress" || request.status === "В процессе";
@@ -42,6 +43,15 @@ const RequestDetailsModal = ({ request, onClose }) => {
const place = [request.address, request.city].filter(Boolean).join(", ");
const requesterName = request.requesterName || "Заявитель";
+ const createdDate = request.date || "";
+ const createdTime = request.time || "";
+
+ // ВЫПОЛНИТЬ ДО: берём дату из заявки
+ let deadlineText = "—";
+ if (request.desiredCompletionDate) {
+ const d = new Date(request.desiredCompletionDate);
+ deadlineText = d.toLocaleDateString("ru-RU");
+ }
return (
@@ -73,10 +83,10 @@ const RequestDetailsModal = ({ request, onClose }) => {
- {request.date}
+ {createdDate}
- {request.time}
+ {createdTime}
@@ -92,9 +102,11 @@ const RequestDetailsModal = ({ request, onClose }) => {
Заявитель: {request.requesterName || requesterName}
Адрес: {place || "Не указан"}
{urgencyText &&
Срочность: {urgencyText}
}
+ {/* НОВОЕ: строка "Выполнить до" */}
+
Выполнить до: {deadlineText}
- {/* Описание / список покупок */}
+ {/* Описание */}
{request.description && (
@@ -103,7 +115,7 @@ const RequestDetailsModal = ({ request, onClose }) => {
)}
- {/* Блок отзыва + рейтинг — и для Выполнена, и для В процессе */}
+ {/* Отзыв и рейтинг */}
{(isDone || isInProgress) && (
<>
@@ -114,7 +126,7 @@ const RequestDetailsModal = ({ request, onClose }) => {
value={review}
onChange={(e) => setReview(e.target.value)}
rows={4}
- className="w-full bg-[#72B8E2] rounded-2xl px-3 py-2 text-sm font-montserrat text-white placeholder:text.white/70 outline-none resize-none border border-white/20"
+ className="w-full bg-[#72B8E2] rounded-2xl px-3 py-2 text-sm font-montserrat text-white placeholder:text-white/70 outline-none resize-none border border-white/20"
placeholder={
isDone
? "Напишите, как прошла помощь"
@@ -123,7 +135,7 @@ const RequestDetailsModal = ({ request, onClose }) => {
/>
-
+
Оценить заявителя
@@ -150,12 +162,11 @@ const RequestDetailsModal = ({ request, onClose }) => {
- {/* Кнопка внизу */}
{(isDone || isInProgress) && (