diff --git a/app/components/ModeratorRequestDetailsModal.jsx b/app/components/ModeratorRequestDetailsModal.jsx index e38119b..1716da5 100644 --- a/app/components/ModeratorRequestDetailsModal.jsx +++ b/app/components/ModeratorRequestDetailsModal.jsx @@ -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 ( <>
-
+
- + {/* Кнопки показываем только для pending_moderation */} + {isPending && (
- + )}
{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" > - + {submitting ? "Сохранение..." : "Подтвердить"} diff --git a/app/components/RequestDetailsModal.jsx b/app/components/RequestDetailsModal.jsx index b8070c4..25c252e 100644 --- a/app/components/RequestDetailsModal.jsx +++ b/app/components/RequestDetailsModal.jsx @@ -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 (
{/* Заголовок */} -
+
+ {/* Блок откликов волонтёров: принять/отклонить */} + {!loading && !loadError && responses.length > 0 && ( +
+

+ Отклики волонтёров +

+ {responses.map((resp) => ( +
+

+ Волонтёр:{" "} + {resp.volunteer_name || resp.volunteername || resp.volunteer?.name} +

+ {resp.message && ( +

+ Сообщение:{" "} + {resp.message} +

+ )} +
+ + {/* Если позже добавишь отклонение отклика — вторая кнопка здесь */} + {/* */} +
+
+ ))} + + {acceptError && ( +

+ {acceptError} +

+ )} + {acceptSuccess && ( +

+ {acceptSuccess} +

+ )} +
+ )} + {/* Выполнена: блок отзыва */} {isDone && (
@@ -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="Напишите, как прошла помощь" />
@@ -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="Расскажите, что можно улучшить" />
@@ -286,9 +403,9 @@ const RequestDetailsModal = ({ request, onClose }) => { diff --git a/app/components/ValounterRequestDetailsModal.jsx b/app/components/ValounterRequestDetailsModal.jsx index cb3d55c..df3af73 100644 --- a/app/components/ValounterRequestDetailsModal.jsx +++ b/app/components/ValounterRequestDetailsModal.jsx @@ -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 (
@@ -73,10 +83,10 @@ const RequestDetailsModal = ({ request, onClose }) => {

- {request.date} + {createdDate}

- {request.time} + {createdTime}

@@ -92,9 +102,11 @@ const RequestDetailsModal = ({ request, onClose }) => {

Заявитель: {request.requesterName || requesterName}

Адрес: {place || "Не указан"}

{urgencyText &&

Срочность: {urgencyText}

} + {/* НОВОЕ: строка "Выполнить до" */} +

Выполнить до: {deadlineText}

- {/* Описание / список покупок */} + {/* Описание */} {request.description && (

@@ -103,7 +115,7 @@ const RequestDetailsModal = ({ request, onClose }) => {

)} - {/* Блок отзыва + рейтинг — и для Выполнена, и для В процессе */} + {/* Отзыв и рейтинг */} {(isDone || isInProgress) && ( <>
@@ -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 }) => { />
-
+

Оценить заявителя

@@ -150,12 +162,11 @@ const RequestDetailsModal = ({ request, onClose }) => {
- {/* Кнопка внизу */} {(isDone || isInProgress) && (
@@ -356,9 +372,9 @@ const MainVolunteerPage = () => { request={selectedRequest} isOpen={isPopupOpen} onClose={closePopup} - onAccept={handleAccept} - loading={acceptLoading} - error={acceptError} + // onAccept={handleAccept} + // loading={acceptLoading} + // error={acceptError} /> ); diff --git a/app/moderatorHistoryRequest/page.jsx b/app/moderatorHistoryRequest/page.jsx index e9b1f5d..5827b60 100644 --- a/app/moderatorHistoryRequest/page.jsx +++ b/app/moderatorHistoryRequest/page.jsx @@ -102,15 +102,18 @@ const HistoryRequestModeratorPage = () => { const list = Array.isArray(data) ? data : []; - // RequestListItem: id, title, description, address, city, urgency, status, requester_name, request_type_name, created_at - // оставляем только approved / rejected - const filtered = list.filter( - (item) => item.status === "approved" || item.status === "rejected" - ); + // status: { request_status: "approved" | "rejected", valid: true } + const filtered = list.filter((item) => { + const s = String(item.status?.request_status || "").toLowerCase(); + return s === "approved" || s === "rejected"; + }); const mapped = filtered.map((item) => { - const m = statusMap[item.status] || { - label: item.status, + const rawStatus = String( + item.status?.request_status || "" + ).toLowerCase(); + const m = statusMap[rawStatus] || { + label: rawStatus || "Неизвестен", color: "#E2E2E2", }; @@ -131,10 +134,9 @@ const HistoryRequestModeratorPage = () => { time, createdAt: date, fullName: item.requester_name, - address: item.city - ? `${item.city}, ${item.address}` - : item.address, - rawStatus: item.status, // "approved" | "rejected" + rejectReason: item.moderation_comment || "", + address: item.city ? `${item.city}, ${item.address}` : item.address, + rawStatus, // "approved" | "rejected" }; }); @@ -163,7 +165,9 @@ const HistoryRequestModeratorPage = () => { const handleModeratedUpdate = (updated) => { setRequests((prev) => - prev.map((r) => (r.id === updated.id ? { ...r, rawStatus: updated.status } : r)) + prev.map((r) => + r.id === updated.id ? { ...r, rawStatus: updated.status } : r + ) ); }; @@ -217,7 +221,7 @@ const HistoryRequestModeratorPage = () => { key={req.id} type="button" onClick={() => handleOpen(req)} - className="w-full text-left bg.white rounded-xl px-3 py-2 flex flex-col gap-1" + className="w-full text-left bg-white rounded-xl px-3 py-2 flex flex-col gap-1" >
{ > {req.status} -
+

{req.date}

@@ -247,7 +251,7 @@ const HistoryRequestModeratorPage = () => { {req.address}

-
+
Развернуть diff --git a/app/moderatorProfilePage/page.jsx b/app/moderatorProfilePage/page.jsx index a97482d..7e92f92 100644 --- a/app/moderatorProfilePage/page.jsx +++ b/app/moderatorProfilePage/page.jsx @@ -1,16 +1,90 @@ "use client"; -import React from "react"; +import React, { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { FaUserCircle, FaStar } from "react-icons/fa"; import TabBar from "../components/TabBar"; +const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL; + const ModeratorProfilePage = () => { const router = useRouter(); - const fullName = "Иванов Александр Сергеевич"; - const birthDate = "12.03.1990"; - const rating = 4.8; + const [profile, setProfile] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = 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 fetchProfile = async () => { + if (!API_BASE) { + setError("API_BASE_URL не задан"); + setLoading(false); + return; + } + const token = getAccessToken(); + if (!token) { + setError("Вы не авторизованы"); + setLoading(false); + return; + } + + try { + const res = await fetch(`${API_BASE}/users/me`, { + headers: { + Accept: "application/json", + Authorization: `Bearer ${token}`, + }, + }); + + 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; + } + setError(msg); + setLoading(false); + return; + } + + const data = await res.json(); // UserProfile[web:598] + setProfile(data); + setLoading(false); + } catch (e) { + setError(e.message || "Ошибка сети"); + setLoading(false); + } + }; + + fetchProfile(); + }, []); + + const fullName = + profile && + ([profile.first_name, profile.last_name].filter(Boolean).join(" ") || + profile.email); + + const rating = + profile && profile.volunteer_rating != null + ? Number(profile.volunteer_rating) + : null; + + const birthDateText = profile?.created_at + ? new Date(profile.created_at).toLocaleDateString("ru-RU") + : "—"; + + const email = profile?.email || "—"; + const phone = profile?.phone || "—"; return (
@@ -32,72 +106,99 @@ const ModeratorProfilePage = () => { {/* Карточка профиля */}
- {/* Аватар */} - - - {/* ФИО и рейтинг */} -
- {/*

- ФИО -

*/} -

- {fullName} + {loading && ( +

+ Загрузка профиля...

+ )} - {/* Рейтинг + звезды */} -
- - Рейтинг: {rating.toFixed(1)} - -
- {[1, 2, 3, 4, 5].map((star) => ( - - ))} + {error && !loading && ( +

+ {error} +

+ )} + + {!loading && profile && ( + <> + {/* Аватар */} + {profile.avatar_url ? ( + // eslint-disable-next-line @next/next/no-img-element + Аватар + ) : ( + + )} + + {/* ФИО и рейтинг */} +
+

+ {fullName} +

+ + {rating != null && ( +
+ + Рейтинг: {rating.toFixed(1)} + +
+ {[1, 2, 3, 4, 5].map((star) => ( + + ))} +
+
+ )}
-
-
- {/* Контакты и день рождения */} -
-

- Дата рождения: {birthDate} -

-

- Почта: example@mail.com -

-

- Телефон: +7 (900) 000-00-00 -

-
+ {/* Контакты и «дата рождения» как дата регистрации */} +
+

+ Дата регистрации: {birthDateText} +

+

Почта: {email}

+

+ Телефон: {phone} +

+
- {/* Кнопки */} -
- - -
+ {/* Кнопки */} +
+ + +
+ + )}
diff --git a/app/moderatorProfileSettings/page.jsx b/app/moderatorProfileSettings/page.jsx index e2f4e58..dd7a88b 100644 --- a/app/moderatorProfileSettings/page.jsx +++ b/app/moderatorProfileSettings/page.jsx @@ -1,31 +1,145 @@ "use client"; -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { FaUserCircle } from "react-icons/fa"; import TabBar from "../components/TabBar"; -const ValounterProfileSettingsPage = () => { +const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL; + +const ModeratorProfileSettingsPage = () => { const router = useRouter(); const [avatarUrl, setAvatarUrl] = useState(""); - const [fullName, setFullName] = useState("Иванов Александр Сергеевич"); - const [birthDate, setBirthDate] = useState("1990-03-12"); - const [email, setEmail] = useState("example@mail.com"); - const [phone, setPhone] = useState("+7 (900) 000-00-00"); + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [email, setEmail] = useState(""); + const [phone, setPhone] = useState(""); - const handleSave = (e) => { - e.preventDefault(); - console.log("Сохранить профиль:", { - avatarUrl, - fullName, - birthDate, - email, - phone, - }); - // здесь будет запрос на бэк + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(""); + const [saveMessage, setSaveMessage] = 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 fetchProfile = async () => { + if (!API_BASE) { + setError("API_BASE_URL не задан"); + setLoading(false); + return; + } + const token = getAccessToken(); + if (!token) { + setError("Вы не авторизованы"); + setLoading(false); + return; + } + + try { + const res = await fetch(`${API_BASE}/users/me`, { + headers: { + Accept: "application/json", + Authorization: `Bearer ${token}`, + }, + }); + + 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; + } + setError(msg); + setLoading(false); + return; + } + + const data = await res.json(); // UserProfile[web:598] + setFirstName(data.first_name || ""); + setLastName(data.last_name || ""); + setEmail(data.email || ""); + setPhone(data.phone || ""); + setAvatarUrl(data.avatar_url || ""); + setLoading(false); + } catch (e) { + setError(e.message || "Ошибка сети"); + setLoading(false); + } + }; + + fetchProfile(); + }, []); + + const handleSave = async (e) => { + e.preventDefault(); + if (!API_BASE) return; + const token = getAccessToken(); + if (!token) { + setError("Вы не авторизованы"); + return; + } + + try { + setSaving(true); + setError(""); + setSaveMessage(""); + + const body = { + first_name: firstName || null, + last_name: lastName || null, + phone: phone || null, + }; // UpdateProfileInput[web:598] + + const res = await fetch(`${API_BASE}/users/me`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(body), + }); + + 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; + } + setError(msg); + setSaving(false); + return; + } + + const updated = await res.json(); + setSaveMessage("Изменения сохранены"); + setSaving(false); + + setFirstName(updated.first_name || ""); + setLastName(updated.last_name || ""); + setPhone(updated.phone || ""); + } catch (e) { + setError(e.message || "Ошибка сети"); + setSaving(false); + } + }; + + const fullName = [firstName, lastName].filter(Boolean).join(" "); + return (
@@ -46,102 +160,128 @@ const ValounterProfileSettingsPage = () => { {/* Карточка настроек */}
- {/* Аватар */} -
-
- {avatarUrl ? ( - // eslint-disable-next-line @next/next/no-img-element - Аватар - ) : ( - + {loading && ( +

+ Загрузка профиля... +

+ )} + + {!loading && ( + <> + {/* Аватар */} +
+
+ {avatarUrl ? ( + // eslint-disable-next-line @next/next/no-img-element + Аватар + ) : ( + + )} +
+ +
+ + {error && ( +

+ {error} +

+ )} + {saveMessage && ( +

+ {saveMessage} +

)} -
- -
-
- {/* ФИО */} -
- - setFullName(e.target.value)} - className="w-full rounded-full bg-[#72B8E2] px-4 py-2 text-sm font-montserrat text-white placeholder:text-white/70 outline-none border border-transparent focus:border-white/70" - placeholder="Введите ФИО" - /> -
+ + {/* Имя */} +
+ + setFirstName(e.target.value)} + className="w-full rounded-full bg-[#72B8E2] px-4 py-2 text-sm font-montserrat text-white placeholder:text-white/70 outline-none border border-transparent focus:border-white/70" + placeholder="Введите имя" + /> +
- {/* Дата рождения */} -
- - setBirthDate(e.target.value)} - className="w-full rounded-full bg-[#72B8E2] px-4 py-2 text-sm font-montserrat text-white outline-none border border-transparent focus:border-white/70" - /> -
+ {/* Фамилия */} +
+ + setLastName(e.target.value)} + className="w-full rounded-full bg-[#72B8E2] px-4 py-2 text-sm font-montserrat text-white placeholder:text-white/70 outline-none border border-transparent focus:border-white/70" + placeholder="Введите фамилию" + /> +
- {/* Почта */} -
- - setEmail(e.target.value)} - className="w-full rounded-full bg-[#72B8E2] px-4 py-2 text-sm font-montserrat text-white placeholder:text-white/70 outline-none border border-transparent focus:border-white/70" - placeholder="example@mail.com" - /> -
+ {/* Почта (только отображение) */} +
+ + +
- {/* Телефон */} -
- - setPhone(e.target.value)} - className="w-full rounded-full bg-[#72B8E2] px-4 py-2 text-sm font-montserrat text:white.placeholder:text-white/70 outline-none border border-transparent focus:border-white/70" - placeholder="+7 (900) 000-00-00" - /> -
+ {/* Телефон */} +
+ + setPhone(e.target.value)} + className="w-full rounded-full bg-[#72B8E2] px-4 py-2 text-sm font-montserrat text-white placeholder:text-white/70 outline-none border border-transparent focus:border-white/70" + placeholder="+7 900 000 00 00" + /> +
- {/* Кнопка сохранить */} - -
+ {/* Кнопка сохранить */} + + + + )}
@@ -150,4 +290,4 @@ const ValounterProfileSettingsPage = () => { ); }; -export default ValounterProfileSettingsPage; +export default ModeratorProfileSettingsPage; diff --git a/app/valounterHistoryRequest/page.jsx b/app/valounterHistoryRequest/page.jsx index 65195f0..fd2d95d 100644 --- a/app/valounterHistoryRequest/page.jsx +++ b/app/valounterHistoryRequest/page.jsx @@ -10,7 +10,7 @@ const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL; const statusMap = { pending_moderation: { label: "На модерации", color: "#E9D171" }, approved: { label: "Принята", color: "#94E067" }, - in_progress: { label: "В процессе", color: "#E971E1" }, + inprogress: { label: "В процессе", color: "#E971E1" }, completed: { label: "Выполнена", color: "#71A5E9" }, cancelled: { label: "Отменена", color: "#FF8282" }, rejected: { label: "Отклонена", color: "#FF8282" }, @@ -19,7 +19,7 @@ const statusMap = { const HistoryRequestPage = () => { const [userName, setUserName] = useState("Волонтёр"); - const [requests, setRequests] = useState([]); // истории заявок волонтёра + const [requests, setRequests] = useState([]); const [selectedRequest, setSelectedRequest] = useState(null); const [error, setError] = useState(""); @@ -32,7 +32,7 @@ const HistoryRequestPage = () => { return authUser?.accessToken || null; }; - // подгружаем имя + // имя useEffect(() => { const fetchProfile = async () => { if (!API_BASE) return; @@ -49,20 +49,18 @@ const HistoryRequestPage = () => { if (!res.ok) return; const data = await res.json(); const fullName = - [data.first_name, data.last_name] - .filter(Boolean) - .join(" ") - .trim() || data.email; + [data.first_name, data.last_name].filter(Boolean).join(" ").trim() || + data.email; setUserName(fullName); } catch { - // оставляем дефолт + // } }; fetchProfile(); }, []); - // загружаем историю заявок волонтёра + // история: /requests/my useEffect(() => { const fetchVolunteerRequests = async () => { if (!API_BASE) { @@ -78,8 +76,12 @@ const HistoryRequestPage = () => { } try { - // вариант 1 (рекомендуется на бэке): отдельный эндпоинт, здесь предположим, что бек отдаёт RequestListItem[] - const res = await fetch(`${API_BASE}/requests/my?role=volunteer`, { + const params = new URLSearchParams({ + limit: "50", + offset: "0", + }); + + const res = await fetch(`${API_BASE}/requests/my?${params}`, { headers: { Accept: "application/json", Authorization: `Bearer ${token}`, @@ -100,10 +102,11 @@ const HistoryRequestPage = () => { return; } - const data = await res.json(); // массив RequestListItem[file:519] + const data = await res.json(); // RequestListItem[][web:598] const mapped = data.map((item) => { - const m = statusMap[item.status] || { + const key = String(item.status || "").toLowerCase(); + const m = statusMap[key] || { label: item.status, color: "#E2E2E2", }; @@ -157,7 +160,7 @@ const HistoryRequestPage = () => { {/* Header */}
-
+

@@ -201,9 +204,8 @@ const HistoryRequestPage = () => { key={req.id} type="button" onClick={() => handleOpen(req)} - className="w-full text-left bg-white rounded-xl px-3.py-2 flex flex-col gap-1" + className="w-full text-left bg-white rounded-xl px-3 py-2 flex flex-col gap-1" > - {/* верхняя строка: статус + дата/время */}

{
- {/* Заголовок заявки */}

{req.title}

- {/* Кнопка "Развернуть" */} -
+
Развернуть @@ -236,7 +236,6 @@ const HistoryRequestPage = () => { ))} - {/* Попап */} {selectedRequest && ( )} diff --git a/public/leaflet/marker-icon-2x.png b/public/leaflet/marker-icon-2x.png new file mode 100644 index 0000000..88f9e50 Binary files /dev/null and b/public/leaflet/marker-icon-2x.png differ diff --git a/public/leaflet/marker-icon.png b/public/leaflet/marker-icon.png new file mode 100644 index 0000000..950edf2 Binary files /dev/null and b/public/leaflet/marker-icon.png differ diff --git a/public/leaflet/marker-shadow.png b/public/leaflet/marker-shadow.png new file mode 100644 index 0000000..9fd2979 Binary files /dev/null and b/public/leaflet/marker-shadow.png differ