Files
frontend/app/components/RequestDetailsModal.jsx
fullofempt 0df52352a8 WIPVOLONT
2025-12-14 21:14:55 +05:00

418 lines
15 KiB
JavaScript
Raw 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); // RequestDetail
const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState("");
const isDone = request.status === "Выполнена";
const isRejected = request.status === "Отклонена";
const [rating, setRating] = useState(0);
const [review, setReview] = useState("");
const [rejectFeedback, setRejectFeedback] = useState("");
// для приёма отклика
const [acceptLoading, setAcceptLoading] = useState(false);
const [acceptError, setAcceptError] = useState("");
const [acceptSuccess, setAcceptSuccess] = useState("");
// подгружаем детальную заявку /requests/{id}
useEffect(() => {
const fetchDetails = async () => {
if (!API_BASE) {
setLoadError("API_BASE_URL не задан");
setLoading(false);
return;
}
const saved =
typeof window !== "undefined"
? localStorage.getItem("authUser")
: null;
const authUser = saved ? JSON.parse(saved) : null;
const accessToken = authUser?.accessToken;
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(); // RequestDetail
setDetails(data);
setLoading(false);
} catch (e) {
setLoadError(e.message || "Ошибка сети");
setLoading(false);
}
};
fetchDetails();
}, [request.id]);
const handleStarClick = (value) => {
setRating(value);
};
const handleSubmit = () => {
console.log("Оставить отзыв:", {
id: request.id,
status: request.status,
rating,
review,
rejectFeedback,
});
onClose();
};
// приём отклика волонтёра: 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 || "Описание отсутствует";
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;
// здесь предполагаем, что детали заявки содержат массив 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">
<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 }}
>
{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>
{/* Блок откликов волонтёров: принять/отклонить */}
{!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">
<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>
)}
{/* Отклонена: причина + свой комментарий */}
{isRejected && (
<>
{request.rejectReason && (
<div className="bg-[#FF8282] rounded-2xl p-3">
<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">
<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>
</>
)}
{/* Оценка волонтёра только для выполненной */}
{isDone && (
<div className="mt-1 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>
)}
</div>
</div>
{/* Кнопка внизу */}
{(isDone || isRejected) && (
<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"
>
<span className="font-mонтserrat font-extrabold text-[16px] text-white">
{isRejected ? "Отправить комментарий" : "Оставить отзыв"}
</span>
</button>
)}
</div>
);
};
export default RequestDetailsModal;