Files
parsshop/components/dashboard/dashboard-client.tsx
2026-03-30 14:14:35 +03:00

827 lines
52 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, { useDeferredValue, useEffect, useState } from "react";
import {
AlertCircle,
ArrowRight,
CheckCircle2,
ChevronLeft,
Clock3,
Filter,
Headphones,
Home,
IdCard,
LoaderCircle,
LogOut,
MapPin,
MessageSquareMore,
Plus,
Search,
Send,
ShoppingBag,
Sparkles,
User,
Wallet,
XCircle,
} from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation";
import { logoutUser } from "@/public/src/services/auth/api";
import {
closeUserTicket,
createUserTicket,
getAdminTicketById,
getAdminTickets,
getUserTicketById,
getUserTickets,
sendAdminTicketMessage,
sendUserTicketMessage,
TicketDetail,
TicketListItem,
TicketListResponse,
TicketPriority,
TicketStatus,
updateAdminTicket,
} from "@/public/src/services/tickets/api";
type UserRole = "user" | "admin";
type SessionState = {
token: string;
role: UserRole;
fullName: string;
username: string;
phone: string;
};
type TicketSummary = {
total: number;
waiting_for_admin: number;
waiting_for_user: number;
closed: number;
};
const sampleOrders = [
{ id: "PR-10452", status: "در حال پردازش", total: "500,000", date: "15 دی 1404" },
{ id: "PR-10451", status: "تحویل شده", total: "120,000", date: "10 دی 1404" },
];
const sampleAddresses = [
{ id: "addr1", title: "آدرس پیش‌فرض", text: "شیراز، بلوار معالی آباد، ساختمان پارس، واحد 4" },
{ id: "addr2", title: "دفتر تهران", text: "تهران، خیابان سعدی جنوبی، کوچه ناظم‌الاطباء، پلاک 20" },
];
const statusLabels: Record<TicketStatus, string> = {
waiting_for_admin: "در انتظار پاسخ پشتیبانی",
waiting_for_user: "در انتظار پاسخ کاربر",
closed: "بسته شده",
};
const priorityLabels: Record<TicketPriority, string> = {
low: "کم",
normal: "عادی",
high: "زیاد",
urgent: "فوری",
};
const statusTone: Record<TicketStatus, string> = {
waiting_for_admin: "bg-amber-100 text-amber-800 border-amber-200",
waiting_for_user: "bg-sky-100 text-sky-800 border-sky-200",
closed: "bg-zinc-200 text-zinc-700 border-zinc-300",
};
const priorityTone: Record<TicketPriority, string> = {
low: "bg-emerald-100 text-emerald-700 border-emerald-200",
normal: "bg-zinc-100 text-zinc-700 border-zinc-200",
high: "bg-orange-100 text-orange-700 border-orange-200",
urgent: "bg-rose-100 text-rose-700 border-rose-200",
};
const userTicketStatusFilters: Array<{ label: string; value: "all" | TicketStatus }> = [
{ label: "همه", value: "all" },
{ label: statusLabels.waiting_for_admin, value: "waiting_for_admin" },
{ label: statusLabels.waiting_for_user, value: "waiting_for_user" },
{ label: statusLabels.closed, value: "closed" },
];
const priorityOptions: TicketPriority[] = ["low", "normal", "high", "urgent"];
const statusOptions: TicketStatus[] = ["waiting_for_admin", "waiting_for_user", "closed"];
function formatDateTime(value?: string | null) {
if (!value) return "ثبت نشده";
return new Intl.DateTimeFormat("fa-IR", { dateStyle: "medium", timeStyle: "short" }).format(new Date(value));
}
function cn(...parts: Array<string | false | null | undefined>) {
return parts.filter(Boolean).join(" ");
}
function Badge({ className, children }: { className: string; children: React.ReactNode }) {
return <span className={cn("inline-flex items-center rounded-full border px-3 py-1 text-xs font-semibold", className)}>{children}</span>;
}
function PanelCard({ children, className = "" }: { children: React.ReactNode; className?: string }) {
return <div className={cn("rounded-[28px] border border-[#e7e3da] bg-white shadow-[0_16px_40px_rgba(34,30,23,0.05)]", className)}>{children}</div>;
}
function buildSummary(
allTickets: TicketListResponse,
waitingAdmin: TicketListResponse,
waitingUser: TicketListResponse,
closedTickets: TicketListResponse
): TicketSummary {
return {
total: allTickets.pagination.total,
waiting_for_admin: waitingAdmin.pagination.total,
waiting_for_user: waitingUser.pagination.total,
closed: closedTickets.pagination.total,
};
}
export function DashboardClient() {
const router = useRouter();
const searchParams = useSearchParams();
const success = searchParams.get("success");
const currentTab = searchParams.get("tab");
const [session, setSession] = useState<SessionState | null>(null);
const [authorized, setAuthorized] = useState<boolean | null>(null);
const [activeTab, setActiveTab] = useState("dash");
const [ticketSummary, setTicketSummary] = useState<TicketSummary>({
total: 0,
waiting_for_admin: 0,
waiting_for_user: 0,
closed: 0,
});
const [userTickets, setUserTickets] = useState<TicketListItem[]>([]);
const [userTicketsLoading, setUserTicketsLoading] = useState(false);
const [userTicketsError, setUserTicketsError] = useState("");
const [userTicketsPagination, setUserTicketsPagination] = useState({ page: 1, totalPages: 1, total: 0 });
const [userTicketStatusFilter, setUserTicketStatusFilter] = useState<"all" | TicketStatus>("all");
const [userTicketPage, setUserTicketPage] = useState(1);
const [selectedUserTicketId, setSelectedUserTicketId] = useState<string | null>(null);
const [selectedUserTicket, setSelectedUserTicket] = useState<TicketDetail | null>(null);
const [selectedUserTicketLoading, setSelectedUserTicketLoading] = useState(false);
const [userReplyMessage, setUserReplyMessage] = useState("");
const [userReplySubmitting, setUserReplySubmitting] = useState(false);
const [newTicketOpen, setNewTicketOpen] = useState(false);
const [newTicketSubmitting, setNewTicketSubmitting] = useState(false);
const [newTicketError, setNewTicketError] = useState("");
const [newTicketForm, setNewTicketForm] = useState({ subject: "", priority: "normal" as TicketPriority, message: "" });
const [adminTickets, setAdminTickets] = useState<TicketListItem[]>([]);
const [adminTicketsLoading, setAdminTicketsLoading] = useState(false);
const [adminTicketsError, setAdminTicketsError] = useState("");
const [adminTicketsPagination, setAdminTicketsPagination] = useState({ page: 1, totalPages: 1, total: 0 });
const [adminStatusFilter, setAdminStatusFilter] = useState<"" | TicketStatus>("");
const [adminPriorityFilter, setAdminPriorityFilter] = useState<"" | TicketPriority>("");
const [adminSearchInput, setAdminSearchInput] = useState("");
const deferredAdminSearch = useDeferredValue(adminSearchInput);
const [adminTicketPage, setAdminTicketPage] = useState(1);
const [selectedAdminTicketId, setSelectedAdminTicketId] = useState<string | null>(null);
const [selectedAdminTicket, setSelectedAdminTicket] = useState<TicketDetail | null>(null);
const [selectedAdminTicketLoading, setSelectedAdminTicketLoading] = useState(false);
const [adminReplyMessage, setAdminReplyMessage] = useState("");
const [adminReplySubmitting, setAdminReplySubmitting] = useState(false);
const [adminUpdateSubmitting, setAdminUpdateSubmitting] = useState(false);
const [adminUpdateForm, setAdminUpdateForm] = useState({
status: "waiting_for_user" as TicketStatus,
priority: "normal" as TicketPriority,
assignedAdminId: "",
});
useEffect(() => {
const token = localStorage.getItem("accessToken");
const role = localStorage.getItem("role") as UserRole | null;
if (!token || (role !== "user" && role !== "admin")) {
setAuthorized(false);
return;
}
setSession({
token,
role,
fullName: localStorage.getItem("fullName") || localStorage.getItem("username") || "کاربر پارس شاپ",
username: localStorage.getItem("username") || "user",
phone: localStorage.getItem("phone") || "شماره ثبت نشده",
});
setAuthorized(true);
}, []);
useEffect(() => {
const validTabs = new Set(["dash", "orders", "wallet", "address", "tickets", "profile"]);
setActiveTab(currentTab && validTabs.has(currentTab) ? currentTab : "dash");
}, [currentTab]);
useEffect(() => {
if (!session) return;
void refreshTicketSummary(session);
}, [session]);
useEffect(() => {
if (!session || activeTab !== "tickets") return;
if (session.role === "admin") {
void loadAdminTickets(session.token, {
status: adminStatusFilter || undefined,
priority: adminPriorityFilter || undefined,
search: deferredAdminSearch || undefined,
page: adminTicketPage,
limit: 12,
});
return;
}
void loadUserTickets(session.token, {
status: userTicketStatusFilter === "all" ? undefined : userTicketStatusFilter,
page: userTicketPage,
limit: 12,
});
}, [activeTab, adminPriorityFilter, adminStatusFilter, adminTicketPage, deferredAdminSearch, session, userTicketPage, userTicketStatusFilter]);
useEffect(() => {
if (!session || session.role !== "user" || !selectedUserTicketId) return;
void loadUserTicketDetail(session.token, selectedUserTicketId);
}, [selectedUserTicketId, session]);
useEffect(() => {
if (!session || session.role !== "admin" || !selectedAdminTicketId) return;
void loadAdminTicketDetail(session.token, selectedAdminTicketId);
}, [selectedAdminTicketId, session]);
async function refreshTicketSummary(currentSession: SessionState) {
try {
if (currentSession.role === "admin") {
const [allTickets, waitingAdmin, waitingUser, closedTickets] = await Promise.all([
getAdminTickets(currentSession.token, { page: 1, limit: 1 }),
getAdminTickets(currentSession.token, { status: "waiting_for_admin", page: 1, limit: 1 }),
getAdminTickets(currentSession.token, { status: "waiting_for_user", page: 1, limit: 1 }),
getAdminTickets(currentSession.token, { status: "closed", page: 1, limit: 1 }),
]);
setTicketSummary(buildSummary(allTickets, waitingAdmin, waitingUser, closedTickets));
return;
}
const [allTickets, waitingAdmin, waitingUser, closedTickets] = await Promise.all([
getUserTickets(currentSession.token, { page: 1, limit: 1 }),
getUserTickets(currentSession.token, { status: "waiting_for_admin", page: 1, limit: 1 }),
getUserTickets(currentSession.token, { status: "waiting_for_user", page: 1, limit: 1 }),
getUserTickets(currentSession.token, { status: "closed", page: 1, limit: 1 }),
]);
setTicketSummary(buildSummary(allTickets, waitingAdmin, waitingUser, closedTickets));
} catch (error) {
console.error("Failed to load ticket summary", error);
}
}
async function loadUserTickets(token: string, query: { status?: TicketStatus; page: number; limit: number }) {
setUserTicketsLoading(true);
setUserTicketsError("");
try {
const response = await getUserTickets(token, query);
setUserTickets(response.items);
setUserTicketsPagination({ page: response.pagination.page, totalPages: response.pagination.totalPages, total: response.pagination.total });
if (response.items.length > 0 && !selectedUserTicketId) setSelectedUserTicketId(response.items[0].id);
if (response.items.length === 0) {
setSelectedUserTicketId(null);
setSelectedUserTicket(null);
}
} catch (error: any) {
setUserTicketsError(error?.message || "دریافت تیکت‌ها با خطا مواجه شد.");
} finally {
setUserTicketsLoading(false);
}
}
async function loadUserTicketDetail(token: string, ticketId: string) {
setSelectedUserTicketLoading(true);
try {
setSelectedUserTicket(await getUserTicketById(token, ticketId));
} finally {
setSelectedUserTicketLoading(false);
}
}
async function loadAdminTickets(token: string, query: { status?: TicketStatus; priority?: TicketPriority; search?: string; page: number; limit: number }) {
setAdminTicketsLoading(true);
setAdminTicketsError("");
try {
const response = await getAdminTickets(token, query);
setAdminTickets(response.items);
setAdminTicketsPagination({ page: response.pagination.page, totalPages: response.pagination.totalPages, total: response.pagination.total });
if (response.items.length > 0 && !selectedAdminTicketId) setSelectedAdminTicketId(response.items[0].id);
if (response.items.length === 0) {
setSelectedAdminTicketId(null);
setSelectedAdminTicket(null);
}
} catch (error: any) {
setAdminTicketsError(error?.message || "دریافت تیکت‌های ادمین با خطا مواجه شد.");
} finally {
setAdminTicketsLoading(false);
}
}
async function loadAdminTicketDetail(token: string, ticketId: string) {
setSelectedAdminTicketLoading(true);
try {
const detail = await getAdminTicketById(token, ticketId);
setSelectedAdminTicket(detail);
setAdminUpdateForm({
status: detail.status,
priority: detail.priority,
assignedAdminId: detail.assignedAdmin?.id || "",
});
} finally {
setSelectedAdminTicketLoading(false);
}
}
async function handleLogout() {
const token = localStorage.getItem("accessToken");
try {
if (token) await logoutUser(token);
} catch (error) {
console.error("Logout failed:", error);
} finally {
localStorage.removeItem("accessToken");
localStorage.removeItem("refreshToken");
localStorage.removeItem("username");
localStorage.removeItem("fullName");
localStorage.removeItem("phone");
localStorage.removeItem("role");
setAuthorized(false);
router.push("/");
}
}
async function handleCreateTicket() {
if (!session) return;
setNewTicketError("");
if (!newTicketForm.subject.trim() || !newTicketForm.message.trim()) {
setNewTicketError("موضوع و متن پیام را کامل کنید.");
return;
}
setNewTicketSubmitting(true);
try {
const detail = await createUserTicket(session.token, {
subject: newTicketForm.subject.trim(),
priority: newTicketForm.priority,
message: newTicketForm.message.trim(),
});
setNewTicketOpen(false);
setNewTicketForm({ subject: "", priority: "normal", message: "" });
setSelectedUserTicketId(detail.id);
setUserTicketPage(1);
await Promise.all([
loadUserTickets(session.token, { status: userTicketStatusFilter === "all" ? undefined : userTicketStatusFilter, page: 1, limit: 12 }),
refreshTicketSummary(session),
]);
} catch (error: any) {
setNewTicketError(error?.message || "ثبت تیکت انجام نشد.");
} finally {
setNewTicketSubmitting(false);
}
}
async function handleSendUserReply() {
if (!session || !selectedUserTicketId || !userReplyMessage.trim()) return;
setUserReplySubmitting(true);
try {
setSelectedUserTicket(await sendUserTicketMessage(session.token, selectedUserTicketId, { message: userReplyMessage.trim() }));
setUserReplyMessage("");
await Promise.all([
loadUserTickets(session.token, { status: userTicketStatusFilter === "all" ? undefined : userTicketStatusFilter, page: userTicketPage, limit: 12 }),
refreshTicketSummary(session),
]);
} finally {
setUserReplySubmitting(false);
}
}
async function handleCloseUserTicket() {
if (!session || !selectedUserTicketId) return;
setSelectedUserTicket(await closeUserTicket(session.token, selectedUserTicketId));
await Promise.all([
loadUserTickets(session.token, { status: userTicketStatusFilter === "all" ? undefined : userTicketStatusFilter, page: userTicketPage, limit: 12 }),
refreshTicketSummary(session),
]);
}
async function handleAdminReply() {
if (!session || !selectedAdminTicketId || !adminReplyMessage.trim()) return;
setAdminReplySubmitting(true);
try {
setSelectedAdminTicket(await sendAdminTicketMessage(session.token, selectedAdminTicketId, { message: adminReplyMessage.trim() }));
setAdminReplyMessage("");
await Promise.all([
loadAdminTickets(session.token, { status: adminStatusFilter || undefined, priority: adminPriorityFilter || undefined, search: deferredAdminSearch || undefined, page: adminTicketPage, limit: 12 }),
refreshTicketSummary(session),
]);
} finally {
setAdminReplySubmitting(false);
}
}
async function handleAdminUpdateTicket() {
if (!session || !selectedAdminTicketId) return;
setAdminUpdateSubmitting(true);
try {
setSelectedAdminTicket(await updateAdminTicket(session.token, selectedAdminTicketId, {
status: adminUpdateForm.status,
priority: adminUpdateForm.priority,
assignedAdminId: adminUpdateForm.assignedAdminId.trim() || undefined,
}));
await Promise.all([
loadAdminTickets(session.token, { status: adminStatusFilter || undefined, priority: adminPriorityFilter || undefined, search: deferredAdminSearch || undefined, page: adminTicketPage, limit: 12 }),
refreshTicketSummary(session),
]);
} finally {
setAdminUpdateSubmitting(false);
}
}
if (authorized === null) {
return <LoaderBlock label="در حال بررسی دسترسی..." />;
}
if (!authorized || !session) {
return (
<div className="min-h-screen bg-[#f7f3ec] px-4 py-16" dir="rtl">
<PanelCard className="mx-auto max-w-xl p-10 text-center">
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-3xl bg-rose-100 text-rose-600">
<XCircle className="h-8 w-8" />
</div>
<h1 className="mt-5 text-2xl font-bold text-[#1f1c17]">دسترسی غیرمجاز</h1>
<p className="mt-3 text-sm leading-7 text-[#6e675b]">برای مشاهده پیشخوان باید ابتدا وارد حساب کاربری شوید.</p>
<button onClick={() => router.push("/")} className="mt-6 inline-flex items-center gap-2 rounded-2xl bg-[#1f1c17] px-5 py-3 text-sm font-semibold text-white">
<ArrowRight className="h-4 w-4" />
بازگشت به صفحه اصلی
</button>
</PanelCard>
</div>
);
}
const menuItems = [
{ id: "dash", label: "پیشخوان", icon: Home },
{ id: "orders", label: "سفارش‌های من", icon: ShoppingBag },
{ id: "wallet", label: "کیف پول", icon: Wallet },
{ id: "address", label: "آدرس‌ها", icon: MapPin },
{ id: "tickets", label: session.role === "admin" ? "مدیریت تیکت‌ها" : "تیکت پشتیبانی", icon: Headphones },
{ id: "profile", label: "مشخصات حساب", icon: IdCard },
];
return (
<div className="min-h-screen bg-[linear-gradient(180deg,#f7f3ec_0%,#fbf9f5_50%,#f6f2ea_100%)] py-8" dir="rtl">
<div className="mx-auto max-w-7xl px-4">
{success === "login" && <NoticeBanner text="ورود با موفقیت انجام شد." />}
{success === "register" && <NoticeBanner text="ثبت‌نام با موفقیت انجام شد." />}
<div className="grid gap-6 lg:grid-cols-[290px_minmax(0,1fr)]">
<PanelCard className="overflow-hidden">
<div className="border-b border-[#efe9dd] bg-[radial-gradient(circle_at_top_right,#fff3c9_0%,#fffaf0_45%,#ffffff_100%)] px-6 py-8">
<div className="flex h-20 w-20 items-center justify-center rounded-[28px] bg-white text-[#1f1c17] shadow-[0_14px_30px_rgba(56,46,32,0.08)]">
<User className="h-9 w-9" strokeWidth={1.5} />
</div>
<div className="mt-5 text-xl font-bold text-[#1f1c17]">{session.fullName}</div>
<div className="mt-1 text-sm text-[#6e675b]" dir="ltr">{session.phone}</div>
<div className="mt-5 inline-flex items-center gap-2 rounded-full bg-[#1f1c17] px-3 py-1.5 text-xs font-semibold text-white">
<Sparkles className="h-3.5 w-3.5" />
{session.role === "admin" ? "ادمین پنل" : "حساب کاربری فعال"}
</div>
</div>
<div className="p-4">
<div className="space-y-1">
{menuItems.map((item) => (
<button
key={item.id}
onClick={() => setActiveTab(item.id)}
className={cn("flex w-full items-center gap-3 rounded-2xl px-4 py-3 text-sm font-semibold transition", activeTab === item.id ? "bg-[#1f1c17] text-white" : "text-[#5f584e] hover:bg-[#f7f2ea]")}
>
<item.icon className="h-4 w-4" />
{item.label}
</button>
))}
</div>
<div className="mt-4 border-t border-[#efe9dd] pt-4">
<button onClick={handleLogout} className="flex w-full items-center gap-3 rounded-2xl px-4 py-3 text-sm font-semibold text-rose-600 hover:bg-rose-50">
<LogOut className="h-4 w-4" />
خروج
</button>
</div>
</div>
</PanelCard>
<div className="space-y-6">
{activeTab === "dash" && (
<>
<PanelCard className="px-6 py-7">
<p className="text-sm font-medium text-[#8a7f6e]">{session.role === "admin" ? "مرکز کنترل تیکت‌ها" : "پیشخوان شخصی شما"}</p>
<h1 className="mt-2 text-3xl font-bold text-[#1f1c17]">{session.role === "admin" ? `سلام ${session.fullName}، مدیریت پشتیبانی آماده است` : `سلام ${session.fullName}، خوش برگشتی`}</h1>
<p className="mt-3 max-w-2xl text-sm leading-7 text-[#6e675b]">{session.role === "admin" ? "تیکت‌ها را اولویت‌بندی کن و پاسخ‌ها را از همین‌جا مدیریت کن." : "سفارش‌ها، تیکت‌ها و اطلاعات حساب را در یک فضای جمع‌وجور و مینیمال مدیریت کن."}</p>
</PanelCard>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<StatCard title="کل تیکت‌ها" value={ticketSummary.total} icon={<MessageSquareMore className="h-5 w-5" />} />
<StatCard title="در انتظار پشتیبانی" value={ticketSummary.waiting_for_admin} icon={<AlertCircle className="h-5 w-5" />} />
<StatCard title="در انتظار کاربر" value={ticketSummary.waiting_for_user} icon={<Clock3 className="h-5 w-5" />} />
<StatCard title="بسته شده" value={ticketSummary.closed} icon={<CheckCircle2 className="h-5 w-5" />} />
</div>
</>
)}
{activeTab === "tickets" && (session.role === "admin" ? renderAdminTicketsView() : renderUserTicketsView())}
{activeTab === "orders" && <SimpleListCard title="سفارش‌های من" items={sampleOrders.map((order) => `${order.id}${order.status}${order.total} تومان`)} />}
{activeTab === "wallet" && <SimpleListCard title="کیف پول" items={["موجودی فعلی: 1,500,000 تومان", "کل واریزی‌ها: 5,200,000 تومان", "کل برداشت‌ها: 3,700,000 تومان"]} />}
{activeTab === "address" && <SimpleListCard title="آدرس‌ها" items={sampleAddresses.map((address) => `${address.title}${address.text}`)} />}
{activeTab === "profile" && <SimpleListCard title="مشخصات حساب" items={[session.fullName, session.username, session.phone, session.role === "admin" ? "ادمین" : "کاربر"]} />}
</div>
</div>
</div>
{newTicketOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-[#1f1c17]/40 px-4 backdrop-blur-sm" dir="rtl">
<div className="absolute inset-0" onClick={() => setNewTicketOpen(false)}></div>
<PanelCard className="relative z-10 w-full max-w-2xl p-6">
<h3 className="text-xl font-bold text-[#1f1c17]">ثبت تیکت جدید</h3>
<div className="mt-6 grid gap-4">
<input value={newTicketForm.subject} onChange={(event) => setNewTicketForm((prev) => ({ ...prev, subject: event.target.value }))} placeholder="موضوع تیکت" className="rounded-2xl border border-[#e7e3da] bg-[#fcfbf8] px-4 py-3 text-sm outline-none focus:border-[#d5b96b]" />
<select value={newTicketForm.priority} onChange={(event) => setNewTicketForm((prev) => ({ ...prev, priority: event.target.value as TicketPriority }))} className="rounded-2xl border border-[#e7e3da] bg-[#fcfbf8] px-4 py-3 text-sm outline-none focus:border-[#d5b96b]">
{priorityOptions.map((option) => <option key={option} value={option}>{priorityLabels[option]}</option>)}
</select>
<textarea value={newTicketForm.message} onChange={(event) => setNewTicketForm((prev) => ({ ...prev, message: event.target.value }))} rows={6} placeholder="متن پیام" className="resize-none rounded-2xl border border-[#e7e3da] bg-[#fcfbf8] px-4 py-3 text-sm leading-7 outline-none focus:border-[#d5b96b]" />
{newTicketError ? <div className="text-sm text-rose-600">{newTicketError}</div> : null}
<div className="flex gap-3">
<button onClick={handleCreateTicket} disabled={newTicketSubmitting} className="flex flex-1 items-center justify-center gap-2 rounded-2xl bg-[#ffcf53] px-5 py-3 text-sm font-bold text-[#1f1c17] disabled:opacity-60">
{newTicketSubmitting ? <LoaderCircle className="h-4 w-4 animate-spin" /> : <Plus className="h-4 w-4" />}
ثبت تیکت
</button>
<button onClick={() => setNewTicketOpen(false)} className="rounded-2xl border border-[#e7e3da] px-5 py-3 text-sm font-semibold text-[#5f584e]">بستن</button>
</div>
</div>
</PanelCard>
</div>
)}
</div>
);
function renderUserTicketsView() {
return (
<div className="grid gap-6 xl:grid-cols-[420px_minmax(0,1fr)]">
<PanelCard className="overflow-hidden">
<div className="bg-[linear-gradient(135deg,#fffaf0_0%,#f8f0dc_100%)] px-6 py-6">
<div className="flex items-start justify-between gap-4">
<div>
<h2 className="text-2xl font-bold text-[#1f1c17]">تیکتهای پشتیبانی</h2>
<p className="mt-2 text-sm leading-7 text-[#6e675b]">درخواستها را از اینجا پیگیری کن و پاسخها را در همان گفتگو ببین.</p>
</div>
<button onClick={() => setNewTicketOpen(true)} className="inline-flex items-center gap-2 rounded-2xl bg-[#1f1c17] px-4 py-3 text-sm font-semibold text-white">
<Plus className="h-4 w-4" />
تیکت جدید
</button>
</div>
</div>
<div className="grid grid-cols-2 gap-3 border-b border-[#efe9dd] p-6 md:grid-cols-4">
<SummaryPill title="کل" value={ticketSummary.total} />
<SummaryPill title="در انتظار پشتیبانی" value={ticketSummary.waiting_for_admin} />
<SummaryPill title="در انتظار شما" value={ticketSummary.waiting_for_user} />
<SummaryPill title="بسته" value={ticketSummary.closed} />
</div>
<div className="border-b border-[#efe9dd] p-4">
<div className="flex gap-2 overflow-x-auto pb-1">
{userTicketStatusFilters.map((filter) => (
<button key={filter.value} onClick={() => { setUserTicketStatusFilter(filter.value); setUserTicketPage(1); }} className={cn("whitespace-nowrap rounded-full px-4 py-2 text-xs font-semibold", userTicketStatusFilter === filter.value ? "bg-[#1f1c17] text-white" : "border border-[#e7e3da] bg-white text-[#6e675b]")}>
{filter.label}
</button>
))}
</div>
</div>
<div className="max-h-[720px] overflow-auto p-4">
{userTicketsLoading ? <LoaderBlock label="در حال دریافت تیکت‌ها..." /> : userTicketsError ? <div className="rounded-2xl bg-rose-50 px-4 py-3 text-sm text-rose-700">{userTicketsError}</div> : userTickets.length === 0 ? <EmptyState title="هنوز تیکتی ندارید" description="اگر سوال یا مشکلی دارید، از همین‌جا یک تیکت جدید ثبت کنید." /> : (
<div className="space-y-3">
{userTickets.map((ticket) => (
<button key={ticket.id} onClick={() => setSelectedUserTicketId(ticket.id)} className={cn("w-full rounded-[24px] border p-4 text-right", selectedUserTicketId === ticket.id ? "border-[#d7bc76] bg-[#fffaf0]" : "border-[#efe9dd] bg-[#fcfbf8]")}>
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-sm font-bold text-[#1f1c17]">{ticket.subject}</div>
<div className="mt-2 text-xs text-[#7b7367]">{ticket.ticketNumber}</div>
</div>
<Badge className={statusTone[ticket.status]}>{statusLabels[ticket.status]}</Badge>
</div>
<div className="mt-3 flex flex-wrap items-center gap-2">
<Badge className={priorityTone[ticket.priority]}>{priorityLabels[ticket.priority]}</Badge>
<span className="text-xs text-[#7b7367]">{formatDateTime(ticket.lastMessageAt || ticket.createdAt)}</span>
</div>
<div className="mt-3 rounded-2xl bg-white/80 px-3 py-3 text-xs leading-6 text-[#5f584e]">{ticket.lastMessage?.message || "هنوز پیامی ثبت نشده است."}</div>
</button>
))}
</div>
)}
</div>
<PaginationControls page={userTicketsPagination.page} totalPages={userTicketsPagination.totalPages} onPrev={() => setUserTicketPage((prev) => Math.max(1, prev - 1))} onNext={() => setUserTicketPage((prev) => Math.min(userTicketsPagination.totalPages, prev + 1))} />
</PanelCard>
{selectedUserTicketLoading ? <LoaderBlock label="در حال دریافت جزئیات تیکت..." /> : selectedUserTicket ? (
<TicketConversationCard ticket={selectedUserTicket} headerExtras={<div className="flex flex-wrap gap-2"><Badge className={statusTone[selectedUserTicket.status]}>{statusLabels[selectedUserTicket.status]}</Badge><Badge className={priorityTone[selectedUserTicket.priority]}>{priorityLabels[selectedUserTicket.priority]}</Badge></div>} composerValue={userReplyMessage} onComposerChange={setUserReplyMessage} onSend={handleSendUserReply} onCloseTicket={selectedUserTicket.status !== "closed" ? handleCloseUserTicket : undefined} sending={userReplySubmitting} composerDisabled={selectedUserTicket.status === "closed"} composerPlaceholder={selectedUserTicket.status === "closed" ? "این تیکت بسته شده و امکان ارسال پیام جدید ندارد." : "پاسخ خود را بنویسید..."} />
) : <EmptyState title="یک تیکت را انتخاب کنید" description="برای دیدن جزئیات گفتگو، یکی از تیکت‌ها را انتخاب کنید." />}
</div>
);
}
function renderAdminTicketsView() {
return (
<div className="grid gap-6 xl:grid-cols-[440px_minmax(0,1fr)]">
<PanelCard className="overflow-hidden">
<div className="bg-[linear-gradient(135deg,#fffaf0_0%,#f8f0dc_100%)] px-6 py-6">
<h2 className="text-2xl font-bold text-[#1f1c17]">مدیریت تیکتها</h2>
<p className="mt-2 text-sm leading-7 text-[#6e675b]">تیکتها را با فیلتر و جستجو مدیریت کن و پاسخها را درجا ارسال کن.</p>
</div>
<div className="grid grid-cols-2 gap-3 border-b border-[#efe9dd] p-6 md:grid-cols-4">
<SummaryPill title="کل" value={ticketSummary.total} />
<SummaryPill title="در انتظار پشتیبانی" value={ticketSummary.waiting_for_admin} />
<SummaryPill title="در انتظار کاربر" value={ticketSummary.waiting_for_user} />
<SummaryPill title="بسته" value={ticketSummary.closed} />
</div>
<div className="space-y-3 border-b border-[#efe9dd] p-4">
<div className="relative">
<Search className="pointer-events-none absolute right-4 top-1/2 h-4 w-4 -translate-y-1/2 text-[#918573]" />
<input value={adminSearchInput} onChange={(event) => { setAdminSearchInput(event.target.value); setAdminTicketPage(1); }} placeholder="جستجو در موضوع یا شماره تیکت" className="w-full rounded-2xl border border-[#e7e3da] bg-[#fcfbf8] py-3 pr-11 pl-4 text-sm outline-none focus:border-[#d5b96b]" />
</div>
<div className="grid gap-3 sm:grid-cols-2">
<select value={adminStatusFilter} onChange={(event) => { setAdminStatusFilter(event.target.value as "" | TicketStatus); setAdminTicketPage(1); }} className="rounded-2xl border border-[#e7e3da] bg-[#fcfbf8] px-4 py-3 text-sm outline-none focus:border-[#d5b96b]">
<option value="">همه وضعیتها</option>
{statusOptions.map((option) => <option key={option} value={option}>{statusLabels[option]}</option>)}
</select>
<select value={adminPriorityFilter} onChange={(event) => { setAdminPriorityFilter(event.target.value as "" | TicketPriority); setAdminTicketPage(1); }} className="rounded-2xl border border-[#e7e3da] bg-[#fcfbf8] px-4 py-3 text-sm outline-none focus:border-[#d5b96b]">
<option value="">همه اولویتها</option>
{priorityOptions.map((option) => <option key={option} value={option}>{priorityLabels[option]}</option>)}
</select>
</div>
</div>
<div className="max-h-[720px] overflow-auto p-4">
{adminTicketsLoading ? <LoaderBlock label="در حال دریافت تیکت‌ها..." /> : adminTicketsError ? <div className="rounded-2xl bg-rose-50 px-4 py-3 text-sm text-rose-700">{adminTicketsError}</div> : adminTickets.length === 0 ? <EmptyState title="تیکتی پیدا نشد" description="فیلترها را تغییر بده یا عبارت جستجو را پاک کن." /> : (
<div className="space-y-3">
{adminTickets.map((ticket) => (
<button key={ticket.id} onClick={() => setSelectedAdminTicketId(ticket.id)} className={cn("w-full rounded-[24px] border p-4 text-right", selectedAdminTicketId === ticket.id ? "border-[#d7bc76] bg-[#fffaf0]" : "border-[#efe9dd] bg-[#fcfbf8]")}>
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-sm font-bold text-[#1f1c17]">{ticket.subject}</div>
<div className="mt-2 text-xs text-[#7b7367]">{ticket.ticketNumber}</div>
</div>
<Badge className={statusTone[ticket.status]}>{statusLabels[ticket.status]}</Badge>
</div>
<div className="mt-3 flex flex-wrap gap-2">
<Badge className={priorityTone[ticket.priority]}>{priorityLabels[ticket.priority]}</Badge>
<span className="text-xs text-[#7b7367]">{ticket.assignedAdmin?.fullName || "بدون ادمین مسئول"}</span>
</div>
</button>
))}
</div>
)}
</div>
<PaginationControls page={adminTicketsPagination.page} totalPages={adminTicketsPagination.totalPages} onPrev={() => setAdminTicketPage((prev) => Math.max(1, prev - 1))} onNext={() => setAdminTicketPage((prev) => Math.min(adminTicketsPagination.totalPages, prev + 1))} />
</PanelCard>
{selectedAdminTicketLoading ? <LoaderBlock label="در حال دریافت جزئیات تیکت..." /> : selectedAdminTicket ? (
<div className="space-y-6">
<PanelCard className="p-6">
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
<div className="text-sm text-[#8a7f6e]">{selectedAdminTicket.ticketNumber}</div>
<h3 className="mt-2 text-xl font-bold text-[#1f1c17]">{selectedAdminTicket.subject}</h3>
<div className="mt-3 flex flex-wrap gap-2"><Badge className={statusTone[selectedAdminTicket.status]}>{statusLabels[selectedAdminTicket.status]}</Badge><Badge className={priorityTone[selectedAdminTicket.priority]}>{priorityLabels[selectedAdminTicket.priority]}</Badge></div>
</div>
<div className="rounded-2xl bg-[#fcfbf8] px-4 py-3 text-sm text-[#5f584e]">آخرین بروزرسانی: {formatDateTime(selectedAdminTicket.updatedAt)}</div>
</div>
<div className="mt-6 grid gap-4 md:grid-cols-2">
<ProfileField label="کاربر" value={selectedAdminTicket.user.fullName} />
<ProfileField label="نام کاربری" value={selectedAdminTicket.user.username} dir="ltr" />
<ProfileField label="شماره تماس" value={selectedAdminTicket.user.phone || "ثبت نشده"} dir="ltr" />
<ProfileField label="ادمین مسئول" value={selectedAdminTicket.assignedAdmin?.fullName || "تخصیص داده نشده"} />
</div>
</PanelCard>
<PanelCard className="p-6">
<div className="mb-4 flex items-center gap-2 text-lg font-bold text-[#1f1c17]"><Filter className="h-5 w-5 text-[#9b7a21]" />تنظیمات تیکت</div>
<div className="grid gap-4 md:grid-cols-3">
<select value={adminUpdateForm.status} onChange={(event) => setAdminUpdateForm((prev) => ({ ...prev, status: event.target.value as TicketStatus }))} className="rounded-2xl border border-[#e7e3da] bg-[#fcfbf8] px-4 py-3 text-sm outline-none focus:border-[#d5b96b]">{statusOptions.map((option) => <option key={option} value={option}>{statusLabels[option]}</option>)}</select>
<select value={adminUpdateForm.priority} onChange={(event) => setAdminUpdateForm((prev) => ({ ...prev, priority: event.target.value as TicketPriority }))} className="rounded-2xl border border-[#e7e3da] bg-[#fcfbf8] px-4 py-3 text-sm outline-none focus:border-[#d5b96b]">{priorityOptions.map((option) => <option key={option} value={option}>{priorityLabels[option]}</option>)}</select>
<input value={adminUpdateForm.assignedAdminId} onChange={(event) => setAdminUpdateForm((prev) => ({ ...prev, assignedAdminId: event.target.value }))} placeholder="assignedAdminId" className="rounded-2xl border border-[#e7e3da] bg-[#fcfbf8] px-4 py-3 text-sm outline-none focus:border-[#d5b96b]" />
</div>
<button onClick={handleAdminUpdateTicket} disabled={adminUpdateSubmitting} className="mt-4 inline-flex items-center gap-2 rounded-2xl bg-[#1f1c17] px-5 py-3 text-sm font-semibold text-white disabled:opacity-60">{adminUpdateSubmitting ? <LoaderCircle className="h-4 w-4 animate-spin" /> : <CheckCircle2 className="h-4 w-4" />}ذخیره تنظیمات</button>
<p className="mt-3 text-xs text-[#7b7367]">به خاطر نبود endpoint لیست ادمینها، `assignedAdminId` بهصورت ورودی مستقیم قرار داده شده است.</p>
</PanelCard>
<TicketConversationCard ticket={selectedAdminTicket} composerValue={adminReplyMessage} onComposerChange={setAdminReplyMessage} onSend={handleAdminReply} sending={adminReplySubmitting} composerDisabled={selectedAdminTicket.status === "closed"} composerPlaceholder={selectedAdminTicket.status === "closed" ? "این تیکت بسته شده و پاسخ جدیدی ثبت نمی‌شود." : "پاسخ پشتیبانی را اینجا بنویسید..."} />
</div>
) : <EmptyState title="یک تیکت را انتخاب کنید" description="جزئیات هر تیکت و تاریخچه پیام‌ها در این بخش نمایش داده می‌شود." />}
</div>
);
}
}
function NoticeBanner({ text }: { text: string }) {
return <div className="mb-4 flex items-center gap-2 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700"><CheckCircle2 className="h-4 w-4" />{text}</div>;
}
function EmptyState({ title, description }: { title: string; description: string }) {
return <PanelCard className="p-8 text-center"><div className="mx-auto flex h-14 w-14 items-center justify-center rounded-2xl bg-[#f6f1e8] text-[#866f43]"><MessageSquareMore className="h-7 w-7" /></div><h3 className="mt-4 text-lg font-bold text-[#1f1c17]">{title}</h3><p className="mt-2 text-sm leading-7 text-[#6e675b]">{description}</p></PanelCard>;
}
function LoaderBlock({ label }: { label: string }) {
return <div className="min-h-screen bg-[#f7f3ec] px-4 py-16" dir="rtl"><PanelCard className="mx-auto max-w-6xl px-8 py-20 text-center text-[#6e675b]"><LoaderCircle className="ml-3 inline h-5 w-5 animate-spin" />{label}</PanelCard></div>;
}
function SummaryPill({ title, value }: { title: string; value: number }) {
return <div className="rounded-2xl bg-white px-4 py-3 text-center shadow-sm"><div className="text-xl font-bold text-[#1f1c17]">{value.toLocaleString("fa-IR")}</div><div className="mt-1 text-[11px] text-[#7b7367]">{title}</div></div>;
}
function StatCard({ title, value, icon }: { title: string; value: number; icon: React.ReactNode }) {
return <PanelCard className="p-5"><div className="flex items-center justify-between"><div><div className="text-sm text-[#7b7367]">{title}</div><div className="mt-3 text-3xl font-bold text-[#1f1c17]">{value.toLocaleString("fa-IR")}</div></div><div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-[#fcfbf8] text-[#9b7a21]">{icon}</div></div></PanelCard>;
}
function SimpleListCard({ title, items }: { title: string; items: string[] }) {
return <PanelCard className="p-6"><h2 className="text-2xl font-bold text-[#1f1c17]">{title}</h2><div className="mt-6 space-y-3">{items.map((item, index) => <div key={`${title}-${index}`} className="rounded-2xl border border-[#efe9dd] bg-[#fcfbf8] px-4 py-4 text-sm leading-7 text-[#5f584e]">{item}</div>)}</div></PanelCard>;
}
function ProfileField({ label, value, dir }: { label: string; value: string; dir?: "rtl" | "ltr" }) {
return <div className="rounded-3xl border border-[#efe9dd] bg-[#fcfbf8] p-5"><div className="text-sm text-[#7b7367]">{label}</div><div className="mt-3 text-base font-semibold text-[#1f1c17]" dir={dir}>{value}</div></div>;
}
function PaginationControls({ page, totalPages, onPrev, onNext }: { page: number; totalPages: number; onPrev: () => void; onNext: () => void }) {
return (
<div className="flex items-center justify-between border-t border-[#efe9dd] px-4 py-4">
<button onClick={onPrev} disabled={page <= 1} className="rounded-2xl border border-[#e7e3da] px-4 py-2 text-xs font-semibold text-[#5f584e] disabled:opacity-50">قبلی</button>
<div className="text-xs text-[#7b7367]">صفحه {page} از {totalPages || 1}</div>
<button onClick={onNext} disabled={page >= totalPages} className="inline-flex items-center gap-2 rounded-2xl border border-[#e7e3da] px-4 py-2 text-xs font-semibold text-[#5f584e] disabled:opacity-50">بعدی<ChevronLeft className="h-4 w-4" /></button>
</div>
);
}
function TicketConversationCard({
ticket,
headerExtras,
composerValue,
onComposerChange,
onSend,
onCloseTicket,
sending,
composerDisabled,
composerPlaceholder,
}: {
ticket: TicketDetail;
headerExtras?: React.ReactNode;
composerValue: string;
onComposerChange: (value: string) => void;
onSend: () => void;
onCloseTicket?: () => void;
sending: boolean;
composerDisabled: boolean;
composerPlaceholder: string;
}) {
return (
<PanelCard className="overflow-hidden">
<div className="border-b border-[#efe9dd] bg-[#fcfbf8] px-6 py-5">
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
<div className="text-sm text-[#8a7f6e]">{ticket.ticketNumber}</div>
<h3 className="mt-2 text-xl font-bold text-[#1f1c17]">{ticket.subject}</h3>
<div className="mt-3">{headerExtras}</div>
</div>
<div className="flex flex-col items-end gap-2 text-sm text-[#6e675b]">
<span>ایجاد: {formatDateTime(ticket.createdAt)}</span>
<span>آخرین پاسخ: {formatDateTime(ticket.lastMessageAt || ticket.updatedAt)}</span>
{onCloseTicket ? <button onClick={onCloseTicket} className="mt-1 rounded-2xl border border-rose-200 px-4 py-2 text-xs font-semibold text-rose-600 hover:bg-rose-50">بستن تیکت</button> : null}
</div>
</div>
</div>
<div className="max-h-[520px] space-y-4 overflow-auto bg-[linear-gradient(180deg,#fffdfa_0%,#fbf8f2_100%)] px-6 py-6">
{ticket.messages.map((message) => (
<div key={message.id} className={cn("flex", message.senderType === "user" ? "justify-end" : "justify-start")}>
<div className={cn("max-w-[85%] rounded-[26px] px-4 py-4 shadow-sm", message.senderType === "user" ? "bg-[#1f1c17] text-white" : "border border-[#e7e3da] bg-white text-[#1f1c17]")}>
<div className={cn("text-xs font-semibold", message.senderType === "user" ? "text-white/75" : "text-[#8a7f6e]")}>{message.sender.fullName} {message.senderType === "user" ? "کاربر" : "پشتیبانی"}</div>
<div className="mt-2 text-sm leading-7">{message.message}</div>
<div className={cn("mt-3 text-[11px]", message.senderType === "user" ? "text-white/70" : "text-[#9a907f]")}>{formatDateTime(message.createdAt)}</div>
</div>
</div>
))}
</div>
<div className="border-t border-[#efe9dd] bg-white px-6 py-5">
<div className="rounded-[26px] border border-[#e7e3da] bg-[#fcfbf8] p-3">
<textarea value={composerValue} onChange={(event) => onComposerChange(event.target.value)} rows={4} disabled={composerDisabled} placeholder={composerPlaceholder} className="w-full resize-none bg-transparent px-2 py-2 text-sm leading-7 text-[#1f1c17] outline-none disabled:cursor-not-allowed disabled:text-[#9a907f]" />
<div className="mt-3 flex items-center justify-between gap-3">
<div className="text-xs text-[#8a7f6e]">{composerDisabled ? "ارسال پیام برای این تیکت غیرفعال است." : "پاسخ شما در تاریخچه گفتگو ثبت می‌شود."}</div>
<button onClick={onSend} disabled={composerDisabled || sending || !composerValue.trim()} className="inline-flex items-center gap-2 rounded-2xl bg-[#ffcf53] px-4 py-2.5 text-sm font-bold text-[#1f1c17] disabled:opacity-60">
{sending ? <LoaderCircle className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
ارسال پیام
</button>
</div>
</div>
</div>
</PanelCard>
);
}