WIPVOLONT
This commit is contained in:
@@ -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 (
|
||||
<>
|
||||
<div className="fixed inset-0 z-40 flex flex-col bg-[#90D2F9] px-4 pt-4 pb-20">
|
||||
<div className="flex.items-center gap-2 mb-3">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
@@ -272,7 +271,7 @@ const handleRejectConfirm = async () => {
|
||||
: "bg-[#E9D171]"
|
||||
}`}
|
||||
>
|
||||
<span className="text-[12px] font-montserrat font-semibold text-black">
|
||||
<span className="text-[12px] font-montserrat font-semibold text-white">
|
||||
{isApproved
|
||||
? "Принята"
|
||||
: isRejected
|
||||
@@ -299,14 +298,14 @@ const handleRejectConfirm = async () => {
|
||||
)}
|
||||
|
||||
{isRejected && (
|
||||
<div className="w-full.bg-[#FFE2E2] rounded-[10px] px-3 py-2">
|
||||
<div className="w-full bg-[#FFE2E2] rounded-[10px] px-3 py-2">
|
||||
<p className="text-[14px] font-montserrat font-bold text-[#E06767] mb-1">
|
||||
Причина отклонения
|
||||
</p>
|
||||
<p className="text-[12px] font-montserrat text-black whitespace-pre-line">
|
||||
{request.rejectReason && request.rejectReason.trim().length > 0
|
||||
{(request.rejectReason && request.rejectReason.trim().length > 0
|
||||
? request.rejectReason
|
||||
: "Причина не указана"}
|
||||
: request.moderation_comment) || "Причина не указана"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -319,7 +318,8 @@ const handleRejectConfirm = async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Кнопки показываем только для pending_moderation */}
|
||||
{isPending && (
|
||||
<div className="mt-4 w-full max-w-[400px] mx-auto flex items-center justify-between gap-3">
|
||||
<button
|
||||
type="button"
|
||||
@@ -342,7 +342,7 @@ const handleRejectConfirm = async () => {
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
)}
|
||||
</div>
|
||||
|
||||
{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"
|
||||
>
|
||||
<span className="text-[16px] font-montserrat font-bold text-white">
|
||||
<span className="text-[16px] font-montserrat font-bold text.white">
|
||||
{submitting ? "Сохранение..." : "Подтвердить"}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -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 (
|
||||
<div className="fixed inset-0 z-40 flex flex-col bg-[#90D2F9] px-4 pt-4 pb-20">
|
||||
{/* Заголовок */}
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="flex.items-center gap-2 mb-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
@@ -182,12 +249,13 @@ const RequestDetailsModal = ({ request, onClose }) => {
|
||||
|
||||
{requesterName && (
|
||||
<p className="font-montserrat text-[12px] text-black">
|
||||
<span className="font-semibold">Заявитель:</span> {requesterName}
|
||||
<span className="font-semibold">Заявитель:</span>{" "}
|
||||
{requesterName}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{details?.contact_phone && (
|
||||
<p className="font-montserrat text-[12px] text.black">
|
||||
<p className="font-montserrat text-[12px] text-black">
|
||||
<span className="font-semibold">Телефон:</span>{" "}
|
||||
{details.contact_phone}
|
||||
</p>
|
||||
@@ -200,7 +268,7 @@ const RequestDetailsModal = ({ request, onClose }) => {
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p className="font-montserrat text-[12px] text.black mt-1 whitespace-pre-line">
|
||||
<p className="font-montserrat text-[12px] text-black mt-1 whitespace-pre-line">
|
||||
<span className="font-semibold">Описание:</span>{" "}
|
||||
{fullDescription}
|
||||
</p>
|
||||
@@ -208,6 +276,55 @@ const RequestDetailsModal = ({ request, onClose }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Блок откликов волонтёров: принять/отклонить */}
|
||||
{!loading && !loadError && responses.length > 0 && (
|
||||
<div className="mt-2 flex flex-col gap-2">
|
||||
<p className="font-montserrat font-semibold text-[13px] text-black">
|
||||
Отклики волонтёров
|
||||
</p>
|
||||
{responses.map((resp) => (
|
||||
<div
|
||||
key={resp.id}
|
||||
className="w-full rounded-xl bg-[#E4E4E4] px-3 py-2 flex flex-col gap-1"
|
||||
>
|
||||
<p className="font-montserrat text-[12px] text-black">
|
||||
<span className="font-semibold">Волонтёр:</span>{" "}
|
||||
{resp.volunteer_name || resp.volunteername || resp.volunteer?.name}
|
||||
</p>
|
||||
{resp.message && (
|
||||
<p className="font-montserrat text-[12px] text-black">
|
||||
<span className="font-semibold">Сообщение:</span>{" "}
|
||||
{resp.message}
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-1 flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
disabled={acceptLoading}
|
||||
onClick={() => handleAcceptResponse(resp.id)}
|
||||
className="flex-1 h-[32px] bg-[#94E067] rounded-full flex items-center justify-center text-white text-[12px] font-montserrat disabled:opacity-60"
|
||||
>
|
||||
Принять
|
||||
</button>
|
||||
{/* Если позже добавишь отклонение отклика — вторая кнопка здесь */}
|
||||
{/* <button ...>Отклонить</button> */}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{acceptError && (
|
||||
<p className="text-[11px] font-montserrat text-red-500">
|
||||
{acceptError}
|
||||
</p>
|
||||
)}
|
||||
{acceptSuccess && (
|
||||
<p className="text-[11px] font-montserrat text-green-600">
|
||||
{acceptSuccess}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Выполнена: блок отзыва */}
|
||||
{isDone && (
|
||||
<div className="bg-[#72B8E2] rounded-3xl p-3 flex flex-col gap-2">
|
||||
@@ -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="Напишите, как прошла помощь"
|
||||
/>
|
||||
</div>
|
||||
@@ -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="Расскажите, что можно улучшить"
|
||||
/>
|
||||
</div>
|
||||
@@ -286,9 +403,9 @@ const RequestDetailsModal = ({ request, onClose }) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
className="mt-4 w-full max-w-[360px] mx-auto bg-[#94E067] rounded-2xl py-3 flex items-center.justify-center"
|
||||
className="mt-4 w-full max-w-[360px] mx-auto bg-[#94E067] rounded-2xl py-3 flex items-center justify-center"
|
||||
>
|
||||
<span className="font-montserrat font-extrabold text-[16px] text-white">
|
||||
<span className="font-mонтserrat font-extrabold text-[16px] text-white">
|
||||
{isRejected ? "Отправить комментарий" : "Оставить отзыв"}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -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 (
|
||||
<div className="fixed inset-0 z-40 flex flex-col bg-[#90D2F9] px-4 pt-4 pb-20">
|
||||
@@ -73,10 +83,10 @@ const RequestDetailsModal = ({ request, onClose }) => {
|
||||
</span>
|
||||
<div className="text-right leading-tight">
|
||||
<p className="font-montserrat text-[10px] text-black">
|
||||
{request.date}
|
||||
{createdDate}
|
||||
</p>
|
||||
<p className="font-montserrat text-[10px] text-black">
|
||||
{request.time}
|
||||
{createdTime}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -92,9 +102,11 @@ const RequestDetailsModal = ({ request, onClose }) => {
|
||||
<p>Заявитель: {request.requesterName || requesterName}</p>
|
||||
<p>Адрес: {place || "Не указан"}</p>
|
||||
{urgencyText && <p>Срочность: {urgencyText}</p>}
|
||||
{/* НОВОЕ: строка "Выполнить до" */}
|
||||
<p>Выполнить до: {deadlineText}</p>
|
||||
</div>
|
||||
|
||||
{/* Описание / список покупок */}
|
||||
{/* Описание */}
|
||||
{request.description && (
|
||||
<div className="bg-[#E4E4E4] rounded-2xl px-3 py-2 max-h-[140px] overflow-y-auto">
|
||||
<p className="text-[11px] leading-[13px] font-montserrat whitespace-pre-line">
|
||||
@@ -103,7 +115,7 @@ const RequestDetailsModal = ({ request, onClose }) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Блок отзыва + рейтинг — и для Выполнена, и для В процессе */}
|
||||
{/* Отзыв и рейтинг */}
|
||||
{(isDone || isInProgress) && (
|
||||
<>
|
||||
<div className="bg-[#72B8E2] rounded-3xl p-3 flex flex-col gap-2">
|
||||
@@ -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 }) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-1 flex flex-col items-center gap-2">
|
||||
<div className="mt-1 flex flex-col.items-center gap-2">
|
||||
<p className="font-montserrat font-semibold text-[14px] text-black">
|
||||
Оценить заявителя
|
||||
</p>
|
||||
@@ -150,12 +162,11 @@ const RequestDetailsModal = ({ request, onClose }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Кнопка внизу */}
|
||||
{(isDone || isInProgress) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
className="mt-4 w-full max-w-[360px] mx-auto bg-[#94E067] rounded-2xl py-3 flex.items-center justify-center"
|
||||
className="mt-4 w-full max-w-[360px] mx-auto bg-[#94E067] rounded-2xl py-3 flex items-center justify-center"
|
||||
>
|
||||
<span className="font-montserrat font-extrabold text-[16px] text-white">
|
||||
{isDone ? "Оставить отзыв" : "Сохранить прогресс"}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { FaTimesCircle } from "react-icons/fa";
|
||||
|
||||
const AcceptPopup = ({ request, isOpen, onClose, onAccept, loading, error }) => {
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
const AcceptPopup = ({ request, isOpen, onClose }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
if (!isOpen || !request) return null;
|
||||
|
||||
const title = request.title;
|
||||
@@ -15,14 +20,21 @@ const AcceptPopup = ({ request, isOpen, onClose, onAccept, loading, error }) =>
|
||||
const city = request.city ? `, ${request.city}` : "";
|
||||
const place = `${baseAddress}${city}`;
|
||||
|
||||
const deadline = request.desired_completion_date
|
||||
? new Date(request.desired_completion_date).toLocaleString("ru-RU", {
|
||||
let deadline = "Не указано";
|
||||
if (request.desired_completion_date) {
|
||||
const d = new Date(request.desired_completion_date);
|
||||
if (!Number.isNaN(d.getTime())) {
|
||||
const datePart = d.toLocaleDateString("ru-RU", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
});
|
||||
const timePart = d.toLocaleTimeString("ru-RU", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
: "Не указано";
|
||||
});
|
||||
deadline = `${datePart}, ${timePart}`;
|
||||
}
|
||||
}
|
||||
|
||||
const phone = request.contact_phone || request.phone;
|
||||
const contactNotes = request.contact_notes || request.contactNotes;
|
||||
@@ -42,26 +54,79 @@ const AcceptPopup = ({ request, isOpen, onClose, onAccept, loading, error }) =>
|
||||
}
|
||||
})();
|
||||
|
||||
const handleClick = () => {
|
||||
// здесь видно, с каким id ты стучишься в /requests/{id}/responses
|
||||
console.log("Отклик на заявку из попапа:", {
|
||||
id: request.id,
|
||||
title: request.title,
|
||||
raw: request,
|
||||
});
|
||||
onAccept(request);
|
||||
const getAccessToken = () => {
|
||||
if (typeof window === "undefined") return null;
|
||||
const saved = localStorage.getItem("authUser");
|
||||
const authUser = saved ? JSON.parse(saved) : null;
|
||||
return authUser?.accessToken || null;
|
||||
};
|
||||
|
||||
// тут полностью логика отклика
|
||||
const handleClick = async () => {
|
||||
if (!API_BASE || !request.id) {
|
||||
setError("Некорректная заявка (нет id)");
|
||||
return;
|
||||
}
|
||||
const accessToken = getAccessToken();
|
||||
if (!accessToken) {
|
||||
setError("Вы не авторизованы");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
|
||||
console.log("POST отклик", {
|
||||
url: `${API_BASE}/requests/${request.id}/responses`,
|
||||
requestId: request.id,
|
||||
});
|
||||
|
||||
const res = await fetch(`${API_BASE}/requests/${request.id}/responses`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
// по схеме тело обязательно, просто пустой объект допустим [file:598]
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
console.error("Ответ API /responses:", msg);
|
||||
setError(msg);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await res.json(); // VolunteerResponse [file:598]
|
||||
setLoading(false);
|
||||
onClose();
|
||||
} catch (e) {
|
||||
setError(e.message || "Ошибка сети");
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* затемнение */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/40"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* карточка на всю страницу */}
|
||||
<div className="relative z-10 w-full h-250px bg-white rounded-2xl px-4 pt-4 pb-6 flex flex-col">
|
||||
{/* карточка поверх всего */}
|
||||
<div className="relative z-50 w-full max-w-[400px] bg-white rounded-2xl px-4 pt-4 pb-6 flex flex-col mt-50">
|
||||
{/* крестик */}
|
||||
<button
|
||||
type="button"
|
||||
@@ -80,9 +145,9 @@ const AcceptPopup = ({ request, isOpen, onClose, onAccept, loading, error }) =>
|
||||
</p>
|
||||
|
||||
{/* Только время выполнить до */}
|
||||
<div className="flex.items-center gap-3 mb-3">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-full h-[40px] bg-[#90D2F9] rounded-full flex flex-col items-center justify-center">
|
||||
<span className="text-[12px] leading-[11px] text-white font-semibold.mb-2">
|
||||
<span className="text-[12px] leading-[11px] text-white font-semibold mb-1">
|
||||
Выполнить до
|
||||
</span>
|
||||
<span className="text-[15px] leading-[13px] text-white font-semibold">
|
||||
|
||||
Reference in New Issue
Block a user