diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 2e87630..6d73c56 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,20 +1,979 @@ -import { Suspense } from "react"; -import { DashboardClient } from "@/components/dashboard/dashboard-client"; +'use client'; + +import React, { useEffect, useMemo, useState } from 'react'; +import { + User, Crown, Home, ShoppingBag, Wallet, MapPin, + Headphones, IdCard, LogOut, TrendingUp, + Plus, Hash, Clock, AlertCircle, Eye, Reply, + CreditCard, Box, Shield, ShieldAlert, Mail, CheckCircle, ShieldX, CircleCheck, Search, Send, X, LoaderCircle +} from 'lucide-react'; +import { useRouter } from "next/navigation"; +import { logoutUser } from "@/public/src/services/auth/api"; +import { + closeUserTicket, + createUserTicket, + getAdminTicketById, + getAdminTickets, + getUserTicketById, + getUserTickets, + sendAdminTicketMessage, + sendUserTicketMessage, + TicketDetail, + TicketListItem, + TicketPriority, + TicketStatus, + updateAdminTicket, +} from "@/public/src/services/tickets/api"; + +const sampleOrders = [ + { id: "PR-10452", status: "در حال پردازش", statusColor: "amber", total: "500,000", regDate: "15 دی 1404", deliveryDate: "20 دی 1404", delivered: false }, + { id: "PR-10451", status: "تحویل شده", statusColor: "green", total: "120,000", regDate: "10 دی 1404", deliveryDate: "12 دی 1404", delivered: true }, +]; + +const sampleAddresses = [ + { id: "addr1", text: "شیراز، بلوار معالی آباد، خیابان پزشکان، ساختمان پارس، واحد 4", receiver: "علی محمدی", phone: "09123456789", isDefault: true }, + { id: "addr2", text: "تهران، خیابان سعدی جنوبی، کوچه ناظم‌الاطباء، پلاک 20", receiver: "دفتر تهران", phone: "021-33900000", isDefault: false }, +]; + +const statusLabels: Record = { + waiting_for_admin: "در انتظار پاسخ پشتیبانی", + waiting_for_user: "در انتظار پاسخ کاربر", + closed: "بسته شده", +}; + +const priorityLabels: Record = { + low: "کم", + normal: "عادی", + high: "زیاد", + urgent: "فوری", +}; + +type TicketCounts = { + total: number; + waiting_for_admin: number; + waiting_for_user: number; + closed: number; +}; + +export default function DashboardPage() { + const [activeTab, setActiveTab] = useState('dash'); + const router = useRouter(); + const [success, setSuccess] = useState<"" | "login" | "register">(""); + const [authorized, setAuthorized] = useState(null); + const [token, setToken] = useState(""); + const [userInfo, setUserInfo] = useState({ + fullName: "کاربر پارس شاپ", + username: "user", + phone: "شماره ثبت نشده", + role: "user" as "user" | "admin", + }); + const [ticketCounts, setTicketCounts] = useState({ + total: 0, + waiting_for_admin: 0, + waiting_for_user: 0, + closed: 0, + }); + const [userTickets, setUserTickets] = useState([]); + const [userTicketsLoading, setUserTicketsLoading] = useState(false); + const [userTicketsError, setUserTicketsError] = useState(""); + const [selectedUserTicketId, setSelectedUserTicketId] = useState(""); + const [selectedUserTicket, setSelectedUserTicket] = useState(null); + const [ticketDetailLoading, setTicketDetailLoading] = useState(false); + const [ticketReply, setTicketReply] = useState(""); + const [ticketReplyLoading, setTicketReplyLoading] = useState(false); + const [ticketStatusFilter, setTicketStatusFilter] = useState<"" | TicketStatus>(""); + const [ticketPage, setTicketPage] = useState(1); + const [ticketTotalPages, setTicketTotalPages] = useState(1); + const [isCreateTicketOpen, setIsCreateTicketOpen] = useState(false); + const [createTicketLoading, setCreateTicketLoading] = useState(false); + const [createTicketError, setCreateTicketError] = useState(""); + const [createTicketForm, setCreateTicketForm] = useState({ + subject: "", + priority: "normal" as TicketPriority, + message: "", + }); + const [adminTickets, setAdminTickets] = useState([]); + const [adminTicketsLoading, setAdminTicketsLoading] = useState(false); + const [adminTicketsError, setAdminTicketsError] = useState(""); + const [adminStatusFilter, setAdminStatusFilter] = useState<"" | TicketStatus>(""); + const [adminPriorityFilter, setAdminPriorityFilter] = useState<"" | TicketPriority>(""); + const [adminSearch, setAdminSearch] = useState(""); + const [adminPage, setAdminPage] = useState(1); + const [adminTotalPages, setAdminTotalPages] = useState(1); + const [selectedAdminTicketId, setSelectedAdminTicketId] = useState(""); + const [selectedAdminTicket, setSelectedAdminTicket] = useState(null); + const [adminReply, setAdminReply] = useState(""); + const [adminReplyLoading, setAdminReplyLoading] = useState(false); + const [adminUpdateLoading, setAdminUpdateLoading] = useState(false); + const [adminTicketForm, setAdminTicketForm] = useState({ + status: "waiting_for_user" as TicketStatus, + priority: "normal" as TicketPriority, + assignedAdminId: "", + }); + const ticketStatValue = useMemo(() => `${ticketCounts.total.toLocaleString('fa-IR')} مورد`, [ticketCounts.total]); + + useEffect(() => { + const token = localStorage.getItem("accessToken"); + const role = localStorage.getItem("role"); + const params = new URLSearchParams(window.location.search); + const currentTab = params.get("tab"); + const currentSuccess = params.get("success"); + + if (currentTab && ['dash', 'orders', 'wallet', 'address', 'tickets', 'profile'].includes(currentTab)) { + setActiveTab(currentTab); + } + if (currentSuccess === "login" || currentSuccess === "register") { + setSuccess(currentSuccess); + } + + if (token && (role === "user" || role === "admin")) { + setToken(token); + setUserInfo({ + fullName: localStorage.getItem("fullName") || localStorage.getItem("username") || "کاربر پارس شاپ", + username: localStorage.getItem("username") || "user", + phone: localStorage.getItem("phone") || "شماره ثبت نشده", + role: role, + }); + setAuthorized(true); + } else { + setAuthorized(false); + } + }, []); + + useEffect(() => { + if (!token) { + return; + } + loadTicketCounts(); + }, [token, userInfo.role]); + + useEffect(() => { + if (!token || activeTab !== 'tickets') { + return; + } + if (userInfo.role === "admin") { + loadAdminTickets(); + return; + } + loadUserTickets(); + }, [token, activeTab, userInfo.role, ticketStatusFilter, ticketPage, adminStatusFilter, adminPriorityFilter, adminSearch, adminPage]); + + useEffect(() => { + if (!token) { + return; + } + if (userInfo.role === "admin") { + if (selectedAdminTicketId) { + loadAdminTicketDetail(selectedAdminTicketId); + } + return; + } + if (selectedUserTicketId) { + loadUserTicketDetail(selectedUserTicketId); + } + }, [token, userInfo.role, selectedUserTicketId, selectedAdminTicketId]); + + const handleLogout = async () => { + 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("/"); + } + }; + + const loadTicketCounts = async () => { + try { + if (userInfo.role === "admin") { + const [allRes, adminWaitRes, userWaitRes, closedRes] = await Promise.all([ + getAdminTickets(token, { page: 1, limit: 1 }), + getAdminTickets(token, { status: "waiting_for_admin", page: 1, limit: 1 }), + getAdminTickets(token, { status: "waiting_for_user", page: 1, limit: 1 }), + getAdminTickets(token, { status: "closed", page: 1, limit: 1 }), + ]); + setTicketCounts({ + total: allRes.pagination.total, + waiting_for_admin: adminWaitRes.pagination.total, + waiting_for_user: userWaitRes.pagination.total, + closed: closedRes.pagination.total, + }); + return; + } + + const [allRes, adminWaitRes, userWaitRes, closedRes] = await Promise.all([ + getUserTickets(token, { page: 1, limit: 1 }), + getUserTickets(token, { status: "waiting_for_admin", page: 1, limit: 1 }), + getUserTickets(token, { status: "waiting_for_user", page: 1, limit: 1 }), + getUserTickets(token, { status: "closed", page: 1, limit: 1 }), + ]); + setTicketCounts({ + total: allRes.pagination.total, + waiting_for_admin: adminWaitRes.pagination.total, + waiting_for_user: userWaitRes.pagination.total, + closed: closedRes.pagination.total, + }); + } catch (error) { + console.error("Ticket count load failed:", error); + } + }; + + const loadUserTickets = async () => { + setUserTicketsLoading(true); + setUserTicketsError(""); + try { + const response = await getUserTickets(token, { + status: ticketStatusFilter || undefined, + page: ticketPage, + limit: 10, + }); + setUserTickets(response.items); + setTicketTotalPages(response.pagination.totalPages || 1); + if (!selectedUserTicketId && response.items[0]) { + setSelectedUserTicketId(response.items[0].id); + } + if (!response.items.length) { + setSelectedUserTicketId(""); + setSelectedUserTicket(null); + } + } catch (error: any) { + setUserTicketsError(error?.message || "دریافت تیکت‌ها انجام نشد."); + } finally { + setUserTicketsLoading(false); + } + }; + + const loadUserTicketDetail = async (ticketId: string) => { + setTicketDetailLoading(true); + try { + const detail = await getUserTicketById(token, ticketId); + setSelectedUserTicket(detail); + } catch (error) { + console.error("User ticket detail failed:", error); + } finally { + setTicketDetailLoading(false); + } + }; + + const loadAdminTickets = async () => { + setAdminTicketsLoading(true); + setAdminTicketsError(""); + try { + const response = await getAdminTickets(token, { + status: adminStatusFilter || undefined, + priority: adminPriorityFilter || undefined, + search: adminSearch || undefined, + page: adminPage, + limit: 10, + }); + setAdminTickets(response.items); + setAdminTotalPages(response.pagination.totalPages || 1); + if (!selectedAdminTicketId && response.items[0]) { + setSelectedAdminTicketId(response.items[0].id); + } + if (!response.items.length) { + setSelectedAdminTicketId(""); + setSelectedAdminTicket(null); + } + } catch (error: any) { + setAdminTicketsError(error?.message || "دریافت تیکت‌های ادمین انجام نشد."); + } finally { + setAdminTicketsLoading(false); + } + }; + + const loadAdminTicketDetail = async (ticketId: string) => { + setTicketDetailLoading(true); + try { + const detail = await getAdminTicketById(token, ticketId); + setSelectedAdminTicket(detail); + setAdminTicketForm({ + status: detail.status, + priority: detail.priority, + assignedAdminId: detail.assignedAdmin?.id || "", + }); + } catch (error) { + console.error("Admin ticket detail failed:", error); + } finally { + setTicketDetailLoading(false); + } + }; + + const handleCreateTicket = async () => { + if (!createTicketForm.subject.trim() || !createTicketForm.message.trim()) { + setCreateTicketError("موضوع و متن پیام را کامل کنید."); + return; + } + setCreateTicketLoading(true); + setCreateTicketError(""); + try { + const detail = await createUserTicket(token, { + subject: createTicketForm.subject.trim(), + priority: createTicketForm.priority, + message: createTicketForm.message.trim(), + }); + setIsCreateTicketOpen(false); + setCreateTicketForm({ subject: "", priority: "normal", message: "" }); + setSelectedUserTicketId(detail.id); + setSelectedUserTicket(detail); + setTicketPage(1); + await Promise.all([loadUserTickets(), loadTicketCounts()]); + } catch (error: any) { + setCreateTicketError(error?.message || "ثبت تیکت انجام نشد."); + } finally { + setCreateTicketLoading(false); + } + }; + + const handleSendUserReply = async () => { + if (!selectedUserTicket || !ticketReply.trim()) return; + setTicketReplyLoading(true); + try { + const detail = await sendUserTicketMessage(token, selectedUserTicket.id, ticketReply.trim()); + setSelectedUserTicket(detail); + setTicketReply(""); + await Promise.all([loadUserTickets(), loadTicketCounts()]); + } catch (error) { + console.error("User reply failed:", error); + } finally { + setTicketReplyLoading(false); + } + }; + + const handleCloseTicket = async () => { + if (!selectedUserTicket) return; + try { + const detail = await closeUserTicket(token, selectedUserTicket.id); + setSelectedUserTicket(detail); + await Promise.all([loadUserTickets(), loadTicketCounts()]); + } catch (error) { + console.error("Close ticket failed:", error); + } + }; + + const handleAdminReply = async () => { + if (!selectedAdminTicket || !adminReply.trim()) return; + setAdminReplyLoading(true); + try { + const detail = await sendAdminTicketMessage(token, selectedAdminTicket.id, adminReply.trim()); + setSelectedAdminTicket(detail); + setAdminReply(""); + await Promise.all([loadAdminTickets(), loadTicketCounts()]); + } catch (error) { + console.error("Admin reply failed:", error); + } finally { + setAdminReplyLoading(false); + } + }; + + const handleAdminUpdate = async () => { + if (!selectedAdminTicket) return; + setAdminUpdateLoading(true); + try { + const detail = await updateAdminTicket(token, selectedAdminTicket.id, { + status: adminTicketForm.status, + priority: adminTicketForm.priority, + assignedAdminId: adminTicketForm.assignedAdminId.trim() || undefined, + }); + setSelectedAdminTicket(detail); + await Promise.all([loadAdminTickets(), loadTicketCounts()]); + } catch (error) { + console.error("Admin update failed:", error); + } finally { + setAdminUpdateLoading(false); + } + }; + + 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: 'تیکت پشتیبانی', icon: Headphones }, + { id: 'profile', label: 'مشخصات حساب', icon: IdCard }, + ]; + + if (authorized === null) { + return ( +
+

در حال بررسی دسترسی...

+
+ ); + } + + if (!authorized) { + return ( +
+
+
+
+ +
+
+

دسترسی غیرمجاز

+

برای مشاهده داشبورد باید ابتدا وارد حساب کاربری خود شوید.

+ +
+
+ ); + } -function DashboardFallback() { return ( -
-
- در حال بارگذاری پیشخوان... +
+
+ {success === "login" &&
ورود با موفقیت انجام شد
} + {success === "register" &&
ثبت نام با موفقیت انجام شد
} + +
+ + +
+ {activeTab === 'dash' && ( +
+

خوش آمدید

+
+ } color="bg-blue-50 text-blue-600" label="موجودی کیف پول" value="1,500,000 تومان" /> + } color="bg-green-50 text-green-600" label="سفارشات جاری" value="2 مورد" /> + } color="bg-orange-50 text-orange-600" label="تیکت‌ها" value={ticketStatValue} /> + } color="bg-purple-50 text-purple-600" label="امتیاز وفاداری" value="2,340 امتیاز" /> +
+
+ )} + + {activeTab === 'tickets' && ( + userInfo.role === "admin" ? ( + { setAdminStatusFilter(value); setAdminPage(1); }} + onPriorityFilterChange={(value) => { setAdminPriorityFilter(value); setAdminPage(1); }} + onSearchChange={(value) => { setAdminSearch(value); setAdminPage(1); }} + selectedTicketId={selectedAdminTicketId} + onSelectTicket={setSelectedAdminTicketId} + selectedTicket={selectedAdminTicket} + detailLoading={ticketDetailLoading} + reply={adminReply} + onReplyChange={setAdminReply} + onReplySubmit={handleAdminReply} + replyLoading={adminReplyLoading} + ticketForm={adminTicketForm} + onTicketFormChange={setAdminTicketForm} + onTicketUpdate={handleAdminUpdate} + updateLoading={adminUpdateLoading} + page={adminPage} + totalPages={adminTotalPages} + onPrevPage={() => setAdminPage((prev) => Math.max(1, prev - 1))} + onNextPage={() => setAdminPage((prev) => Math.min(adminTotalPages, prev + 1))} + /> + ) : ( + { setTicketStatusFilter(value); setTicketPage(1); }} + selectedTicketId={selectedUserTicketId} + onSelectTicket={setSelectedUserTicketId} + selectedTicket={selectedUserTicket} + detailLoading={ticketDetailLoading} + reply={ticketReply} + onReplyChange={setTicketReply} + onReplySubmit={handleSendUserReply} + replyLoading={ticketReplyLoading} + onCloseTicket={handleCloseTicket} + onOpenCreateTicket={() => setIsCreateTicketOpen(true)} + page={ticketPage} + totalPages={ticketTotalPages} + onPrevPage={() => setTicketPage((prev) => Math.max(1, prev - 1))} + onNextPage={() => setTicketPage((prev) => Math.min(ticketTotalPages, prev + 1))} + /> + ) + )} + + {activeTab === 'address' && ( +
+ {sampleAddresses.map((address) => ( +
+
{address.text}
+
+
گیرنده: {address.receiver}
+
تماس: {address.phone}
+
+
+ ))} +
+ )} + + {activeTab === 'orders' && ( +
+ {sampleOrders.map((order) => ( +
+
+
+

{order.id}

+

ثبت سفارش: {order.regDate}

+
+ {order.status} +
+
+
+
تاریخ ثبت: {order.regDate}
+
{order.delivered ? : }{order.delivered ? 'تحویل شده:' : 'تحویل تا:'} {order.deliveryDate}
+
مبلغ کل: {order.total} تومان
+
+
+
+ ))} +
+ )} + + {activeTab === 'wallet' && ( +
+ موجودی فعلی حساب شما: +
1,500,000 تومان
+
+ )} + + {activeTab === 'profile' && ( +
+

اطلاعات شخصی

+
+ + + + +
+
+
+
+
+

تغییر رمز عبور

آخرین تغییر: 2 ماه پیش

+
+
+
+
+
+

احراز هویت دو مرحله‌ای

وضعیت: غیرفعال

+
+
+
+
+
+

اطلاع‌رسانی ایمیلی

دریافت ایمیل برای هشدارهای امنیتی

+
+
+
+
+ )} +
+
+
+ + {isCreateTicketOpen && ( + setIsCreateTicketOpen(false)} + onSubmit={handleCreateTicket} + loading={createTicketLoading} + error={createTicketError} + /> + )} +
+ ); +} + +function DashboardStat({ icon, color, label, value }: { icon: React.ReactNode; color: string; label: string; value: string }) { + return ( +
+
{icon}
+
+

{label}

+ {value}
); } -export default function DashboardPage() { +function TicketSummaryCard({ value, label, color }: { value: string; label: string; color: string }) { return ( - }> - - +
+ {value} +
{label}
+
); } + +function UserTicketsView(props: { + tickets: TicketListItem[]; + loading: boolean; + error: string; + counts: TicketCounts; + statusFilter: "" | TicketStatus; + onStatusFilterChange: (value: "" | TicketStatus) => void; + selectedTicketId: string; + onSelectTicket: (id: string) => void; + selectedTicket: TicketDetail | null; + detailLoading: boolean; + reply: string; + onReplyChange: (value: string) => void; + onReplySubmit: () => void; + replyLoading: boolean; + onCloseTicket: () => void; + onOpenCreateTicket: () => void; + page: number; + totalPages: number; + onPrevPage: () => void; + onNextPage: () => void; +}) { + return ( +
+
+

تیکت‌های پشتیبانی

+ +
+
+ + + + +
+
+
+ props.onStatusFilterChange("")}>همه تیکت‌ها + props.onStatusFilterChange("waiting_for_admin")}>در انتظار پشتیبانی + props.onStatusFilterChange("waiting_for_user")}>در انتظار شما + props.onStatusFilterChange("closed")}>بسته شده +
+
+
+
+ {props.loading ? : props.error ? : props.tickets.length === 0 ? : ( + <> + {props.tickets.map((ticket) => ( + + ))} + + + )} +
+
+ {props.detailLoading ? : !props.selectedTicket ? : ( + + )} +
+
+
+ ); +} + +function AdminTicketsView(props: { + tickets: TicketListItem[]; + loading: boolean; + error: string; + counts: TicketCounts; + statusFilter: "" | TicketStatus; + priorityFilter: "" | TicketPriority; + search: string; + onStatusFilterChange: (value: "" | TicketStatus) => void; + onPriorityFilterChange: (value: "" | TicketPriority) => void; + onSearchChange: (value: string) => void; + selectedTicketId: string; + onSelectTicket: (id: string) => void; + selectedTicket: TicketDetail | null; + detailLoading: boolean; + reply: string; + onReplyChange: (value: string) => void; + onReplySubmit: () => void; + replyLoading: boolean; + ticketForm: { status: TicketStatus; priority: TicketPriority; assignedAdminId: string }; + onTicketFormChange: React.Dispatch>; + onTicketUpdate: () => void; + updateLoading: boolean; + page: number; + totalPages: number; + onPrevPage: () => void; + onNextPage: () => void; +}) { + return ( +
+
+

مدیریت تیکت‌ها

+
+
+ + + + +
+
+
+
+ + props.onSearchChange(e.target.value)} placeholder="جستجو در موضوع یا شماره تیکت" className="w-full pr-10 px-4 py-3 border border-gray-200 rounded-xl text-sm outline-none focus:border-[#ffb900]" /> +
+ + +
+
+
+
+ {props.loading ? : props.error ? : props.tickets.length === 0 ? : ( + <> + {props.tickets.map((ticket) => ( + + ))} + + + )} +
+
+ {props.detailLoading ? : !props.selectedTicket ? : ( + <> +
+
+
+
{props.selectedTicket.ticketNumber}
+

{props.selectedTicket.subject}

+
+
+ + +
+
+
+ + + props.onTicketFormChange((prev) => ({ ...prev, assignedAdminId: e.target.value }))} placeholder="assignedAdminId" className="px-4 py-3 border border-gray-200 rounded-xl text-sm outline-none focus:border-[#ffb900]" /> +
+ +
+ + + )} +
+
+
+ ); +} + +function ProfileField({ label, value }: { label: string; value: string }) { + return ( +
+ +
{value}
+
+ ); +} + +function TicketConversationCard({ ticket, reply, onReplyChange, onReplySubmit, replyLoading, onCloseTicket, adminMode = false }: { + ticket: TicketDetail; + reply: string; + onReplyChange: (value: string) => void; + onReplySubmit: () => void; + replyLoading: boolean; + onCloseTicket?: () => void; + adminMode?: boolean; +}) { + const closed = ticket.status === "closed"; + + return ( +
+
+
+
{ticket.ticketNumber}
+

{ticket.subject}

+
+
+ + +
+
+ {!adminMode && onCloseTicket && ( +
+ +
+ )} +
+ {ticket.messages.map((message) => ( +
+
+
{message.sender.fullName} • {message.senderType === 'user' ? 'کاربر' : 'پشتیبانی'}
+
{message.message}
+
{formatDateTime(message.createdAt)}
+
+
+ ))} +
+
+