WIPVOLONT

This commit is contained in:
fullofempt
2025-12-14 21:14:55 +05:00
parent 433b9e896c
commit 0df52352a8
12 changed files with 893 additions and 440 deletions

View File

@@ -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>