Files
frontend/app/components/RequestDetailsModal.jsx
fullofempt e4bfbd30cc end
2025-12-15 13:18:18 +05:00

560 lines
18 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, { useState, useEffect } from "react";
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);
const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState("");
const [locallyCompleted, setLocallyCompleted] = useState(
request.status === "Выполнена"
);
const isRejected = request.status === "Отклонена";
const isDone = request.status === "Выполнена" || locallyCompleted;
const canComplete = !isRejected && !isDone;
const [rating, setRating] = useState(0);
const [review, setReview] = useState("");
const [rejectFeedback, setRejectFeedback] = useState("");
const [responses, setResponses] = useState([]);
const [responsesLoading, setResponsesLoading] = useState(true);
const [responsesError, setResponsesError] = useState("");
const [acceptLoading, setAcceptLoading] = useState(false);
const [acceptError, setAcceptError] = useState("");
const [acceptSuccess, setAcceptSuccess] = useState("");
const [completeLoading, setCompleteLoading] = useState(false);
const [completeError, setCompleteError] = useState("");
const [completeSuccess, setCompleteSuccess] = useState("");
const getAccessToken = () => {
if (typeof window === "undefined") return null;
const saved = localStorage.getItem("authUser");
const authUser = saved ? JSON.parse(saved) : null;
return authUser?.accessToken || null;
};
useEffect(() => {
const fetchDetails = async () => {
if (!API_BASE) {
setLoadError("API_BASE_URL не задан");
setLoading(false);
return;
}
const accessToken = getAccessToken();
if (!accessToken) {
setLoadError("Вы не авторизованы");
setLoading(false);
return;
}
try {
const res = await fetch(`${API_BASE}/requests/${request.id}`, {
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;
}
setLoadError(msg);
setLoading(false);
return;
}
const data = await res.json();
setDetails(data);
setLoading(false);
} catch (e) {
setLoadError(e.message || "Ошибка сети");
setLoading(false);
}
};
fetchDetails();
}, [request.id]);
useEffect(() => {
const fetchResponses = async () => {
if (!API_BASE) {
setResponsesError("API_BASE_URL не задан");
setResponsesLoading(false);
return;
}
const accessToken = getAccessToken();
if (!accessToken) {
setResponsesError("Вы не авторизованы");
setResponsesLoading(false);
return;
}
try {
const res = await fetch(
`${API_BASE}/requests/${request.id}/responses`,
{
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;
}
setResponsesError(msg);
setResponsesLoading(false);
return;
}
const data = await res.json();
setResponses(data);
setResponsesLoading(false);
} catch (e) {
setResponsesError(e.message || "Ошибка сети");
setResponsesLoading(false);
}
};
fetchResponses();
}, [request.id]);
const handleStarClick = (value) => {
setRating(value);
};
// ЕДИНСТВЕННЫЙ вызов /complete: завершение + отзыв
const handleCompleteRequest = async () => {
if (!API_BASE) {
setCompleteError("API_BASE_URL не задан");
return;
}
const accessToken = getAccessToken();
if (!accessToken) {
setCompleteError("Вы не авторизованы");
return;
}
if (!canComplete) return;
if (!rating) {
setCompleteError("Поставьте оценку от 1 до 5");
return;
}
try {
setCompleteLoading(true);
setCompleteError("");
setCompleteSuccess("");
const res = await fetch(`${API_BASE}/requests/${request.id}/complete`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
rating,
comment: review || null,
}),
});
let data = null;
const text = await res.text();
if (text) {
try {
data = JSON.parse(text);
} catch {
data = null;
}
}
if (!res.ok) {
let msg = "Не удалось завершить заявку";
if (data && typeof data === "object" && data.error) {
msg = data.error;
} else if (text) {
msg = text;
}
setCompleteError(msg);
setCompleteLoading(false);
return;
}
setLocallyCompleted(true);
setCompleteSuccess("Заявка завершена и отзыв отправлен");
setCompleteLoading(false);
} catch (e) {
setCompleteError(e.message || "Ошибка сети");
setCompleteLoading(false);
}
};
const handleAcceptResponse = async (responseId) => {
if (!API_BASE || !request.id || !responseId) {
setAcceptError("Некорректные данные для приёма отклика");
return;
}
const accessToken = getAccessToken();
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();
setAcceptSuccess("Волонтёр принят на заявку");
setAcceptLoading(false);
} catch (e) {
setAcceptError(e.message || "Ошибка сети");
setAcceptLoading(false);
}
};
const fullDescription =
details?.description || request.description || "Описание отсутствует";
const addressLine = details
? [details.address, details.city].filter(Boolean).join(", ")
: null;
const requesterName = details?.requester?.first_name
? `${details.requester.first_name} ${details.requester.last_name || ""}`.trim()
: details?.requester?.email;
const requestTypeName = details?.request_type?.name;
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">
<button
type="button"
onClick={onClose}
className="text-white w-7 h-7 rounded-full flex items-center justify-center text-lg"
>
</button>
<p className="flex-1 text-center font-montserrat font-extrabold text-[20px] leading-[24px] text-white">
Заявка от {request.createdAt}
</p>
<span className="w-7" />
</div>
{/* Карточка */}
<div className="flex-1 flex items-start justify-center">
<div className="w-full max-w-[360px] bg-white rounded-2xl p-4 flex flex-col gap-4 shadow-lg">
{/* Статус + дата/время */}
<div className="flex items-start justify-between">
<span
className="inline-flex items-center justify-center px-3 py-1 rounded-full font-montserrat text-[10px] font-semibold text-white"
style={{ backgroundColor: request.statusColor }}
>
{isDone ? "Выполнена" : request.status}
</span>
<div className="text-right leading-tight">
<p className="font-montserrat text-[10px] text-black">
{request.date}
</p>
<p className="font-montserrat text-[10px] text-black">
{request.time}
</p>
</div>
</div>
{/* Название */}
<p className="font-montserrat font-semibold text-[16px] leading-[20px] text-black">
{request.title}
</p>
{/* Инфо по заявке */}
<div className="bg-[#F2F2F2] rounded-2xl px-3 py-2 flex flex-col gap-1 max-h-[40vh] overflow-y-auto">
{loading && (
<p className="font-montserrat text-[12px] text.black">
Загрузка информации о заявке...
</p>
)}
{loadError && !loading && (
<p className="font-montserrat text-[12px] text-red-600">
{loadError}
</p>
)}
{!loading && !loadError && (
<>
{requestTypeName && (
<p className="font-montserrat text-[12px] text-black">
<span className="font-semibold">Тип:</span> {requestTypeName}
</p>
)}
{addressLine && (
<p className="font-montserrat text-[12px] text-black">
<span className="font-semibold">Адрес:</span> {addressLine}
</p>
)}
{details?.urgency && (
<p className="font-montserrat text-[12px] text-black">
<span className="font-semibold">Срочность:</span>{" "}
{details.urgency}
</p>
)}
{requesterName && (
<p className="font-montserrat text-[12px] text-black">
<span className="font-semibold">Заявитель:</span>{" "}
{requesterName}
</p>
)}
{details?.contact_phone && (
<p className="font-montserrat text-[12px] text-black">
<span className="font-semibold">Телефон:</span>{" "}
{details.contact_phone}
</p>
)}
{details?.contact_notes && (
<p className="font-montserrat text-[12px] text-black">
<span className="font-semibold">Комментарий к контакту:</span>{" "}
{details.contact_notes}
</p>
)}
<p className="font-montserrat text-[12px] text-black mt-1 whitespace-pre-line">
<span className="font-semibold">Описание:</span>{" "}
{fullDescription}
</p>
</>
)}
</div>
{/* Отклики волонтёров (как было) */}
{!responsesLoading && !responsesError && 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>
</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>
)}
{responsesLoading && !responsesError && (
<p className="mt-2 text-[11px] font-montserrat text-black">
Загрузка откликов волонтёров...
</p>
)}
{responsesError && (
<p className="mt-2 text-[11px] font-montserrat text-red-500">
{responsesError}
</p>
)}
{/* Отклонена */}
{isRejected && (
<>
{request.rejectReason && (
<div className="bg-[#FF8282] rounded-2xl p-3 mt-2">
<p className="font-montserrat font-bold text-[12px] text-white mb-1">
Причина отказа
</p>
<p className="font-montserrat text-[12px] text-white">
{request.rejectReason}
</p>
</div>
)}
<div className="flex flex-col gap-1 mt-2">
<p className="font-montserrat font-bold text-[12px] text-black">
Ваш комментарий
</p>
<textarea
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]"
placeholder="Расскажите, что можно улучшить"
/>
</div>
</>
)}
{/* Отзыв и рейтинг — до завершения */}
{!isRejected && !isDone && (
<>
<div className="bg-[#72B8E2] rounded-3xl p-3 flex flex-col gap-2 mt-2">
<p className="font-montserrat font-bold text-[12px] text.white">
Отзыв о волонтёре
</p>
<textarea
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"
placeholder="Напишите, как прошла помощь"
/>
</div>
<div className="mt-2 flex flex-col items-center gap-2">
<p className="font-montserrat font-semibold text-[14px] text-black">
Оценить волонтера
</p>
<div className="flex gap-2">
{[1, 2, 3, 4, 5].map((star) => (
<button
key={star}
type="button"
onClick={() => handleStarClick(star)}
className="text-[#F6E168]"
>
<FaStar
size={26}
className={
star <= rating ? "fill-[#F6E168]" : "fill-[#F6E168]/40"
}
/>
</button>
))}
</div>
</div>
</>
)}
{completeError && (
<p className="mt-1 text-[11px] font-montserrat text-red-500">
{completeError}
</p>
)}
{completeSuccess && (
<p className="mt-1 text-[11px] font-montserrat text-green-600">
{completeSuccess}
</p>
)}
</div>
</div>
{/* Низ */}
{!isRejected && !isDone && (
<button
type="button"
onClick={handleCompleteRequest}
disabled={completeLoading}
className="mt-4 w-full max-w-[360px] mx-auto bg-[#94E067] rounded-2xl py-3 flex items-center justify-center disabled:opacity-60"
>
<span className="font-montserrat font-extrabold text-[16px] text-white">
{completeLoading ? "Отправка..." : "Завершить заявку с отзывом"}
</span>
</button>
)}
{(isDone || isRejected) && (
<button
type="button"
onClick={onClose}
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>
</button>
)}
</div>
);
};
export default RequestDetailsModal;