255 lines
8.3 KiB
JavaScript
255 lines
8.3 KiB
JavaScript
"use client";
|
||
|
||
import React, { useEffect, useState } from "react";
|
||
|
||
const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL;
|
||
|
||
// Статус ИМЕННО ОТКЛИКА волонтёра (ResponseStatus)
|
||
const responseStatusMap = {
|
||
pending: { label: "Ожидает ответа", color: "#E9D171" },
|
||
accepted: { label: "Принят", color: "#94E067" },
|
||
rejected: { label: "Отклонён", color: "#FF8282" },
|
||
cancelled: { label: "Отменён", color: "#FF8282" },
|
||
};
|
||
|
||
const VolunteerRequestDetailsModal = ({ request, onClose }) => {
|
||
const [details, setDetails] = useState(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [loadError, setLoadError] = useState("");
|
||
|
||
// статус отклика (из списка /responses/my)
|
||
const responseRawStatus = String(
|
||
request.rawStatus || request.status || ""
|
||
).toLowerCase();
|
||
const responseStatus =
|
||
responseStatusMap[responseRawStatus] ||
|
||
{ label: "Неизвестен", color: "#E2E2E2" };
|
||
|
||
const isAccepted = responseRawStatus === "accepted";
|
||
const isDone = false; // для волонтёра нет completed в ResponseStatus
|
||
const isInProgress = isAccepted; // принятый отклик ~ «в процессе»
|
||
|
||
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 token = getAccessToken();
|
||
if (!token) {
|
||
setLoadError("Вы не авторизованы");
|
||
setLoading(false);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const res = await fetch(
|
||
`${API_BASE}/requests/${request.requestId || request.request_id || request.id}`,
|
||
{
|
||
headers: {
|
||
Accept: "application/json",
|
||
Authorization: `Bearer ${token}`,
|
||
},
|
||
}
|
||
);
|
||
|
||
const text = await res.text();
|
||
let data = null;
|
||
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;
|
||
}
|
||
setLoadError(msg);
|
||
setLoading(false);
|
||
return;
|
||
}
|
||
|
||
setDetails(data);
|
||
setLoading(false);
|
||
} catch (e) {
|
||
setLoadError(e.message || "Ошибка сети");
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
fetchDetails();
|
||
}, [request.requestId, request.request_id, request.id]);
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="fixed inset-0 z-40 flex items-center justify-center bg-[#90D2F9]/80">
|
||
<p className="text-white font-montserrat text-sm">Загрузка заявки...</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (loadError || !details) {
|
||
return (
|
||
<div className="fixed inset-0 z-40 flex flex-col items-center justify-center bg-[#90D2F9]/80 px-4">
|
||
<p className="text-white font-montserrat text-sm mb-3">
|
||
{loadError || "Заявка не найдена"}
|
||
</p>
|
||
<button
|
||
type="button"
|
||
onClick={onClose}
|
||
className="px-4 py-2 bg-white rounded-xl font-montserrat text-sm"
|
||
>
|
||
Закрыть
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// Тип заявки из RequestDetail
|
||
const requestTypeName = details.request_type?.name || "Не указан";
|
||
|
||
const urgencyText = (() => {
|
||
switch (details.urgency) {
|
||
case "low":
|
||
return "Низкая";
|
||
case "medium":
|
||
return "Средняя";
|
||
case "high":
|
||
return "Высокая";
|
||
case "urgent":
|
||
return "Срочно";
|
||
default:
|
||
return null;
|
||
}
|
||
})();
|
||
|
||
const place = [details.address, details.city].filter(Boolean).join(", ");
|
||
|
||
const requesterName =
|
||
(details.requester &&
|
||
[details.requester.first_name, details.requester.last_name]
|
||
.filter(Boolean)
|
||
.join(" ")
|
||
.trim()) ||
|
||
details.requester?.email ||
|
||
"Заявитель";
|
||
|
||
const created = details.created_at ? new Date(details.created_at) : null;
|
||
const createdDate = created ? created.toLocaleDateString("ru-RU") : "";
|
||
const createdTime = created
|
||
? created.toLocaleTimeString("ru-RU", {
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
})
|
||
: "";
|
||
|
||
let deadlineText = "Не указано";
|
||
if (details.desired_completion_date) {
|
||
const d = new Date(details.desired_completion_date);
|
||
if (!Number.isNaN(d.getTime())) {
|
||
deadlineText = d.toLocaleDateString("ru-RU", {
|
||
day: "2-digit",
|
||
month: "2-digit",
|
||
year: "numeric",
|
||
});
|
||
}
|
||
}
|
||
|
||
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">
|
||
Заявка от {createdDate}
|
||
</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: responseStatus.color }}
|
||
>
|
||
{responseStatus.label}
|
||
</span>
|
||
<div className="text-right leading-tight">
|
||
<p className="font-montserrat text-[10px] text-black">
|
||
{createdDate}
|
||
</p>
|
||
<p className="font-montserrat text-[10px] text-black">
|
||
{createdTime}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Название задачи */}
|
||
<p className="font-montserrat font-semibold text-[16px] leading-[20px] text-black">
|
||
{details.title}
|
||
</p>
|
||
|
||
{/* Полная информация о заявке */}
|
||
<div className="flex flex-col gap-1 text-[12px] font-montserrat text-black">
|
||
<p>Тип: {requestTypeName}</p>
|
||
<p>Заявитель: {requesterName}</p>
|
||
<p>Адрес: {place || "Не указан"}</p>
|
||
{urgencyText && <p>Срочность: {urgencyText}</p>}
|
||
{details.contact_phone && <p>Телефон: {details.contact_phone}</p>}
|
||
{details.contact_notes && (
|
||
<p>Комментарий к контакту: {details.contact_notes}</p>
|
||
)}
|
||
<p>Выполнить до: {deadlineText}</p>
|
||
</div>
|
||
|
||
{/* Описание */}
|
||
{details.description && (
|
||
<div className="bg-[#E4E4E4] rounded-2xl px-3 py-2 max-h-[160px] overflow-y-auto">
|
||
<p className="text-[11px] leading-[13px] font-montserrat whitespace-pre-line">
|
||
{details.description}
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Блок для принятых откликов */}
|
||
{(isAccepted || isInProgress || isDone) && details.assigned_volunteer && (
|
||
<div className="bg-[#F3F8FF] rounded-2xl px-3 py-2">
|
||
<p className="font-montserrat text-[11px] font-semibold text-black mb-1">
|
||
Вы назначены волонтёром
|
||
</p>
|
||
<p className="font-montserrat text-[11px] text-black">
|
||
Контакты заявителя: {details.contact_phone || "не указаны"}
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default VolunteerRequestDetailsModal;
|