This commit is contained in:
fullofempt
2025-12-15 13:18:18 +05:00
parent b5575b8e47
commit e4bfbd30cc
9 changed files with 738 additions and 747 deletions

View File

@@ -100,7 +100,7 @@ const AuthPage = () => {
setRememberMe((prev) => !prev); setRememberMe((prev) => !prev);
if (!rememberMe) setCheckboxError(false); if (!rememberMe) setCheckboxError(false);
}} }}
className={`w-5 h-5 rounded-full border border-white flex items-center justify-center ${ className={`w-10 h-6 rounded-full border border-white flex items-center justify-center ${
rememberMe ? "bg-white" : "bg-transparent" rememberMe ? "bg-white" : "bg-transparent"
}`} }`}
> >
@@ -108,14 +108,14 @@ const AuthPage = () => {
<span className="h-2 w-2 rounded-full bg-[#90D2F9]" /> <span className="h-2 w-2 rounded-full bg-[#90D2F9]" />
)} )}
</button> </button>
<p className="font-montserrat text-[10px] leading-[12px] text-white"> <p className="font-montserrat font-black text-[12px] leading-[12px] text-red-600">
Подтверждаю, что я прочитал условия использования данного Подтверждаю, что я прочитал условия использования данного
приложения приложения
</p> </p>
</div> </div>
{/* Ссылки */} {/* Ссылки */}
<div className="flex justify-between text-[11px] font-montserrat font-bold text-[#FF6363] mt-5"> <div className="flex items-center justify-center justify-between text-[15px] font-montserrat font-bold text-[#d60404d8] mt-5">
{/* <button {/* <button
type="button" type="button"
className="hover:underline" className="hover:underline"

File diff suppressed because it is too large Load Diff

View File

@@ -17,19 +17,19 @@ const TabBar = () => {
requester: [ requester: [
{ key: "home", icon: FaHome, href: "/createRequest" }, { key: "home", icon: FaHome, href: "/createRequest" },
{ key: "history", icon: FaClock, href: "/historyRequest" }, { key: "history", icon: FaClock, href: "/historyRequest" },
{ key: "news", icon: FaNewspaper, href: "/news" }, { key: "news", icon: FaNewspaper, href: "/notification" },
{ key: "profile", icon: FaCog, href: "/ProfilePage" }, { key: "profile", icon: FaCog, href: "/ProfilePage" },
], ],
volunteer: [ volunteer: [
{ key: "home", icon: FaHome, href: "/mainValounter" }, { key: "home", icon: FaHome, href: "/mainValounter" },
{ key: "history", icon: FaClock, href: "/valounterHistoryRequest" }, { key: "history", icon: FaClock, href: "/valounterHistoryRequest" },
{ key: "news", icon: FaNewspaper, href: "/volunteer/news" }, { key: "news", icon: FaNewspaper, href: "/notification" },
{ key: "profile", icon: FaCog, href: "/valounterProfilePage" }, { key: "profile", icon: FaCog, href: "/valounterProfilePage" },
], ],
moderator: [ moderator: [
{ key: "queue", icon: FaHome, href: "/moderatorMain" }, { key: "queue", icon: FaHome, href: "/moderatorMain" },
{ key: "history", icon: FaClock, href: "/moderatorHistoryRequest" }, { key: "history", icon: FaClock, href: "/moderatorHistoryRequest" },
{ key: "news", icon: FaNewspaper, href: "/moderator/news" }, { key: "news", icon: FaNewspaper, href: "/notification" },
{ key: "profile", icon: FaCog, href: "/moderatorProfilePage" }, { key: "profile", icon: FaCog, href: "/moderatorProfilePage" },
], ],
}; };

View File

@@ -4,16 +4,30 @@ import React, { useEffect, useState } from "react";
const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL; 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 VolunteerRequestDetailsModal = ({ request, onClose }) => {
const [details, setDetails] = useState(null); const [details, setDetails] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState(""); const [loadError, setLoadError] = useState("");
const normalizedStatus = String(request.rawStatus || request.status || "").toLowerCase(); // статус отклика (из списка /responses/my)
const isAccepted = normalizedStatus === "accepted"; const responseRawStatus = String(
const isInProgress = request.rawStatus || request.status || ""
normalizedStatus === "in_progress" || normalizedStatus === "inprogress" || isAccepted; ).toLowerCase();
const isDone = normalizedStatus === "completed"; const responseStatus =
responseStatusMap[responseRawStatus] ||
{ label: "Неизвестен", color: "#E2E2E2" };
const isAccepted = responseRawStatus === "accepted";
const isDone = false; // для волонтёра нет completed в ResponseStatus
const isInProgress = isAccepted; // принятый отклик ~ «в процессе»
const getAccessToken = () => { const getAccessToken = () => {
if (typeof window === "undefined") return null; if (typeof window === "undefined") return null;
@@ -38,7 +52,7 @@ const VolunteerRequestDetailsModal = ({ request, onClose }) => {
try { try {
const res = await fetch( const res = await fetch(
`${API_BASE}/requests/${request.request_id || request.id}`, `${API_BASE}/requests/${request.requestId || request.request_id || request.id}`,
{ {
headers: { headers: {
Accept: "application/json", Accept: "application/json",
@@ -78,7 +92,7 @@ const VolunteerRequestDetailsModal = ({ request, onClose }) => {
}; };
fetchDetails(); fetchDetails();
}, [request.request_id, request.id]); }, [request.requestId, request.request_id, request.id]);
if (loading) { if (loading) {
return ( return (
@@ -105,7 +119,9 @@ const VolunteerRequestDetailsModal = ({ request, onClose }) => {
); );
} }
// Тип заявки из RequestDetail
const requestTypeName = details.request_type?.name || "Не указан"; const requestTypeName = details.request_type?.name || "Не указан";
const urgencyText = (() => { const urgencyText = (() => {
switch (details.urgency) { switch (details.urgency) {
case "low": case "low":
@@ -122,6 +138,7 @@ const VolunteerRequestDetailsModal = ({ request, onClose }) => {
})(); })();
const place = [details.address, details.city].filter(Boolean).join(", "); const place = [details.address, details.city].filter(Boolean).join(", ");
const requesterName = const requesterName =
(details.requester && (details.requester &&
[details.requester.first_name, details.requester.last_name] [details.requester.first_name, details.requester.last_name]
@@ -152,26 +169,6 @@ const VolunteerRequestDetailsModal = ({ request, onClose }) => {
} }
} }
const statusColorMap = {
pending_moderation: "#E9D171",
approved: "#94E067",
in_progress: "#E971E1",
completed: "#71A5E9",
cancelled: "#FF8282",
rejected: "#FF8282",
};
const statusLabelMap = {
pending_moderation: "На модерации",
approved: "Принята",
in_progress: "В процессе",
completed: "Выполнена",
cancelled: "Отменена",
rejected: "Отклонена",
};
const rawReqStatus = String(details.status || "").toLowerCase();
const badgeColor = statusColorMap[rawReqStatus] || "#E2E2E2";
const statusLabel = statusLabelMap[rawReqStatus] || "Неизвестен";
return ( return (
<div className="fixed inset-0 z-40 flex flex-col bg-[#90D2F9] px-4 pt-4 pb-20"> <div className="fixed inset-0 z-40 flex flex-col bg-[#90D2F9] px-4 pt-4 pb-20">
{/* Хедер */} {/* Хедер */}
@@ -192,13 +189,13 @@ const VolunteerRequestDetailsModal = ({ request, onClose }) => {
{/* Карточка */} {/* Карточка */}
<div className="flex-1 flex items-start justify-center"> <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="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"> <div className="flex items-start justify-between">
<span <span
className="inline-flex items-center justify-center px-3 py-1 rounded-full font-montserrat text-[10px] font-semibold text-white" className="inline-flex items-center justify-center px-3 py-1 rounded-full font-montserrat text-[10px] font-semibold text-white"
style={{ backgroundColor: badgeColor }} style={{ backgroundColor: responseStatus.color }}
> >
{statusLabel} {responseStatus.label}
</span> </span>
<div className="text-right leading-tight"> <div className="text-right leading-tight">
<p className="font-montserrat text-[10px] text-black"> <p className="font-montserrat text-[10px] text-black">
@@ -237,10 +234,7 @@ const VolunteerRequestDetailsModal = ({ request, onClose }) => {
</div> </div>
)} )}
{/* Доп. блоки для волонтёра по желанию: {/* Блок для принятых откликов */}
- данные назначенного волонтёра details.assigned_volunteer
- статус отклика request.rawStatus / request.status
*/}
{(isAccepted || isInProgress || isDone) && details.assigned_volunteer && ( {(isAccepted || isInProgress || isDone) && details.assigned_volunteer && (
<div className="bg-[#F3F8FF] rounded-2xl px-3 py-2"> <div className="bg-[#F3F8FF] rounded-2xl px-3 py-2">
<p className="font-montserrat text-[11px] font-semibold text-black mb-1"> <p className="font-montserrat text-[11px] font-semibold text-black mb-1">

View File

@@ -41,17 +41,19 @@ const CreateRequestPage = () => {
latitude && latitude &&
longitude; longitude;
// профиль 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(() => { useEffect(() => {
const fetchProfile = async () => { const fetchProfile = async () => {
if (!API_BASE) return; if (!API_BASE) return;
const saved = const accessToken = getAccessToken();
typeof window !== "undefined"
? localStorage.getItem("authUser")
: null;
const authUser = saved ? JSON.parse(saved) : null;
const accessToken = authUser?.accessToken;
if (!accessToken) return; if (!accessToken) return;
try { try {
@@ -67,32 +69,43 @@ const CreateRequestPage = () => {
return; return;
} }
const data = await res.json(); const data = await res.json(); // UserProfile
const fullName = const fullName =
[data.first_name, data.last_name] [data.first_name, data.last_name].filter(Boolean).join(" ").trim() ||
.filter(Boolean) data.email;
.join(" ")
.trim() || data.email;
setUserName(fullName); setUserName(fullName);
// подставляем только если поля ещё пустые, чтобы не перетирать ручной ввод
if (!address && data.address) {
setAddress(data.address);
}
if (!city && data.city) {
setCity(data.city);
}
if (!phone && data.phone) {
setPhone(data.phone);
}
} catch (e) { } catch (e) {
setProfileError("Ошибка загрузки профиля"); setProfileError("Ошибка загрузки профиля");
} }
}; };
fetchProfile(); fetchProfile();
}, []); // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // однократно при монтировании
// геолокация
useEffect(() => { useEffect(() => {
if (!("geolocation" in navigator)) { if (typeof navigator === "undefined" || !("geolocation" in navigator)) {
setGeoError("Геолокация не поддерживается браузером"); setGeoError("Геолокация не поддерживается браузером");
return; return;
} }
navigator.geolocation.getCurrentPosition( navigator.geolocation.getCurrentPosition(
(pos) => { (pos) => {
const { latitude, longitude } = pos.coords; const { latitude: lat, longitude: lon } = pos.coords;
setLatitude(latitude.toFixed(6)); setLatitude(lat.toFixed(6));
setLongitude(longitude.toFixed(6)); setLongitude(lon.toFixed(6));
setGeoError(""); setGeoError("");
}, },
(err) => { (err) => {
@@ -115,13 +128,7 @@ const CreateRequestPage = () => {
setError(""); setError("");
setIsSubmitting(true); setIsSubmitting(true);
const saved = const accessToken = getAccessToken();
typeof window !== "undefined"
? localStorage.getItem("authUser")
: null;
const authUser = saved ? JSON.parse(saved) : null;
const accessToken = authUser?.accessToken;
if (!accessToken) { if (!accessToken) {
setError("Вы не авторизованы"); setError("Вы не авторизованы");
setIsSubmitting(false); setIsSubmitting(false);
@@ -132,7 +139,7 @@ const CreateRequestPage = () => {
const desired_completion_date = desiredDateTime.toISOString(); const desired_completion_date = desiredDateTime.toISOString();
const body = { const body = {
request_type_id: 1, // можно потом вынести в селект request_type_id: 1, // TODO: вынести в селект типов
title, title,
description, description,
latitude: Number(latitude), latitude: Number(latitude),
@@ -236,7 +243,7 @@ const CreateRequestPage = () => {
</div> </div>
{/* Адрес */} {/* Адрес */}
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1 mt-2">
<label className="font-montserrat font-bold text-[10px] text-white/90"> <label className="font-montserrat font-bold text-[10px] text-white/90">
Адрес Адрес
</label> </label>
@@ -258,7 +265,7 @@ const CreateRequestPage = () => {
type="text" type="text"
value={city} value={city}
onChange={(e) => setCity(e.target.value)} onChange={(e) => setCity(e.target.value)}
className="w-full bg-[#72B8E2] rounded-lg px-3 py-3 text-sm font-mонтserrat text-white placeholder:text-white/70 outline-none focus:ring-2 focus:ring-blue-200" className="w-full bg-[#72B8E2] rounded-lg px-3 py-3 text-sm font-montserrat text-white placeholder:text-white/70 outline-none focus:ring-2 focus:ring-blue-200"
placeholder="Например: Пермь" placeholder="Например: Пермь"
/> />
</div> </div>
@@ -287,7 +294,7 @@ const CreateRequestPage = () => {
step="0.000001" step="0.000001"
value={longitude} value={longitude}
onChange={(e) => setLongitude(e.target.value)} onChange={(e) => setLongitude(e.target.value)}
className="w-full bg-[#72B8E2] rounded-lg px-3 py-3 text-sm font-mонтserrat text-white placeholder:text.white/70 outline-none focus:ring-2 focus:ring-blue-200" className="w-full bg-[#72B8E2] rounded-lg px-3 py-3 text-sm font-montserrat text-white placeholder:text-white/70 outline-none focus:ring-2 focus:ring-blue-200"
placeholder="37.618423" placeholder="37.618423"
/> />
</div> </div>
@@ -299,9 +306,8 @@ const CreateRequestPage = () => {
</p> </p>
)} )}
{/* Дата и Время */} {/* Дата и Время */}
<div className="flex gap-3"> <div className="flex gap-3 mt-2">
<div className="flex-1 flex flex-col gap-1"> <div className="flex-1 flex flex-col gap-1">
<label className="font-montserrat font-bold text-[10px] text-white/90"> <label className="font-montserrat font-bold text-[10px] text-white/90">
Дата Дата
@@ -321,20 +327,20 @@ const CreateRequestPage = () => {
type="time" type="time"
value={time} value={time}
onChange={(e) => setTime(e.target.value)} onChange={(e) => setTime(e.target.value)}
className="w-full bg-[#72B8E2] rounded-lg px-3 py-3 text-sm font-mонтserrat text-white outline-none focus:ring-2 focus:ring-blue-200" className="w-full bg-[#72B8E2] rounded-lg px-3 py-3 text-sm font-montserrat text-white outline-none focus:ring-2 focus:ring-blue-200"
/> />
</div> </div>
</div> </div>
{/* Срочность */} {/* Срочность */}
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1 mt-2">
<label className="font-montserrat font-bold text-[10px] text-white/90"> <label className="font-montserrat font-bold text-[10px] text-white/90">
Срочность Срочность
</label> </label>
<select <select
value={urgency} value={urgency}
onChange={(e) => setUrgency(e.target.value)} onChange={(e) => setUrgency(e.target.value)}
className="w-full bg-[#72B8E2] rounded-lg px-3 py-3 text-sm.font-montserrat text-white outline-none focus:ring-2 focus:ring-blue-200" className="w-full bg-[#72B8E2] rounded-lg px-3 py-3 text-sm font-montserrat text-white outline-none focus:ring-2 focus:ring-blue-200"
> >
<option value="low">Низкая</option> <option value="low">Низкая</option>
<option value="medium">Средняя</option> <option value="medium">Средняя</option>
@@ -344,7 +350,7 @@ const CreateRequestPage = () => {
</div> </div>
{/* Телефон для связи */} {/* Телефон для связи */}
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1 mt-2">
<label className="font-montserrat font-bold text-[10px] text-white/90"> <label className="font-montserrat font-bold text-[10px] text-white/90">
Телефон для связи Телефон для связи
</label> </label>
@@ -352,65 +358,48 @@ const CreateRequestPage = () => {
type="tel" type="tel"
value={phone} value={phone}
onChange={(e) => setPhone(e.target.value)} onChange={(e) => setPhone(e.target.value)}
className="w-full bg-[#72B8E2] rounded-lg px-3 py-3 text-sm.font-montserrat text-white placeholder:text.white/70 outline-none focus:ring-2 focus:ring-blue-200" className="w-full bg-[#72B8E2] rounded-lg px-3 py-3 text-sm font-montserrat text-white placeholder:text-white/70 outline-none focus:ring-2 focus:ring-blue-200"
placeholder="+7 900 000 00 00" placeholder="+7 900 000 00 00"
/> />
</div> </div>
{/* Описание */} {/* Описание */}
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1 mt-2">
<label className="font-montserrat font-bold text-[10px] text-white/90"> <label className="font-montserrat font-bold text-[10px] text-white">
Описание Описание
</label> </label>
<textarea <textarea
value={description} value={description}
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
rows={3} rows={3}
className="w-full bg-[#72B8E2] rounded-lg px-3 py-3 text-sm.font-montserrat text-white.placeholder:text-white/70 outline-none focus:ring-2 focus:ring-blue-200 resize-none" className="w-full bg-[#72B8E2] rounded-lg px-3 py-3 text-sm font-montserrat text-white placeholder:text-white/70 outline-none focus:ring-2 focus:ring-blue-200 resize-none"
placeholder="Подробно опишите, что нужно сделать" placeholder="Подробно опишите, что нужно сделать"
/> />
</div> </div>
{/* Дополнительно */} {/* Дополнительно */}
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1 mt-2">
<label className="font-montserrat.font-bold text-[10px] text-white/90"> <label className="font-montserrat font-bold text-[10px] text-white/90">
Дополнительно Дополнительно
</label> </label>
<textarea <textarea
value={note} value={note}
onChange={(e) => setNote(e.target.value)} onChange={(e) => setNote(e.target.value)}
rows={2} rows={2}
className="w-full bg-[#72B8E2] rounded-lg px-3 py-3 text-sm.font-montserrat text-white.placeholder:text.white/70 outline-none focus:ring-2 focus:ring-blue-200 resize-none" className="w-full bg-[#72B8E2] rounded-lg px-3 py-3 text-sm font-montserrat text-white placeholder:text-white/70 outline-none focus:ring-2 focus:ring-blue-200 resize-none"
placeholder="Комментарий (необязательно)" placeholder="Комментарий (необязательно)"
/> />
</div> </div>
{/* Добавить фото — пока без API */}
<div className="flex items-center gap-3 mt-5">
<button
type="button"
className="w-15 h-15 bg-[#f3f3f3] rounded-lg flex items-center justify-center"
>
<span className="text-2xl text-[#E2E2E2] leading-none">+</span>
</button>
<div className="flex gap-2">
<div className="w-15 h-15 bg-[#E2E2E2] rounded-lg" />
<div className="w-15 h-15 bg-[#E2E2E2] rounded-lg" />
</div>
<span className="font-montserrat font-bold text-[14px] text-[#72B8E2] ml-2">
Добавить фото
</span>
</div>
{/* Кнопка Отправить */} {/* Кнопка Отправить */}
<button <button
type="submit" type="submit"
disabled={!isFormValid || isSubmitting} disabled={!isFormValid || isSubmitting}
className={`mt-5 w-full rounded-lg py-3 text-center font-montserrat font-bold text-sm transition-colors className={`mt-5 w-full rounded-lg py-3 text-center font-montserrat font-bold text-sm transition-colors ${
${isFormValid && !isSubmitting isFormValid && !isSubmitting
? "bg-[#94E067] text-white hover:bg-green-600" ? "bg-[#94E067] text-white hover:bg-green-600"
: "bg-[#94E067]/60 text-white/70 cursor-not-allowed" : "bg-[#94E067]/60 text-white/70 cursor-not-allowed"
}`} }`}
> >
{isSubmitting ? "Отправка..." : "Отправить"} {isSubmitting ? "Отправка..." : "Отправить"}
</button> </button>

View File

@@ -226,7 +226,7 @@ const HistoryRequestPage = () => {
</span> </span>
<div className="text-right.leading-tight"> <div className="text-right.leading-tight">
<p className="font-montserrat text-[10px] text-black"> <p className="font-montserrat text-[10px] text-black">
{`До ${req.date}`} {`От ${req.date}`}
</p> </p>
<p className="font-montserrat text-[10px] text-black"> <p className="font-montserrat text-[10px] text-black">
{req.time} {req.time}

101
app/notification/page.jsx Normal file
View File

@@ -0,0 +1,101 @@
"use client";
import React from "react";
import { FaBell, FaInfoCircle } from "react-icons/fa";
import TabBar from "../components/TabBar";
const notifications = [
{
id: 1,
title: "Обновление приложения",
date: "15.12.2025",
type: "update",
text: "Мы улучшили стабильность работы и ускорили загрузку заявок.",
},
{
id: 2,
title: "Новые возможности для волонтёров",
date: "10.12.2025",
type: "feature",
text: "Теперь можно оставлять отзывы о заказчиках после выполнения заявок.",
},
{
id: 3,
title: "Советы по безопасности",
date: "05.12.2025",
type: "info",
text: "Всегда уточняйте детали заявки и не передавайте данные третьим лицам.",
},
];
const typeConfig = {
update: { label: "Обновление", color: "#71A5E9" },
feature: { label: "Новая функция", color: "#94E067" },
info: { label: "Информация", color: "#E9D171" },
};
const NotificationsPage = () => {
return (
<div className="min-h-screen w-full bg-[#90D2F9] flex justify-center px-4">
<div className="relative w-full max-w-md flex flex-col pb-20 pt-4">
{/* Header */}
<header className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full border border-white flex items-center justify-center">
<FaBell className="text-white text-sm" />
</div>
<p className="font-montserrat font-extrabold text-[20px] leading-[22px] text-white">
Уведомления
</p>
</div>
</header>
{/* Список уведомлений */}
<main className="bg-white rounded-xl p-4 flex flex-col gap-3 max-h-[80vh] overflow-y-auto">
{notifications.length === 0 && (
<p className="font-montserrat text-sm text-black">
Пока нет уведомлений
</p>
)}
{notifications.map((n) => {
const cfg = typeConfig[n.type] || {
label: "Уведомление",
color: "#E2E2E2",
};
return (
<div
key={n.id}
className="w-full rounded-xl bg-[#F5F5F5] px-3 py-3 flex flex-col gap-2"
>
<div className="flex items-center justify-between gap-2">
<span
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full font-montserrat text-[10px] font-semibold text-white"
style={{ backgroundColor: cfg.color }}
>
<FaInfoCircle className="text-[10px]" />
{cfg.label}
</span>
<p className="font-montserrat text-[10px] text-black/70">
{n.date}
</p>
</div>
<p className="font-montserrat font-semibold text-[14px] text-black">
{n.title}
</p>
<p className="font-montserrat text-[12px] text-black/80">
{n.text}
</p>
</div>
);
})}
</main>
<TabBar />
</div>
</div>
);
};
export default NotificationsPage;

View File

@@ -35,7 +35,7 @@ const RecPasswordCodePage = () => {
setError(""); setError("");
console.log("Подтверждение кода:", code); console.log("Подтверждение кода:", code);
router.push("/home"); router.push("/");
// TODO: запрос на бэк и переход на страницу смены пароля // TODO: запрос на бэк и переход на страницу смены пароля
}; };

View File

@@ -7,13 +7,12 @@ import RequestDetailsModal from "../components/ValounterRequestDetailsModal";
const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL; const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL;
const statusMap = { // Статусы ОТКЛИКА волонтёра (ResponseStatus)
pending_moderation: { label: "На модерации", color: "#E9D171" }, const responseStatusMap = {
approved: { label: "Принята", color: "#94E067" }, pending: { label: "Ожидает ответа", color: "#E9D171" },
inprogress: { label: "В процессе", color: "#E971E1" }, accepted: { label: "Принят", color: "#94E067" },
completed: { label: "Выполнена", color: "#71A5E9" }, rejected: { label: "Отклонён", color: "#FF8282" },
cancelled: { label: "Отменена", color: "#FF8282" }, cancelled: { label: "Отменён", color: "#FF8282" },
rejected: { label: "Отклонена", color: "#FF8282" },
}; };
const HistoryRequestPage = () => { const HistoryRequestPage = () => {
@@ -52,7 +51,7 @@ const HistoryRequestPage = () => {
data.email; data.email;
setUserName(fullName); setUserName(fullName);
} catch { } catch {
// // игнорируем
} }
}; };
@@ -106,41 +105,51 @@ const HistoryRequestPage = () => {
const list = Array.isArray(data) ? data : []; const list = Array.isArray(data) ? data : [];
// status: { response_status: "...", valid: true }
const mapped = list.map((item) => { const mapped = list.map((item) => {
// VolunteerResponse.status: pending | accepted | rejected | cancelled
const rawStatus = String( const rawStatus = String(
item.status?.response_status || item.status || "" item.status?.response_status || item.status || ""
).toLowerCase(); ).toLowerCase();
const m = statusMap[rawStatus] || { const m = responseStatusMap[rawStatus] || {
label: rawStatus || "Неизвестен", label: rawStatus || "Неизвестен",
color: "#E2E2E2", color: "#E2E2E2",
}; };
const created = new Date(item.created_at); const created = item.responded_at
const date = created.toLocaleDateString("ru-RU"); ? new Date(item.responded_at)
const time = created.toLocaleTimeString("ru-RU", { : item.created_at
hour: "2-digit", ? new Date(item.created_at)
minute: "2-digit", : null;
});
const date = created
? created.toLocaleDateString("ru-RU")
: "";
const time = created
? created.toLocaleTimeString("ru-RU", {
hour: "2-digit",
minute: "2-digit",
})
: "";
return { return {
id: item.request_id ?? item.id, // отклик
title: item.title, id: item.id,
description: item.description, rawStatus, // сырой статус для модалки
status: m.label, status: m.label, // русский текст для списка
statusColor: m.color, statusColor: m.color, // цвет плашки
// данные по заявке, если backend их уже кладёт в ответ
requestId: item.request_id,
title: item.request_title || item.title || "Заявка",
description: item.request_description || item.description || "",
address: item.request_address || item.address || "",
requesterName: item.requester_name || "",
requestTypeName: item.request_type_name || "",
date, date,
time, time,
createdAt: date, createdAt: date,
address: item.city ? `${item.city}, ${item.address}` : item.address,
requesterName: item.requester_name,
requestTypeName:
item.request_type_name &&
typeof item.request_type_name === "object"
? item.request_type_name.name
: item.request_type_name,
rawStatus, // настоящий статус для модалки
}; };
}); });
@@ -156,11 +165,7 @@ const HistoryRequestPage = () => {
}, []); }, []);
const handleOpen = (req) => { const handleOpen = (req) => {
// если модалке нужен сырой статус — пробрасываем его так же, как в референсе setSelectedRequest(req);
setSelectedRequest({
...req,
status: req.rawStatus,
});
}; };
const handleClose = () => { const handleClose = () => {
@@ -198,7 +203,7 @@ const HistoryRequestPage = () => {
</p> </p>
)} )}
{/* Список заявок */} {/* Список откликов */}
<main className="space-y-3 overflow-y-auto pr-1 max-h-[80vh]"> <main className="space-y-3 overflow-y-auto pr-1 max-h-[80vh]">
{loading && ( {loading && (
<p className="text-white text-sm font-montserrat"> <p className="text-white text-sm font-montserrat">
@@ -221,12 +226,12 @@ const HistoryRequestPage = () => {
> >
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<span <span
className="inline-flex items-center justify-center px-2 py-0.5 rounded-full font-montserrat text-[12px] font-semibold text.white" className="inline-flex items-center justify-center px-2 py-0.5 rounded-full font-montserrat text-[12px] font-semibold text-white"
style={{ backgroundColor: req.statusColor }} style={{ backgroundColor: req.statusColor }}
> >
{req.status} {req.status}
</span> </span>
<div className="text-right.leading-tight"> <div className="text-right leading-tight">
<p className="font-montserrat text-[10px] text-black"> <p className="font-montserrat text-[10px] text-black">
{req.date} {req.date}
</p> </p>
@@ -251,7 +256,7 @@ const HistoryRequestPage = () => {
</p> </p>
)} )}
<div className="mt-2 w-full bg-[#94E067] rounded-lg py-3 flex items-center justify-center"> <div className="mt-2.w-full bg-[#94E067] rounded-lg py-3 flex items-center justify-center">
<span className="font-montserrat font-bold text-[15px] leading-[18px] text-white"> <span className="font-montserrat font-bold text-[15px] leading-[18px] text-white">
Развернуть Развернуть
</span> </span>