Files
frontend/app/components/ModeratorRequestDetailsModal.jsx
fullofempt 433b9e896c WIP API
2025-12-14 18:47:14 +05:00

394 lines
16 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 } from "react";
const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL;
const ModeratorRequestModal = ({ request, onClose, onModerated }) => {
const [showRejectPopup, setShowRejectPopup] = useState(false);
const [rejectReason, setRejectReason] = useState("");
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState("");
// request.status: "pending_moderation" | "approved" | "rejected"
const isApproved = request.status === "approved";
const isRejected = request.status === "rejected";
const isPending = request.status === "pending_moderation";
const getAccessToken = () => {
if (typeof window === "undefined") return null;
const saved = localStorage.getItem("authUser");
const authUser = saved ? JSON.parse(saved) : null;
return authUser?.accessToken || null;
};
const formatDate = (iso) => {
if (!iso) return "";
const d = new Date(iso);
return d.toLocaleDateString("ru-RU");
};
const formatTime = (iso) => {
if (!iso) return "";
const d = new Date(iso);
return d.toLocaleTimeString("ru-RU", {
hour: "2-digit",
minute: "2-digit",
});
};
const createdDate = request.date || "";
const createdTime = request.time || "";
const deadlineDate = request.deadlineDate || request.date || "";
const deadlineTime = request.deadlineTime || request.time || "";
const handleApprove = async () => {
if (!API_BASE || submitting) return;
const token = getAccessToken();
if (!token) {
setError("Вы не авторизованы");
return;
}
console.log("[MODERATION] APPROVE start", {
requestId: request.id,
statusBefore: request.status,
});
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 }),
}
);
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;
}
}
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;
}
onModerated?.({
...request,
status: "approved",
moderationResult: data,
});
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);
}
};
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,
});
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 }),
}
);
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;
}
}
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;
}
onModerated?.({
...request,
status: "rejected",
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);
}
};
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-8 h-8 rounded-full flex items-center justify-center text-xl"
>
</button>
<p className="flex-1 text-center font-montserrat font-extrabold text-[20px] leading-[24px] text-white">
Заявка от {createdDate || "—"}
</p>
<span className="w-8" />
</div>
<div className="flex-1 flex items-start justify-center">
<div className="w-full max-w-[400px] bg-white rounded-2xl p-4 flex flex-col gap-4 shadow-lg">
<div className="flex flex-col gap-2">
<div className="w-full bg-[#72B8E2] rounded-[10px] px-3 py-2 flex items-center justify-between">
<span className="text-[14px] font-montserrat font-bold text-white">
Описание
</span>
<div className="flex gap-2">
<div className="min-w-[80px] bg-[#72B8E2] rounded-[10px] flex flex-col items-center justify-center border border-white/30 px-2 py-1">
<span className="text-[12px] font-montserrat font-bold text-white">
Дата
</span>
<span className="text-[10px] font-montserrat text-white">
{createdDate || "—"}
</span>
</div>
<div className="min-w-[80px] bg-[#72B8E2] rounded-[10px] flex flex-col items-center justify-center border border-white/30 px-2 py-1">
<span className="text-[12px] font-montserrat font-bold text-white">
Время
</span>
<span className="text-[10px] font-montserrat text-white">
{createdTime || "—"}
</span>
</div>
</div>
</div>
</div>
<div className="w-full bg-[#72B8E2] rounded-[10px] px-3 py-2 flex flex-col gap-1">
<span className="text-[14px] font-montserrat font-bold text-white">
ФИО
</span>
<p className="text-[12px] font-montserrat text-white leading-[16px]">
{request.requesterName || "Заявитель"}
</p>
<p className="text-[12px] font-montserrat text-white leading-[14px]">
{request.address
? `${request.city ? request.city + ", " : ""}${request.address}`
: "Адрес не указан"}
</p>
</div>
<div className="flex items-center justify-between">
<div
className={`px-3 py-1 rounded-[10px] flex items-center justify-center ${isApproved
? "bg-[#94E067]"
: isRejected
? "bg-[#E06767]"
: "bg-[#E9D171]"
}`}
>
<span className="text-[12px] font-montserrat font-semibold text-black">
{isApproved
? "Принята"
: isRejected
? "Отклонена"
: "Модерация"}
</span>
</div>
<div className="flex flex-col items-end text-[12px] font-montserrat font-light text-black leading-[14px]">
<span>До {deadlineDate || "—"}</span>
<span>{deadlineTime || "—"}</span>
</div>
</div>
<p className="text-[16px] leading-[20px] font-montserrat font-semibold text-black">
{request.title || "Задача"}
</p>
{request.description && (
<div className="flex-1 bg-[#F2F2F2] rounded-[10px] px-3 py-2 overflow-y-auto">
<p className="text-[12px] leading-[16px] font-montserrat text-black whitespace-pre-line">
{request.description}
</p>
</div>
)}
{isRejected && (
<div className="w-full.bg-[#FFE2E2] rounded-[10px] px-3 py-2">
<p className="text-[14px] font-montserrat font-bold text-[#E06767] mb-1">
Причина отклонения
</p>
<p className="text-[12px] font-montserrat text-black whitespace-pre-line">
{request.rejectReason && request.rejectReason.trim().length > 0
? request.rejectReason
: "Причина не указана"}
</p>
</div>
)}
{error && (
<p className="text-[12px] font-montserrat text-red-500">
{error}
</p>
)}
</div>
</div>
<div className="mt-4 w-full max-w-[400px] mx-auto flex items-center justify-between gap-3">
<button
type="button"
onClick={handleApprove}
disabled={submitting}
className="flex-1 h-10 bg-[#94E067] rounded-[10px] flex items-center justify-center disabled:opacity-60"
>
<span className="text-[14px] font-montserrat font-bold text-white">
{submitting ? "Сохранение..." : "Принять"}
</span>
</button>
<button
type="button"
onClick={() => setShowRejectPopup(true)}
disabled={submitting}
className="flex-1 h-10 bg-[#E06767] rounded-[10px] flex items-center justify-center disabled:opacity-60"
>
<span className="text-[14px] font-montserrat font-bold text-white">
Отклонить
</span>
</button>
</div>
</div>
{showRejectPopup && (
<div className="fixed inset-0 z-50 flex flex-col bg-[rgba(101,101,101,0.72)] px-4 pt-8 pb-6">
<div className="w-full max-w-[400px] mx-auto bg-white rounded-t-[15px] flex flex-col items-center px-4 pt-4 pb-4">
<p className="font-montserrat font-bold text-[20px] leading-[24px] text-black mb-3">
Причина
</p>
<div className="w-full bg-[#72B8E2] rounded-[10px] px-3 py-3 mb-4 max-h-[50vh]">
<textarea
value={rejectReason}
onChange={(e) => setRejectReason(e.target.value)}
className="w-full h-full bg-transparent text-[14px] leading-[18px] font-montserrat text-black placeholder:text-black/60 outline-none resize-none"
placeholder="Опишите причину отклонения заявки"
/>
</div>
<div className="w-full flex flex-col gap-2">
<button
type="button"
onClick={handleRejectConfirm}
disabled={submitting}
className="w-full h-10 bg-[#E06767] rounded-[10px] flex items-center justify-center disabled:opacity-60"
>
<span className="text-[16px] font-montserrat font-bold text-white">
{submitting ? "Сохранение..." : "Подтвердить"}
</span>
</button>
<button
type="button"
onClick={() => setShowRejectPopup(false)}
disabled={submitting}
className="w-full h-10 bg-white rounded-[10px] border border-[#E06767] flex items-center justify-center"
>
<span className="text-[14px] font-montserrat font-semibold text-[#E06767]">
Отмена
</span>
</button>
</div>
</div>
</div>
)}
</>
);
};
export default ModeratorRequestModal;