1571 lines
89 KiB
TypeScript
1571 lines
89 KiB
TypeScript
"use client";
|
||
import React, { useState, useEffect } from "react";
|
||
import { motion, AnimatePresence } from "motion/react";
|
||
import { User, ShoppingBag, Wallet, MapPin, LogOut, ChevronRight, Package, Calendar, UserCircle, ShoppingCart, Trash2, Edit2, Phone, Hash, CheckCircle2, ArrowUpCircle, ArrowDownCircle, Info, Clock, CheckCircle, Heart, Sparkles, MessageSquare, Send, Stethoscope, FileText, Lock, Key, Eye, EyeOff, ShieldCheck, Dog, X, Plus, CreditCard, Activity } from "lucide-react";
|
||
import { OrderRowSkeleton } from "./Skeleton";
|
||
import { useUserStore, Address } from "../lib/store/userStore";
|
||
import { useCartStore } from "../lib/store/cartStore";
|
||
import { useSettingsStore } from "../lib/store/settingsStore";
|
||
import { toPersian, toEnglishDigits, cn } from "../lib/utils";
|
||
import { toast } from "sonner";
|
||
import OrderDetailsModal from "./OrderDetailsModal";
|
||
import AddressModal from "./AddressModal";
|
||
import DeleteConfirmModal from "./DeleteConfirmModal";
|
||
import TopUpModal from "./TopUpModal";
|
||
import { ticketService, Ticket, TicketMessage } from "../lib/services/ticketService";
|
||
import { usePetStore } from "../lib/store/usePetStore";
|
||
import PetProfile from "./PetProfile";
|
||
import api, { BASE_DOMAIN } from "../lib/services/api";
|
||
|
||
import { useRouter, useSearchParams } from 'next/navigation';
|
||
|
||
export default function UserDashboard() {
|
||
const router = useRouter();
|
||
const searchParams = useSearchParams();
|
||
const { profile, isLoggedIn, logout, updateProfile, addAddress, updateAddress, deleteAddress, setDefaultAddress, topUpWallet, fetchProfile } = useUserStore();
|
||
const { pets, activePetId, setActivePet, removePet } = usePetStore();
|
||
const isWithdrawalEnabled = useSettingsStore((s) => s.getBoolean('wallet_withdrawal_enabled', false));
|
||
|
||
React.useEffect(() => {
|
||
if (!isLoggedIn && typeof window !== "undefined") {
|
||
router.push("/");
|
||
toast.error("برای دسترسی به داشبورد لطفاً ابتدا وارد حساب کاربری خود شوید.");
|
||
}
|
||
}, [isLoggedIn, router]);
|
||
|
||
const { orders } = useCartStore();
|
||
const [activeTab, setActiveTab] = React.useState<"profile" | "orders" | "wallet" | "addresses" | "tickets" | "pets" | "prescriptions" | "overview">("profile");
|
||
|
||
// Set tab from URL query param if present
|
||
useEffect(() => {
|
||
const tabParam = searchParams.get('tab');
|
||
if (tabParam && ['profile', 'orders', 'wallet', 'addresses', 'tickets', 'pets', 'prescriptions'].includes(tabParam)) {
|
||
setActiveTab(tabParam as "profile" | "orders" | "wallet" | "addresses" | "tickets" | "pets" | "prescriptions");
|
||
}
|
||
}, [searchParams]);
|
||
|
||
const [isLoadingOrders, setIsLoadingOrders] = useState(false);
|
||
const [prescriptions, setPrescriptions] = useState<any[]>([]);
|
||
const [isLoadingPrescriptions, setIsLoadingPrescriptions] = useState(false);
|
||
|
||
const fetchPrescriptions = React.useCallback(async () => {
|
||
try {
|
||
setIsLoadingPrescriptions(true);
|
||
const res = await api.get('/prescriptions');
|
||
setPrescriptions(Array.isArray(res.data) ? res.data : (res.data?.data || []));
|
||
} catch {
|
||
// ignore
|
||
} finally {
|
||
setIsLoadingPrescriptions(false);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (activeTab === 'prescriptions') {
|
||
fetchPrescriptions();
|
||
}
|
||
}, [activeTab, fetchPrescriptions]);
|
||
|
||
// Tickets State
|
||
const [tickets, setTickets] = useState<Ticket[]>([]);
|
||
const [isLoadingTickets, setIsLoadingTickets] = useState(false);
|
||
const [isNewTicketModalOpen, setIsNewTicketModalOpen] = useState(false);
|
||
const [selectedTicket, setSelectedTicket] = useState<Ticket | null>(null);
|
||
const [newTicketSubject, setNewTicketSubject] = useState("");
|
||
const [newTicketMessage, setNewTicketMessage] = useState("");
|
||
const [newTicketCategory, setNewTicketCategory] = useState<string>("VET_CONSULTATION");
|
||
const [newTicketPriority, setNewTicketPriority] = useState<string>("MEDIUM");
|
||
const [newTicketPetId, setNewTicketPetId] = useState<string>("");
|
||
const [isSubmittingTicket, setIsSubmittingTicket] = useState(false);
|
||
const [replyMessage, setReplyMessage] = useState("");
|
||
const [isSendingReply, setIsSendingReply] = useState(false);
|
||
|
||
const fetchTickets = React.useCallback(async () => {
|
||
try {
|
||
setIsLoadingTickets(true);
|
||
const data = await ticketService.getMyTickets();
|
||
setTickets(data);
|
||
} catch (err: unknown) {
|
||
if (!(err as Record<string, boolean>)?._toastShown) {
|
||
toast.error("خطا در دریافت لیست تیکتها");
|
||
}
|
||
} finally {
|
||
setIsLoadingTickets(false);
|
||
}
|
||
}, []);
|
||
|
||
React.useEffect(() => {
|
||
if (activeTab === "tickets") {
|
||
fetchTickets();
|
||
}
|
||
}, [activeTab, fetchTickets]);
|
||
|
||
const handleCreateTicket = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
if (!newTicketSubject.trim() || !newTicketMessage.trim()) {
|
||
toast.error("لطفاً موضوع و متن پیام را وارد کنید.");
|
||
return;
|
||
}
|
||
try {
|
||
setIsSubmittingTicket(true);
|
||
await ticketService.createTicket({
|
||
subject: newTicketSubject.trim(),
|
||
message: newTicketMessage.trim(),
|
||
category: newTicketCategory,
|
||
priority: newTicketPriority,
|
||
petId: newTicketPetId || undefined,
|
||
});
|
||
toast.success("تیکت شما با موفقیت ثبت شد!");
|
||
setIsNewTicketModalOpen(false);
|
||
setNewTicketSubject("");
|
||
setNewTicketMessage("");
|
||
setNewTicketPetId("");
|
||
fetchTickets();
|
||
} catch (err: unknown) {
|
||
if (!(err as Record<string, boolean>)?._toastShown) {
|
||
const gErr = err as { response?: { data?: { message?: string } }; message?: string };
|
||
toast.error(gErr.response?.data?.message || gErr.message || "خطا در ثبت تیکت");
|
||
}
|
||
} finally {
|
||
setIsSubmittingTicket(false);
|
||
}
|
||
};
|
||
|
||
const handleSendReply = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
if (!selectedTicket || !replyMessage.trim()) return;
|
||
try {
|
||
setIsSendingReply(true);
|
||
const newMsg = await ticketService.replyTicket(selectedTicket.id, replyMessage.trim());
|
||
setSelectedTicket((prev) =>
|
||
prev ? { ...prev, messages: [...prev.messages, newMsg] } : null,
|
||
);
|
||
setReplyMessage("");
|
||
toast.success("پاسخ شما با موفقیت ارسال شد");
|
||
fetchTickets();
|
||
} catch (err: unknown) {
|
||
if (!(err as Record<string, boolean>)?._toastShown) {
|
||
const gErr = err as { response?: { data?: { message?: string } }; message?: string };
|
||
toast.error(gErr.response?.data?.message || gErr.message || "خطا در ارسال پاسخ");
|
||
}
|
||
} finally {
|
||
setIsSendingReply(false);
|
||
}
|
||
};
|
||
|
||
// Fetch fresh profile data (which includes orders) whenever orders tab is opened
|
||
React.useEffect(() => {
|
||
if (activeTab === "orders") {
|
||
let isSubscribed = true;
|
||
Promise.resolve().then(() => {
|
||
if (isSubscribed) setIsLoadingOrders(true);
|
||
});
|
||
useUserStore.getState().fetchProfile().finally(() => {
|
||
if (isSubscribed) setIsLoadingOrders(false);
|
||
});
|
||
return () => {
|
||
isSubscribed = false;
|
||
};
|
||
}
|
||
}, [activeTab]);
|
||
const [selectedOrder, setSelectedOrder] = useState<Record<string, unknown> | null>(null);
|
||
|
||
// Address State
|
||
const [isAddressModalOpen, setIsAddressModalOpen] = useState(false);
|
||
const [editingAddress, setEditingAddress] = useState<Address | null>(null);
|
||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||
const [addressToDelete, setAddressToDelete] = useState<Address | null>(null);
|
||
|
||
// Wallet State
|
||
const [isTopUpModalOpen, setIsTopUpModalOpen] = useState(false);
|
||
const [isWithdrawModalOpen, setIsWithdrawModalOpen] = useState(false);
|
||
const [withdrawAmount, setWithdrawAmount] = useState("");
|
||
const [withdrawIban, setWithdrawIban] = useState("");
|
||
const [isSubmittingWithdrawal, setIsSubmittingWithdrawal] = useState(false);
|
||
|
||
// Pet Delete State
|
||
const [petToDelete, setPetToDelete] = useState<{ id: string; name: string } | null>(null);
|
||
|
||
// Profile State
|
||
const [isEditing, setIsEditing] = useState(false);
|
||
const [isSavingProfile, setIsSavingProfile] = useState(false);
|
||
const [profileForm, setProfileForm] = useState(() => ({
|
||
firstName: profile.firstName || "",
|
||
lastName: profile.lastName || "",
|
||
email: profile.email || "",
|
||
mobile: profile.mobile || ""
|
||
}));
|
||
|
||
// Sync profile data on change
|
||
React.useEffect(() => {
|
||
let isSubscribed = true;
|
||
Promise.resolve().then(() => {
|
||
if (isSubscribed) {
|
||
setProfileForm({
|
||
firstName: profile.firstName || "",
|
||
lastName: profile.lastName || "",
|
||
email: profile.email || "",
|
||
mobile: profile.mobile || ""
|
||
});
|
||
}
|
||
});
|
||
return () => {
|
||
isSubscribed = false;
|
||
};
|
||
}, [profile]);
|
||
|
||
if (!isLoggedIn) {
|
||
return null;
|
||
}
|
||
|
||
const handleSaveProfile = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
setIsSavingProfile(true);
|
||
try {
|
||
const cleanPhone = toEnglishDigits(profileForm.mobile);
|
||
await updateProfile({
|
||
firstName: profileForm.firstName,
|
||
lastName: profileForm.lastName,
|
||
email: profileForm.email,
|
||
mobile: cleanPhone
|
||
});
|
||
setIsEditing(false);
|
||
toast.success("اطلاعات کاربری با موفقیت ویرایش شد");
|
||
} catch {
|
||
toast.error("خطا در ویرایش اطلاعات");
|
||
} finally {
|
||
setIsSavingProfile(false);
|
||
}
|
||
};
|
||
|
||
// Password Management State
|
||
const [currentPassword, setCurrentPassword] = useState("");
|
||
const [newPassword, setNewPassword] = useState("");
|
||
const [confirmPassword, setConfirmPassword] = useState("");
|
||
const [showCurrentPass, setShowCurrentPass] = useState(false);
|
||
const [showNewPass, setShowNewPass] = useState(false);
|
||
const [showConfirmPass, setShowConfirmPass] = useState(false);
|
||
const [isSavingPassword, setIsSavingPassword] = useState(false);
|
||
|
||
const handleSavePassword = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
if (!newPassword || newPassword.length < 6) {
|
||
toast.error("رمز عبور جدید باید حداقل ۶ کاراکتر باشد");
|
||
return;
|
||
}
|
||
if (newPassword !== confirmPassword) {
|
||
toast.error("رمز عبور جدید و تکرار آن یکسان نیستند");
|
||
return;
|
||
}
|
||
if (profile.hasPassword && !currentPassword) {
|
||
toast.error("لطفاً رمز عبور فعلی خود را وارد کنید");
|
||
return;
|
||
}
|
||
|
||
setIsSavingPassword(true);
|
||
try {
|
||
await updateProfile({
|
||
password: newPassword.trim(),
|
||
currentPassword: profile.hasPassword ? currentPassword.trim() : undefined,
|
||
});
|
||
toast.success(profile.hasPassword ? "رمز عبور با موفقیت تغییر کرد" : "رمز عبور با موفقیت برای حساب شما ثبت شد");
|
||
setCurrentPassword("");
|
||
setNewPassword("");
|
||
setConfirmPassword("");
|
||
} catch (err: unknown) {
|
||
const errObj = err as { message?: string };
|
||
toast.error(errObj.message || "خطا در ثبت رمز عبور");
|
||
} finally {
|
||
setIsSavingPassword(false);
|
||
}
|
||
};
|
||
|
||
const handleLogout = () => {
|
||
logout();
|
||
router.push('/');
|
||
};
|
||
|
||
const handleSaveAddress = async (addr: Address) => {
|
||
try {
|
||
if (editingAddress) {
|
||
await updateAddress(addr.id, addr);
|
||
} else {
|
||
await addAddress(addr);
|
||
}
|
||
} catch {
|
||
toast.error("خطا در ثبت آدرس");
|
||
}
|
||
setEditingAddress(null);
|
||
};
|
||
|
||
const handleDeleteClick = (addr: Address) => {
|
||
setAddressToDelete(addr);
|
||
setIsDeleteModalOpen(true);
|
||
};
|
||
|
||
const handleConfirmDelete = async () => {
|
||
if (addressToDelete) {
|
||
try {
|
||
await deleteAddress(addressToDelete.id);
|
||
toast.success("آدرس با موفقیت حذف شد");
|
||
} catch {
|
||
toast.error("خطا در حذف آدرس");
|
||
}
|
||
setAddressToDelete(null);
|
||
}
|
||
};
|
||
|
||
const handleEditClick = (addr: Address) => {
|
||
setEditingAddress(addr);
|
||
setIsAddressModalOpen(true);
|
||
};
|
||
|
||
const handleAddClick = () => {
|
||
setEditingAddress(null);
|
||
setIsAddressModalOpen(true);
|
||
};
|
||
|
||
const handleWithdrawSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
const numAmount = parseInt(toEnglishDigits(withdrawAmount));
|
||
if (!numAmount || numAmount < 50000) {
|
||
toast.error("حداقل مبلغ قابل برداشت ۵۰,۰۰۰ تومان است.");
|
||
return;
|
||
}
|
||
if (numAmount > (profile.walletBalance || 0)) {
|
||
toast.error("مبلغ درخواستی از موجودی کیف پول شما بیشتر است.");
|
||
return;
|
||
}
|
||
if (!withdrawIban.trim() || withdrawIban.trim().length < 10) {
|
||
toast.error("لطفاً شماره شبا یا شماره کارت معتبر وارد نمایید.");
|
||
return;
|
||
}
|
||
try {
|
||
setIsSubmittingWithdrawal(true);
|
||
await ticketService.createTicket({
|
||
subject: `درخواست تسویه و برداشت وجه (${toPersian(numAmount.toLocaleString())} تومان)`,
|
||
message: `درخواست برداشت وجه از کیف پول کاربری:\nمبلغ: ${numAmount.toLocaleString('fa-IR')} تومان\nشماره شبا/کارت: ${withdrawIban}\nشماره همراه کاربر: ${profile.mobile || ''}`,
|
||
category: 'FINANCIAL',
|
||
priority: 'HIGH'
|
||
});
|
||
toast.success(`درخواست برداشت ${toPersian(numAmount.toLocaleString())} تومان با موفقیت ثبت شد و پس از بررسی واحد مالی واریز خواهد شد.`);
|
||
setIsWithdrawModalOpen(false);
|
||
setWithdrawAmount("");
|
||
setWithdrawIban("");
|
||
fetchTickets();
|
||
} catch {
|
||
toast.error("خطا در ثبت درخواست برداشت. لطفاً با پشتیبانی تماس بگیرید.");
|
||
} finally {
|
||
setIsSubmittingWithdrawal(false);
|
||
}
|
||
};
|
||
|
||
const tabs = [
|
||
{ id: "profile", label: "اطلاعات فردی", icon: <UserCircle className="w-5 h-5" /> },
|
||
{ id: "pets", label: "پتهای من", icon: <Dog className="w-5 h-5" /> },
|
||
{ id: "orders", label: "سفارشات من", icon: <Package className="w-5 h-5" /> },
|
||
{ id: "prescriptions", label: "نسخههای من", icon: <Stethoscope className="w-5 h-5" /> },
|
||
{ id: "wallet", label: "کیف پول", icon: <Wallet className="w-5 h-5" /> },
|
||
{ id: "addresses", label: "آدرسها", icon: <MapPin className="w-5 h-5" /> },
|
||
{ id: "tickets", label: "پشتیبانی و مشاوره", icon: <MessageSquare className="w-5 h-5" /> },
|
||
];
|
||
|
||
return (
|
||
<div className="min-h-screen bg-medical-gray-50 pt-6 sm:pt-12 pb-24 px-3 sm:px-4 font-vazir overflow-x-hidden" dir="rtl">
|
||
<div className="max-w-6xl mx-auto w-full">
|
||
{/* Breadcrumb */}
|
||
<div className="flex items-center gap-2 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest mb-6 sm:mb-10 overflow-x-auto whitespace-nowrap pb-2 no-scrollbar">
|
||
<span className="cursor-pointer hover:text-canina-blue" onClick={() => router.push('/')}>خانه</span>
|
||
<ChevronRight className="w-2.5 h-2.5 flex-shrink-0" />
|
||
<span className="text-canina-blue">پنل کاربری</span>
|
||
</div>
|
||
|
||
<div className="grid lg:grid-cols-12 gap-6 sm:gap-8">
|
||
{/* Top User Info & Navigation Tabs */}
|
||
<div className="lg:col-span-12">
|
||
<div className="bg-white rounded-[1.5rem] sm:rounded-[3rem] border border-medical-gray-200 p-4 sm:p-6 shadow-xl flex flex-col md:flex-row items-stretch md:items-center justify-between gap-4">
|
||
<div className="flex items-center gap-3 sm:gap-4">
|
||
<div className="w-12 h-12 sm:w-14 sm:h-14 bg-canina-blue rounded-xl sm:rounded-2xl flex items-center justify-center text-white shadow-lg flex-shrink-0">
|
||
<User className="w-6 h-6 sm:w-7 sm:h-7" />
|
||
</div>
|
||
<div className="min-w-0 flex-1">
|
||
<h2 className="text-base sm:text-lg font-black text-medical-gray-900 truncate">{profile.firstName} {profile.lastName}</h2>
|
||
<p className="text-[11px] sm:text-xs font-bold text-medical-gray-400 truncate">{profile.email}</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Tab Navigation */}
|
||
<div className="grid grid-cols-2 sm:flex sm:flex-wrap items-center gap-2 pt-2 md:pt-0 border-t md:border-t-0 border-medical-gray-100">
|
||
{tabs.map(tab => (
|
||
<button
|
||
key={tab.id}
|
||
onClick={() => setActiveTab(tab.id as any)}
|
||
className={`flex items-center justify-center gap-2 px-3 sm:px-4 py-2.5 sm:py-3 rounded-xl sm:rounded-2xl transition-all font-bold text-xs sm:text-sm cursor-pointer ${activeTab === tab.id ? "bg-canina-blue text-white shadow-lg shadow-canina-blue/20" : "text-medical-gray-600 hover:bg-medical-gray-100 bg-medical-gray-50"}`}
|
||
>
|
||
<div className="flex-shrink-0">{tab.icon}</div>
|
||
<span>{tab.label}</span>
|
||
</button>
|
||
))}
|
||
<button
|
||
onClick={handleLogout}
|
||
className="flex items-center justify-center gap-2 px-3 sm:px-4 py-2.5 sm:py-3 rounded-xl sm:rounded-2xl text-red-500 hover:bg-red-50 transition-all font-bold text-xs sm:text-sm bg-red-50/40 col-span-2 sm:col-span-1"
|
||
>
|
||
<LogOut className="w-4 h-4 sm:w-5 sm:h-5 flex-shrink-0" />
|
||
<span>خروج</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Main Content Area */}
|
||
<div className="lg:col-span-8 min-w-0 order-1 lg:order-1">
|
||
<motion.div
|
||
key={activeTab}
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
className="bg-white rounded-[1.5rem] sm:rounded-[3.5rem] border border-medical-gray-200 p-4 sm:p-10 min-h-[400px] sm:min-h-[600px] shadow-sm overflow-hidden"
|
||
>
|
||
{activeTab === "profile" && (
|
||
<div>
|
||
<h3 className="text-2xl font-black text-medical-gray-900 mb-8 italic">اطلاعات فردی</h3>
|
||
<form onSubmit={handleSaveProfile} className="space-y-8">
|
||
<div className="grid md:grid-cols-2 gap-8">
|
||
<div className="space-y-2">
|
||
<label htmlFor="firstName" className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-4">نام</label>
|
||
<input
|
||
autoFocus={isEditing}
|
||
type="text"
|
||
id="firstName"
|
||
name="firstName"
|
||
autoComplete="given-name"
|
||
required
|
||
readOnly={!isEditing}
|
||
value={isEditing ? profileForm.firstName : profile.firstName}
|
||
onChange={e => setProfileForm({ ...profileForm, firstName: e.target.value })}
|
||
className={cn(
|
||
"w-full border p-4 rounded-2xl font-bold outline-none transition-all",
|
||
isEditing
|
||
? "bg-white border-canina-blue/30 focus:ring-2 focus:ring-canina-blue/20"
|
||
: "bg-medical-gray-50 border-medical-gray-100"
|
||
)}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<label htmlFor="lastName" className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-4">نام خانوادگی</label>
|
||
<input
|
||
type="text"
|
||
id="lastName"
|
||
name="lastName"
|
||
autoComplete="family-name"
|
||
required
|
||
readOnly={!isEditing}
|
||
value={isEditing ? profileForm.lastName : profile.lastName}
|
||
onChange={e => setProfileForm({ ...profileForm, lastName: e.target.value })}
|
||
className={cn(
|
||
"w-full border p-4 rounded-2xl font-bold outline-none transition-all",
|
||
isEditing
|
||
? "bg-white border-canina-blue/30 focus:ring-2 focus:ring-canina-blue/20"
|
||
: "bg-medical-gray-50 border-medical-gray-100"
|
||
)}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<label htmlFor="email" className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-4">ایمیل</label>
|
||
<input
|
||
type="email"
|
||
id="email"
|
||
name="email"
|
||
autoComplete="email"
|
||
required
|
||
readOnly={!isEditing}
|
||
value={isEditing ? profileForm.email : profile.email}
|
||
onChange={e => setProfileForm({ ...profileForm, email: e.target.value })}
|
||
className={cn(
|
||
"w-full border p-4 rounded-2xl font-bold outline-none transition-all text-left",
|
||
isEditing
|
||
? "bg-white border-canina-blue/30 focus:ring-2 focus:ring-canina-blue/20"
|
||
: "bg-medical-gray-50 border-medical-gray-100"
|
||
)}
|
||
dir="ltr"
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<label htmlFor="mobile" className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-4">شماره موبایل</label>
|
||
<input
|
||
type="tel"
|
||
id="mobile"
|
||
name="mobile"
|
||
autoComplete="tel"
|
||
required
|
||
readOnly={!isEditing}
|
||
disabled={!isEditing}
|
||
value={isEditing ? profileForm.mobile : toPersian(profile.mobile || "")}
|
||
onChange={e => setProfileForm({ ...profileForm, mobile: e.target.value.replace(/[^0-9]/g, '') })}
|
||
className={cn(
|
||
"w-full border p-4 rounded-2xl font-bold outline-none transition-all text-left",
|
||
isEditing
|
||
? "bg-white border-canina-blue/30 focus:ring-2 focus:ring-canina-blue/20"
|
||
: "bg-medical-gray-50 border-medical-gray-100 opacity-75 cursor-not-allowed"
|
||
)}
|
||
dir="ltr"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex gap-4 mt-8">
|
||
{isEditing ? (
|
||
<>
|
||
<button
|
||
type="submit"
|
||
disabled={isSavingProfile}
|
||
className="bg-canina-blue text-white px-10 py-4 rounded-2xl font-black hover:bg-indigo-700 transition-all flex items-center gap-2 disabled:opacity-50"
|
||
>
|
||
{isSavingProfile && <span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />}
|
||
ذخیره تغییرات
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setProfileForm({
|
||
firstName: profile.firstName || "",
|
||
lastName: profile.lastName || "",
|
||
email: profile.email || "",
|
||
mobile: profile.mobile || ""
|
||
});
|
||
setIsEditing(false);
|
||
}}
|
||
className="bg-medical-gray-50 text-medical-gray-500 border border-medical-gray-200 px-10 py-4 rounded-2xl font-black hover:bg-medical-gray-100 transition-all"
|
||
>
|
||
انصراف
|
||
</button>
|
||
</>
|
||
) : (
|
||
<button
|
||
type="button"
|
||
onClick={() => setIsEditing(true)}
|
||
className="bg-medical-gray-900 text-white px-10 py-4 rounded-2xl font-black hover:bg-canina-blue transition-all"
|
||
>
|
||
ویرایش اطلاعات
|
||
</button>
|
||
)}
|
||
</div>
|
||
</form>
|
||
|
||
{/* Password & Security Section */}
|
||
<div className="mt-12 pt-8 border-t border-medical-gray-100">
|
||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-6">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-10 h-10 rounded-2xl bg-canina-blue/10 text-canina-blue flex items-center justify-center">
|
||
<Lock className="w-5 h-5" />
|
||
</div>
|
||
<div>
|
||
<h4 className="text-lg font-black text-medical-gray-900 italic">
|
||
{profile.hasPassword ? "تغییر رمز عبور حساب" : "تعریف رمز عبور برای حساب کاربری"}
|
||
</h4>
|
||
<p className="text-xs font-medium text-medical-gray-400">
|
||
{profile.hasPassword
|
||
? "برای افزایش امنیت میتوانید رمز عبور ورود خود را تغییر دهید."
|
||
: "با تعیین رمز عبور میتوانید در دفعات بعدی علاوه بر پیامک، با رمز عبور نیز وارد شوید."}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<span
|
||
className={cn(
|
||
"px-3 py-1 rounded-full text-[11px] font-bold self-start sm:self-auto",
|
||
profile.hasPassword
|
||
? "bg-green-50 text-green-700 border border-green-200"
|
||
: "bg-amber-50 text-amber-700 border border-amber-200"
|
||
)}
|
||
>
|
||
{profile.hasPassword ? "رمز عبور فعال است" : "ورود فقط با OTP"}
|
||
</span>
|
||
</div>
|
||
|
||
<form onSubmit={handleSavePassword} className="bg-medical-gray-50/50 p-6 sm:p-8 rounded-[2rem] border border-medical-gray-100 space-y-4">
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||
{profile.hasPassword && (
|
||
<div className="space-y-1.5">
|
||
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-2">
|
||
رمز عبور فعلی *
|
||
</label>
|
||
<div className="relative">
|
||
<input
|
||
type={showCurrentPass ? "text" : "password"}
|
||
autoComplete="current-password"
|
||
required
|
||
placeholder="رمز فعلی..."
|
||
value={currentPassword}
|
||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||
className="w-full bg-white border border-medical-gray-200 rounded-xl py-3 pr-3 pl-10 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-mono"
|
||
dir="ltr"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowCurrentPass(!showCurrentPass)}
|
||
className="absolute left-2.5 top-1/2 -translate-y-1/2 p-1 text-medical-gray-400 hover:text-medical-gray-700 transition-colors cursor-pointer"
|
||
tabIndex={-1}
|
||
>
|
||
{showCurrentPass ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="space-y-1.5">
|
||
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-2">
|
||
{profile.hasPassword ? "رمز عبور جدید *" : "رمز عبور دلخواه *"}
|
||
</label>
|
||
<div className="relative">
|
||
<input
|
||
type={showNewPass ? "text" : "password"}
|
||
autoComplete="new-password"
|
||
required
|
||
placeholder="حداقل ۶ کاراکتر"
|
||
value={newPassword}
|
||
onChange={(e) => setNewPassword(e.target.value)}
|
||
className="w-full bg-white border border-medical-gray-200 rounded-xl py-3 pr-3 pl-10 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-mono"
|
||
dir="ltr"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowNewPass(!showNewPass)}
|
||
className="absolute left-2.5 top-1/2 -translate-y-1/2 p-1 text-medical-gray-400 hover:text-medical-gray-700 transition-colors cursor-pointer"
|
||
tabIndex={-1}
|
||
>
|
||
{showNewPass ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-1.5">
|
||
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-2">
|
||
تکرار رمز عبور *
|
||
</label>
|
||
<div className="relative">
|
||
<input
|
||
type={showConfirmPass ? "text" : "password"}
|
||
autoComplete="new-password"
|
||
required
|
||
placeholder="تکرار رمز عبور"
|
||
value={confirmPassword}
|
||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||
className="w-full bg-white border border-medical-gray-200 rounded-xl py-3 pr-3 pl-10 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-mono"
|
||
dir="ltr"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowConfirmPass(!showConfirmPass)}
|
||
className="absolute left-2.5 top-1/2 -translate-y-1/2 p-1 text-medical-gray-400 hover:text-medical-gray-700 transition-colors cursor-pointer"
|
||
tabIndex={-1}
|
||
>
|
||
{showConfirmPass ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex justify-end pt-3">
|
||
<button
|
||
type="submit"
|
||
disabled={isSavingPassword || newPassword.length < 6 || !confirmPassword}
|
||
className="px-8 py-3.5 bg-canina-blue hover:bg-indigo-700 text-white rounded-xl font-black text-xs transition-all shadow-md shadow-canina-blue/15 flex items-center gap-2 cursor-pointer disabled:opacity-50"
|
||
>
|
||
{isSavingPassword ? (
|
||
<span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||
) : (
|
||
<ShieldCheck className="w-4 h-4" />
|
||
)}
|
||
{profile.hasPassword ? "بروزرسانی رمز عبور" : "ثبت رمز عبور"}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === "pets" && (
|
||
<div className="w-full">
|
||
<PetProfile embedded={true} initialView={pets.length > 0 ? "index" : "add"} />
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === "orders" && (
|
||
<div>
|
||
<div className="flex items-center justify-between mb-8">
|
||
<h3 className="text-2xl font-black text-medical-gray-900 italic">تاریخچه سفارشات</h3>
|
||
<div className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest border border-medical-gray-100 px-4 py-2 rounded-full bg-medical-gray-50">
|
||
تعداد کل: {toPersian((orders || []).length.toString())} مورد
|
||
</div>
|
||
</div>
|
||
{(orders || []).length === 0 ? (
|
||
<div className="text-center py-20">
|
||
<ShoppingBag className="w-16 h-16 text-medical-gray-200 mx-auto mb-4" />
|
||
<p className="text-medical-gray-400 font-bold">هنوز سفارشی ثبت نکردهاید.</p>
|
||
<button onClick={() => router.push('/shop')} className="mt-6 text-canina-blue font-black underline underline-offset-8">برو به فروشگاه</button>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-4">
|
||
{isLoadingOrders ? (
|
||
[1, 2, 3].map(i => <OrderRowSkeleton key={i} />)
|
||
) : (
|
||
(orders || []).map(order => (
|
||
<motion.div
|
||
key={order.id}
|
||
whileHover={{ scale: 1.01, x: -5 }}
|
||
onClick={() => setSelectedOrder(order as unknown as Record<string, unknown>)}
|
||
className="p-6 border border-medical-gray-100 rounded-[2.5rem] hover:border-canina-blue transition-all group cursor-pointer bg-white overflow-hidden relative"
|
||
>
|
||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||
<div className="flex items-center gap-4">
|
||
<div className="w-12 h-12 bg-medical-gray-50 rounded-2xl flex items-center justify-center text-canina-blue group-hover:bg-canina-blue group-hover:text-white transition-all shadow-sm">
|
||
<Package className="w-6 h-6" />
|
||
</div>
|
||
<div>
|
||
<div className="text-sm font-black text-medical-gray-900 group-hover:text-canina-blue transition-colors italic">سفارش {toPersian(order.trackingNumber || order.id?.substring(0, 8))}</div>
|
||
<div className="text-[10px] font-bold text-medical-gray-400 flex items-center gap-1">
|
||
<Calendar className="w-3 h-3 opacity-30" />
|
||
{toPersian(new Date(order.date || (order as unknown as Record<string, string>).createdAt || 0).toLocaleDateString("fa-IR"))} - ساعت {toPersian(new Date(order.date || (order as unknown as Record<string, string>).createdAt || 0).toLocaleTimeString("fa-IR", { hour: '2-digit', minute: '2-digit' }))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="text-left">
|
||
<div className="text-lg font-black text-medical-gray-900 italic">{toPersian(order.total.toLocaleString())} <span className="text-xs">تومان</span></div>
|
||
<div className="flex flex-wrap items-center gap-2 justify-end mt-1.5">
|
||
{order.isRefill && (
|
||
<span className="text-[9px] font-black px-2 py-0.5 rounded-full bg-emerald-50 text-emerald-600 border border-emerald-100">
|
||
تمدید خودکار ۵٪-
|
||
</span>
|
||
)}
|
||
{Number(order.charityDonation) > 0 && (
|
||
<span className="text-[9px] font-black px-2 py-0.5 rounded-full bg-rose-50 text-rose-600 border border-rose-100 flex items-center gap-1">
|
||
<Heart className="w-2.5 h-2.5 fill-rose-500 text-rose-500" />
|
||
مهربانی: {toPersian(Number(order.charityDonation).toLocaleString())} ت
|
||
</span>
|
||
)}
|
||
{(() => {
|
||
const st = order.status;
|
||
let badgeLabel = 'در حال پردازش';
|
||
let badgeColor = 'bg-amber-50 text-amber-700 border-amber-200';
|
||
if (st === 'delivered') {
|
||
badgeLabel = 'تحویل شده';
|
||
badgeColor = 'bg-green-50 text-green-700 border-green-200';
|
||
} else if (st === 'shipped') {
|
||
badgeLabel = 'ارسال شده';
|
||
badgeColor = 'bg-blue-50 text-blue-700 border-blue-200';
|
||
} else if (st === 'packaged' || st === 'ready_to_ship') {
|
||
badgeLabel = 'بستهبندی شده (آماده ارسال)';
|
||
badgeColor = 'bg-purple-50 text-purple-700 border-purple-200';
|
||
} else if (st === 'pending_payment') {
|
||
badgeLabel = 'در انتظار پرداخت / ناموفق';
|
||
badgeColor = 'bg-rose-50 text-rose-700 border-rose-200';
|
||
} else if (st === 'cancelled') {
|
||
badgeLabel = 'لغو شده';
|
||
badgeColor = 'bg-red-50 text-red-700 border-red-200';
|
||
} else if (st === 'refunded') {
|
||
badgeLabel = 'مرجوع شده';
|
||
badgeColor = 'bg-gray-100 text-gray-700 border-gray-200';
|
||
}
|
||
return (
|
||
<span className={`text-[9px] font-black px-2.5 py-0.5 rounded-full border uppercase tracking-tighter ${badgeColor}`}>
|
||
{badgeLabel}
|
||
</span>
|
||
);
|
||
})()}
|
||
<ChevronRight className="w-4 h-4 text-medical-gray-300 -rotate-180" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
))
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === "addresses" && (
|
||
<div>
|
||
<div className="flex items-center justify-between mb-8">
|
||
<h3 className="text-2xl font-black text-medical-gray-900 italic">آدرسهای من</h3>
|
||
<button
|
||
onClick={handleAddClick}
|
||
className="px-6 py-3 bg-canina-blue text-white rounded-2xl font-black text-xs flex items-center gap-2 hover:bg-medical-gray-900 transition-all shadow-lg shadow-canina-blue/10"
|
||
>
|
||
<MapPin className="w-4 h-4" />
|
||
افزودن آدرس جدید
|
||
</button>
|
||
</div>
|
||
|
||
<div className="space-y-6">
|
||
{(profile?.addresses || []).length === 0 ? (
|
||
<div className="text-center py-20 border-2 border-dashed border-medical-gray-100 rounded-[3rem]">
|
||
<MapPin className="w-16 h-16 text-medical-gray-200 mx-auto mb-4" />
|
||
<p className="text-medical-gray-400 font-bold">هنوز آدرسی ثبت نکردهاید.</p>
|
||
</div>
|
||
) : (
|
||
(profile?.addresses || []).map((addr) => (
|
||
<motion.div
|
||
key={addr.id}
|
||
initial={{ opacity: 0, y: 10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
className={cn(
|
||
"p-8 bg-white border rounded-[2.5rem] relative group transition-all",
|
||
addr.isDefault ? "border-canina-blue shadow-xl shadow-canina-blue/5" : "border-medical-gray-100 hover:border-medical-gray-200"
|
||
)}
|
||
>
|
||
<div className="flex justify-between items-start mb-6">
|
||
<div className="flex items-center gap-4">
|
||
<div className={cn("w-12 h-12 rounded-2xl flex items-center justify-center transition-all", addr.isDefault ? "bg-canina-blue text-white shadow-lg" : "bg-medical-gray-50 text-canina-blue")}>
|
||
<MapPin className="w-6 h-6" />
|
||
</div>
|
||
<div>
|
||
<div className="flex items-center gap-3">
|
||
<h4 className="text-xl font-black text-medical-gray-900 italic">{addr.title}</h4>
|
||
{addr.isDefault && (
|
||
<span className="px-3 py-1 bg-green-50 text-green-600 text-[9px] font-black rounded-full uppercase tracking-widest">پیشفرض</span>
|
||
)}
|
||
</div>
|
||
<p className="text-xs font-bold text-medical-gray-400">تحویل گیرنده: {addr.receptorName}</p>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
onClick={() => handleEditClick(addr)}
|
||
className="w-10 h-10 flex items-center justify-center rounded-xl bg-medical-gray-50 text-medical-gray-400 hover:bg-canina-blue hover:text-white transition-all shadow-sm"
|
||
>
|
||
<Edit2 className="w-5 h-5" />
|
||
</button>
|
||
<button
|
||
onClick={() => handleDeleteClick(addr)}
|
||
className="w-10 h-10 flex items-center justify-center rounded-xl bg-medical-gray-50 text-medical-gray-400 hover:bg-red-500 hover:text-white transition-all shadow-sm"
|
||
>
|
||
<Trash2 className="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-medical-gray-50/50 rounded-2xl p-6 mb-6">
|
||
<p className="text-sm font-black text-medical-gray-700 leading-relaxed text-right">{toPersian(addr.province)}، {toPersian(addr.city)}، {toPersian(addr.detail)}</p>
|
||
</div>
|
||
|
||
<div className="flex flex-wrap items-center gap-6">
|
||
<div className="flex items-center gap-2">
|
||
<Phone className="w-4 h-4 text-canina-blue/40" />
|
||
<span className="text-xs font-bold text-medical-gray-500" dir="ltr">{toPersian(addr.phone)}</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Hash className="w-4 h-4 text-canina-blue/40" />
|
||
<span className="text-xs font-bold text-medical-gray-500" dir="ltr">{toPersian(addr.zipCode)}</span>
|
||
</div>
|
||
{!addr.isDefault && (
|
||
<button
|
||
onClick={async () => {
|
||
try {
|
||
await setDefaultAddress(addr.id);
|
||
toast.success("آدرس پیشفرض با موفقیت تغییر کرد");
|
||
} catch {
|
||
toast.error("خطا در تغییر آدرس پیشفرض");
|
||
}
|
||
}}
|
||
className="mr-auto text-[10px] font-black text-canina-blue hover:text-medical-gray-900 uppercase tracking-widest whitespace-nowrap"
|
||
>
|
||
انتخاب به عنوان پیشفرض
|
||
</button>
|
||
)}
|
||
{addr.isDefault && (
|
||
<div className="mr-auto flex items-center gap-2 text-green-600 text-[10px] font-black italic">
|
||
<CheckCircle2 className="w-4 h-4" />
|
||
آدرس اصلی ارسال
|
||
</div>
|
||
)}
|
||
</div>
|
||
</motion.div>
|
||
))
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === "wallet" && (
|
||
<div className="space-y-10">
|
||
<div className="flex items-center justify-between">
|
||
<h3 className="text-2xl font-black text-medical-gray-900 italic">مدیریت کیف پول</h3>
|
||
<div className="text-[10px] font-black text-green-600 bg-green-50 px-4 py-2 rounded-full uppercase tracking-widest border border-green-100 flex items-center gap-2">
|
||
<CheckCircle className="w-3 h-3" />
|
||
حساب تایید شده
|
||
</div>
|
||
</div>
|
||
|
||
{/* Main Balance Card */}
|
||
<div className="bg-gradient-to-br from-canina-blue via-blue-600 to-indigo-700 rounded-[2rem] sm:rounded-[3rem] p-6 sm:p-10 text-white shadow-2xl shadow-canina-blue/20 relative overflow-hidden group">
|
||
<div className="absolute top-0 right-0 p-6 sm:p-10 opacity-10 group-hover:scale-110 transition-transform duration-700 pointer-events-none">
|
||
<Wallet className="w-32 h-32 sm:w-44 sm:h-44" />
|
||
</div>
|
||
<div className="relative z-10 flex flex-col lg:flex-row items-center justify-between gap-6 sm:gap-8">
|
||
<div className="text-center lg:text-right w-full lg:w-auto">
|
||
<div className="text-xs font-black uppercase tracking-[0.2em] text-white/80 mb-2 sm:mb-3 flex items-center gap-2 justify-center lg:justify-start">
|
||
<div className="w-2 h-2 bg-emerald-400 rounded-full animate-pulse shadow-sm shadow-emerald-400/50" />
|
||
موجودی زنده و قابل استفاده
|
||
</div>
|
||
<div className="flex items-baseline gap-2 flex-row-reverse justify-end">
|
||
<div className="text-3xl sm:text-4xl lg:text-5xl font-black italic tracking-tight whitespace-nowrap">
|
||
{toPersian((profile.walletBalance || 0).toLocaleString())}
|
||
</div>
|
||
<span className="text-sm sm:text-base font-bold opacity-90 decoration-white/30 underline underline-offset-8">
|
||
تومان
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div className="flex flex-wrap sm:flex-nowrap gap-3 w-full lg:w-auto shrink-0 justify-center">
|
||
{isWithdrawalEnabled && (
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
if ((profile.walletBalance || 0) < 50000) {
|
||
toast.error("حداقل موجودی برای ثبت درخواست برداشت ۵۰,۰۰۰ تومان است.");
|
||
return;
|
||
}
|
||
setWithdrawAmount(String(profile.walletBalance || 50000));
|
||
setIsWithdrawModalOpen(true);
|
||
}}
|
||
className="flex-1 sm:flex-initial sm:w-36 h-12 sm:h-14 bg-white/10 backdrop-blur-md rounded-2xl font-black text-xs sm:text-sm flex items-center justify-center gap-2 border border-white/20 hover:bg-white/20 transition-all cursor-pointer whitespace-nowrap"
|
||
>
|
||
<ArrowDownCircle className="w-4 h-4 sm:w-5 sm:h-5 shrink-0" />
|
||
برداشت وجه
|
||
</button>
|
||
)}
|
||
<button
|
||
type="button"
|
||
onClick={() => setIsTopUpModalOpen(true)}
|
||
className="flex-1 sm:flex-initial sm:w-36 h-12 sm:h-14 bg-white text-canina-blue rounded-2xl font-black text-xs sm:text-sm flex items-center justify-center gap-2 hover:bg-medical-gray-900 hover:text-white transition-all shadow-xl shadow-black/10 group/topup cursor-pointer whitespace-nowrap"
|
||
>
|
||
<ArrowUpCircle className="w-4 h-4 sm:w-5 sm:h-5 group-hover:-translate-y-1 transition-transform shrink-0" />
|
||
شارژ آنی
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Transaction History */}
|
||
<div className="pt-10 border-t border-medical-gray-100">
|
||
<div className="flex items-center gap-4 mb-8">
|
||
<div className="w-10 h-10 bg-medical-gray-50 rounded-xl flex items-center justify-center text-medical-gray-400">
|
||
<Clock className="w-5 h-5" />
|
||
</div>
|
||
<h4 className="text-lg font-black text-medical-gray-900 italic">تاریخچه تراکنشها</h4>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
{(profile?.transactions || []).length === 0 ? (
|
||
<div className="text-center py-20 bg-medical-gray-50 rounded-[2.5rem] border border-dashed border-medical-gray-200">
|
||
<Info className="w-12 h-12 text-medical-gray-200 mx-auto mb-4" />
|
||
<p className="text-medical-gray-400 font-bold">تراکنشی یافت نشد.</p>
|
||
</div>
|
||
) : (
|
||
(profile?.transactions || []).map((trx) => (
|
||
<div key={trx.id} className="p-4 sm:p-6 bg-white border border-medical-gray-100 rounded-[1.5rem] sm:rounded-[2rem] flex flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-6 hover:shadow-lg hover:border-canina-blue/10 transition-all group">
|
||
<div className="flex items-center gap-3 sm:gap-4">
|
||
<div className={cn(
|
||
"w-10 h-10 sm:w-12 sm:h-12 rounded-xl sm:rounded-2xl flex items-center justify-center transition-colors flex-shrink-0",
|
||
trx.type === 'top_up' ? "bg-green-50 text-green-500 group-hover:bg-green-500 group-hover:text-white" : "bg-red-50 text-red-500 group-hover:bg-red-500 group-hover:text-white"
|
||
)}>
|
||
{trx.type === 'top_up' ? <ArrowUpCircle className="w-5 h-5 sm:w-6 sm:h-6" /> : <ShoppingCart className="w-5 h-5 sm:w-6 sm:h-6" />}
|
||
</div>
|
||
<div className="min-w-0">
|
||
<div className="text-xs sm:text-sm font-black text-medical-gray-900 truncate">{trx.type === 'top_up' ? "افزایش موجودی (شارژ)" : "پرداخت سفارش"}</div>
|
||
<div className="text-[10px] font-bold text-medical-gray-400 flex flex-wrap items-center gap-1 sm:gap-2 mt-0.5">
|
||
<span className="truncate">کد: {trx.id}</span>
|
||
<span className="w-1 h-1 bg-medical-gray-200 rounded-full hidden sm:inline-block" />
|
||
<span>{toPersian(new Date(trx.date).toLocaleDateString("fa-IR"))}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="text-right sm:text-left flex sm:flex-col justify-between items-center sm:items-end border-t sm:border-t-0 pt-2 sm:pt-0 border-medical-gray-50">
|
||
<div className={cn(
|
||
"text-base sm:text-lg font-black italic",
|
||
trx.type === 'top_up' ? "text-green-600" : "text-red-500"
|
||
)}>
|
||
{trx.type === 'top_up' ? "+" : ""}{toPersian(trx.amount.toLocaleString())} <span className="text-xs not-italic">تومان</span>
|
||
</div>
|
||
<div className="text-[9px] font-black uppercase text-green-500 tracking-widest sm:mt-1">
|
||
تایید شده
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === "tickets" && (
|
||
<div>
|
||
<div className="flex items-center justify-between mb-8">
|
||
<div>
|
||
<h3 className="text-2xl font-black text-medical-gray-900 italic">پشتیبانی و مشاوره آنلاین دامپزشک</h3>
|
||
<p className="text-xs text-medical-gray-500 mt-1 font-medium">پاسخگویی تخصصی به سوالات دارویی، مکملها و پیگیری سفارشات توسط تیم پزشکی کنینا</p>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => setIsNewTicketModalOpen(true)}
|
||
className="px-4 py-2.5 bg-canina-blue text-white rounded-xl text-xs font-black flex items-center gap-1.5 hover:bg-canina-dark transition-all shadow-md shadow-canina-blue/20 cursor-pointer"
|
||
>
|
||
<Send className="w-3.5 h-3.5" />
|
||
ثبت تیکت جدید
|
||
</button>
|
||
</div>
|
||
|
||
{isLoadingTickets ? (
|
||
<div className="py-20 text-center text-medical-gray-400 font-bold">در حال بارگذاری تیکتها...</div>
|
||
) : tickets.length === 0 ? (
|
||
<div className="text-center py-20 bg-medical-gray-50 rounded-[2.5rem] border border-dashed border-medical-gray-200">
|
||
<MessageSquare className="w-12 h-12 text-medical-gray-300 mx-auto mb-4" />
|
||
<h4 className="text-base font-black text-medical-gray-800 mb-1">هنوز تیکتی ثبت نکردهاید</h4>
|
||
<p className="text-xs text-medical-gray-400 font-medium max-w-md mx-auto mb-6">
|
||
شما میتوانید سوالات پزشکی و تغذیهای حیوان خانگی خود را از دامپزشکان متخصص کنینا بپرسید یا وضعیت سفارشات خود را پیگیری کنید.
|
||
</p>
|
||
<button
|
||
type="button"
|
||
onClick={() => setIsNewTicketModalOpen(true)}
|
||
className="px-6 py-3 bg-canina-blue text-white rounded-xl text-xs font-black inline-flex items-center gap-2 hover:bg-canina-dark transition-all shadow-md shadow-canina-blue/20 cursor-pointer"
|
||
>
|
||
<Send className="w-4 h-4" />
|
||
ثبت اولین تیکت مشاوره
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-4">
|
||
{tickets.map((t) => {
|
||
const isVet = t.category === 'VET_CONSULTATION';
|
||
return (
|
||
<div
|
||
key={t.id}
|
||
onClick={() => setSelectedTicket(t)}
|
||
className="p-5 sm:p-6 bg-medical-gray-50 hover:bg-white border border-medical-gray-200 hover:border-canina-blue/30 rounded-[1.5rem] sm:rounded-[2rem] flex flex-col md:flex-row items-start md:items-center justify-between gap-4 transition-all hover:shadow-lg hover:shadow-canina-blue/5 cursor-pointer group"
|
||
>
|
||
<div className="flex items-center gap-4">
|
||
<div className={cn(
|
||
"w-12 h-12 rounded-2xl flex items-center justify-center shrink-0 transition-transform group-hover:scale-105",
|
||
isVet ? "bg-emerald-50 text-emerald-600" : "bg-canina-blue/10 text-canina-blue"
|
||
)}>
|
||
{isVet ? <Stethoscope className="w-6 h-6" /> : <FileText className="w-6 h-6" />}
|
||
</div>
|
||
<div>
|
||
<div className="flex items-center gap-2 flex-wrap mb-1">
|
||
<h4 className="font-black text-medical-gray-900 text-sm group-hover:text-canina-blue transition-colors">
|
||
{t.subject}
|
||
</h4>
|
||
{t.pet && (
|
||
<span className="px-2 py-0.5 bg-purple-50 text-purple-700 text-[10px] font-bold rounded-lg border border-purple-100">
|
||
🐾 {t.pet.name} ({t.pet.breed || t.pet.type})
|
||
</span>
|
||
)}
|
||
</div>
|
||
<p className="text-[11px] text-medical-gray-400 font-medium">
|
||
شناسه: {t.ticketNumber} • {t.messages?.length || 1} پیام • {new Date(t.updatedAt).toLocaleDateString('fa-IR')}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-3 self-end md:self-center">
|
||
<span className={cn(
|
||
"px-3 py-1 text-xs font-black rounded-full border",
|
||
t.status === 'ANSWERED' && "bg-emerald-50 text-emerald-700 border-emerald-200",
|
||
t.status === 'OPEN' && "bg-amber-50 text-amber-700 border-amber-200",
|
||
t.status === 'IN_PROGRESS' && "bg-blue-50 text-blue-700 border-blue-200",
|
||
t.status === 'CLOSED' && "bg-gray-100 text-gray-600 border-gray-200",
|
||
)}>
|
||
{t.status === 'ANSWERED' ? 'پاسخ داده شد' :
|
||
t.status === 'OPEN' ? 'در انتظار پاسخ' :
|
||
t.status === 'IN_PROGRESS' ? 'در حال بررسی' : 'بسته شده'}
|
||
</span>
|
||
<ChevronRight className="w-4 h-4 text-medical-gray-300 group-hover:translate-x-[-2px] transition-transform" />
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === "prescriptions" && (
|
||
<div>
|
||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-8">
|
||
<div>
|
||
<h3 className="text-2xl font-black text-medical-gray-900 italic flex items-center gap-2">
|
||
<Stethoscope className="w-6 h-6 text-canina-blue" />
|
||
نسخههای پزشکی من
|
||
</h3>
|
||
<p className="text-xs font-bold text-medical-gray-400 mt-1">
|
||
پیگیری وضعیت نسخههای بارگذاری شده و توصیههای دارویی دامپزشک
|
||
</p>
|
||
</div>
|
||
<button
|
||
onClick={() => router.push('/?rx=1')}
|
||
className="px-5 py-3 bg-canina-blue text-white rounded-2xl text-xs font-black hover:bg-canina-dark transition-all flex items-center gap-2 shadow-lg shadow-canina-blue/20 cursor-pointer"
|
||
>
|
||
<Plus className="w-4 h-4" />
|
||
<span>ارسال نسخه جدید</span>
|
||
</button>
|
||
</div>
|
||
|
||
{isLoadingPrescriptions ? (
|
||
<div className="py-20 text-center text-medical-gray-400 font-bold text-sm">
|
||
در حال بارگذاری نسخهها...
|
||
</div>
|
||
) : prescriptions.length === 0 ? (
|
||
<div className="py-20 text-center space-y-4">
|
||
<div className="w-16 h-16 bg-medical-gray-50 text-medical-gray-300 rounded-full flex items-center justify-center mx-auto">
|
||
<Stethoscope className="w-8 h-8" />
|
||
</div>
|
||
<p className="text-sm font-bold text-medical-gray-400">هنوز هیچ نسخهای ثبت نکردهاید.</p>
|
||
<button
|
||
onClick={() => router.push('/?rx=1')}
|
||
className="px-6 py-2.5 bg-medical-gray-900 text-white rounded-xl text-xs font-bold hover:bg-canina-blue transition-all cursor-pointer"
|
||
>
|
||
ثبت اولین نسخه
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-4">
|
||
{prescriptions.map((rx: any) => (
|
||
<div
|
||
key={rx.id}
|
||
className="p-5 sm:p-6 bg-medical-gray-50 border border-medical-gray-200 rounded-[1.5rem] sm:rounded-[2rem] space-y-3 transition-all hover:shadow-md"
|
||
>
|
||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 pb-3 border-b border-medical-gray-200">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-xs font-black text-medical-gray-900">نسخه #{rx.id.slice(0, 8)}</span>
|
||
{rx.pet && (
|
||
<span className="px-2 py-0.5 bg-purple-50 text-purple-700 text-[10px] font-bold rounded-lg border border-purple-100">
|
||
🐾 {rx.pet.name} ({rx.pet.breed || rx.pet.type})
|
||
</span>
|
||
)}
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<span className={`px-3 py-1 text-xs font-black rounded-full ${
|
||
rx.status === 'APPROVED' ? 'bg-green-100 text-green-700' :
|
||
rx.status === 'REJECTED' ? 'bg-red-100 text-red-700' :
|
||
'bg-amber-100 text-amber-700'
|
||
}`}>
|
||
{rx.status === 'APPROVED' ? 'تایید شده' : rx.status === 'REJECTED' ? 'عدم تایید' : 'در حال بررسی'}
|
||
</span>
|
||
<span className="text-[11px] text-medical-gray-400 font-bold">
|
||
{toPersian(new Date(rx.createdAt).toLocaleDateString('fa-IR'))}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{rx.notes && (
|
||
<p className="text-xs text-medical-gray-600">
|
||
<strong className="text-medical-gray-800">توضیحات شما: </strong>
|
||
{rx.notes}
|
||
</p>
|
||
)}
|
||
|
||
{rx.adminNotes && (
|
||
<div className="p-3.5 bg-canina-blue/5 border border-canina-blue/10 rounded-xl text-xs text-canina-blue leading-relaxed font-bold">
|
||
<strong className="text-canina-dark block mb-1">پاسخ دامپزشک کنینا:</strong>
|
||
{rx.adminNotes.split('__PRESCRIBED_PRODUCTS__:')[0]}
|
||
</div>
|
||
)}
|
||
|
||
<div className="pt-2 flex items-center justify-end gap-2">
|
||
<a
|
||
href={rx.fileUrl.startsWith('http') ? rx.fileUrl : `${BASE_DOMAIN}${rx.fileUrl}`}
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
className="px-4 py-2 bg-white border border-medical-gray-200 text-medical-gray-700 hover:text-canina-blue hover:border-canina-blue rounded-xl text-xs font-bold transition-all"
|
||
>
|
||
مشاهده تصویر نسخه
|
||
</a>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</motion.div>
|
||
</div>
|
||
|
||
{/* Sidebar Widgets (Wallet & Charity) */}
|
||
<div className="lg:col-span-4 space-y-4 sm:space-y-6 min-w-0 order-2 lg:order-2">
|
||
<div className="bg-medical-gray-900 rounded-[1.5rem] sm:rounded-[3rem] p-5 sm:p-8 text-white relative overflow-hidden shadow-2xl">
|
||
<div className="absolute top-0 left-0 p-6 opacity-20 pointer-events-none">
|
||
<Wallet className="w-12 h-12 text-canina-blue" />
|
||
</div>
|
||
<div className="relative z-10">
|
||
<div className="text-[10px] font-black uppercase tracking-widest text-white/70 mb-2">موجودی کیف پول</div>
|
||
<div className="flex items-baseline gap-2 flex-row-reverse justify-end">
|
||
<div className="text-2xl sm:text-4xl font-black italic">{toPersian((profile.walletBalance ?? 0).toLocaleString())}</div>
|
||
<span className="text-xs sm:text-sm not-italic font-bold text-white/80">تومان</span>
|
||
</div>
|
||
<button
|
||
onClick={() => { setActiveTab("wallet"); setIsTopUpModalOpen(true); }}
|
||
disabled={isTopUpModalOpen}
|
||
className="mt-5 sm:mt-8 bg-canina-blue text-white w-full py-3.5 sm:py-4 px-4 rounded-2xl font-black hover:bg-white hover:text-canina-blue transition-all shadow-lg shadow-black/20 flex items-center justify-center gap-2 text-xs sm:text-sm border border-canina-blue cursor-pointer"
|
||
>
|
||
<ArrowUpCircle className="w-4 h-4 sm:w-5 sm:h-5 flex-shrink-0" />
|
||
<span>شارژ کیف پول</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-pink-500 rounded-[1.5rem] sm:rounded-[3rem] p-5 sm:p-8 text-white relative overflow-hidden shadow-2xl shadow-pink-200">
|
||
<div className="absolute -top-4 -right-4 opacity-10 pointer-events-none">
|
||
<Heart className="w-32 sm:w-40 h-32 sm:h-40 fill-white" />
|
||
</div>
|
||
<div className="relative z-10">
|
||
<div className="flex items-center gap-2 sm:gap-3 mb-4 sm:mb-6">
|
||
<Heart className="w-4 h-4 sm:w-5 sm:h-5 fill-white flex-shrink-0" />
|
||
<h4 className="text-xs sm:text-sm font-black italic tracking-widest">ردپای مهربانی</h4>
|
||
</div>
|
||
<div className="space-y-4 sm:space-y-6">
|
||
<div>
|
||
<p className="text-[10px] font-bold text-white/70 mb-1">مجموع کمکهای اهدایی</p>
|
||
<div className="flex items-baseline gap-2 flex-row-reverse justify-end">
|
||
<span className="text-xl sm:text-3xl font-black italic">
|
||
{toPersian((Math.max(profile.charityDonationTotal || 0, orders.reduce((sum, o) => sum + Number(o.charityDonation || 0), 0))).toLocaleString())}
|
||
</span>
|
||
<span className="text-xs font-bold opacity-60">تومان</span>
|
||
</div>
|
||
</div>
|
||
<div className="p-3 sm:p-4 bg-white/10 rounded-xl sm:rounded-2xl border border-white/20 backdrop-blur-sm">
|
||
<div className="flex items-start gap-2.5 sm:gap-3">
|
||
<div className="w-6 h-6 sm:w-8 sm:h-8 bg-white/20 rounded-lg sm:rounded-xl flex items-center justify-center flex-shrink-0 mt-0.5">
|
||
<Sparkles className="w-3 h-3 sm:w-4 sm:h-4 text-white" />
|
||
</div>
|
||
<p className="text-[10px] sm:text-xs font-black leading-relaxed">
|
||
همکاری شما باعث تامین هزینه <span className="text-xs sm:text-sm text-yellow-300 mx-1">{toPersian(Math.floor((profile.charityDonationTotal || 0) / 15000).toString())} وعده غذا</span> برای حیوانات بیپناه شده است.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<OrderDetailsModal
|
||
isOpen={!!selectedOrder}
|
||
onClose={() => setSelectedOrder(null)}
|
||
order={selectedOrder}
|
||
/>
|
||
<AddressModal
|
||
isOpen={isAddressModalOpen}
|
||
onClose={() => setIsAddressModalOpen(false)}
|
||
onSave={handleSaveAddress}
|
||
editingAddress={editingAddress}
|
||
/>
|
||
<DeleteConfirmModal
|
||
isOpen={isDeleteModalOpen}
|
||
onClose={() => setIsDeleteModalOpen(false)}
|
||
onConfirm={handleConfirmDelete}
|
||
title="حذف آدرس"
|
||
message={`آیا از حذف آدرس "${addressToDelete?.title}" اطمینان دارید؟ این عمل غیرقابل بازگشت است.`}
|
||
/>
|
||
<TopUpModal
|
||
isOpen={isTopUpModalOpen}
|
||
onClose={() => setIsTopUpModalOpen(false)}
|
||
onConfirm={topUpWallet}
|
||
/>
|
||
|
||
{/* New Ticket Modal */}
|
||
{isNewTicketModalOpen && (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
|
||
<div className="bg-white w-full max-w-lg rounded-3xl p-6 sm:p-8 shadow-2xl space-y-6 max-h-[90vh] overflow-y-auto" dir="rtl">
|
||
<div className="flex items-center justify-between pb-4 border-b border-medical-gray-100">
|
||
<div className="flex items-center gap-2">
|
||
<div className="w-10 h-10 bg-canina-blue/10 text-canina-blue rounded-xl flex items-center justify-center">
|
||
<Stethoscope className="w-5 h-5" />
|
||
</div>
|
||
<div>
|
||
<h3 className="text-lg font-black text-gray-900">ثبت تیکت و مشاوره دامپزشک</h3>
|
||
<p className="text-xs text-gray-400 font-medium">پاسخگویی توسط دامپزشکان و کارشناسان کنینا</p>
|
||
</div>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => setIsNewTicketModalOpen(false)}
|
||
className="w-8 h-8 rounded-full bg-gray-100 hover:bg-gray-200 text-gray-600 flex items-center justify-center font-bold text-sm cursor-pointer"
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
|
||
<form onSubmit={handleCreateTicket} className="space-y-4">
|
||
<div>
|
||
<label className="block text-xs font-bold text-gray-700 mb-1.5">موضوع تیکت *</label>
|
||
<input
|
||
required
|
||
type="text"
|
||
placeholder="مثال: سوال در مورد دوز مصرف مکمل سگ ژرمن"
|
||
value={newTicketSubject}
|
||
onChange={(e) => setNewTicketSubject(e.target.value)}
|
||
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-sm font-medium focus:border-canina-blue outline-none"
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-xs font-bold text-gray-700 mb-1.5">دستهبندی موضوع</label>
|
||
<select
|
||
value={newTicketCategory}
|
||
onChange={(e) => setNewTicketCategory(e.target.value)}
|
||
className="w-full px-3 py-3 bg-gray-50 border border-gray-200 rounded-xl text-xs font-bold focus:border-canina-blue outline-none"
|
||
>
|
||
<option value="VET_CONSULTATION">مشاوره تخصصی دامپزشک</option>
|
||
<option value="ORDER_SUPPORT">پیگیری و پشتیبانی سفارش</option>
|
||
<option value="PRODUCT_INQUIRY">سوال درباره داروها و مکملها</option>
|
||
<option value="GENERAL">عمومی و پیشنهادات</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-xs font-bold text-gray-700 mb-1.5">حیوان خانگی مرتبط</label>
|
||
<select
|
||
value={newTicketPetId}
|
||
onChange={(e) => setNewTicketPetId(e.target.value)}
|
||
className="w-full px-3 py-3 bg-gray-50 border border-gray-200 rounded-xl text-xs font-bold focus:border-canina-blue outline-none"
|
||
>
|
||
<option value="">بدون انتخاب / عمومی</option>
|
||
{pets.map((p) => (
|
||
<option key={p.id} value={p.id}>
|
||
{p.name} ({p.breed || p.type})
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-xs font-bold text-gray-700 mb-1.5">شرح پیام یا سوال پزشکی *</label>
|
||
<textarea
|
||
required
|
||
rows={4}
|
||
placeholder="سن، وزن، سابقه بیماری، علائم یا سوال خود را با جزئیات کامل بنویسید..."
|
||
value={newTicketMessage}
|
||
onChange={(e) => setNewTicketMessage(e.target.value)}
|
||
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-sm font-medium focus:border-canina-blue outline-none"
|
||
/>
|
||
</div>
|
||
|
||
<div className="pt-2 flex justify-end gap-3">
|
||
<button
|
||
type="button"
|
||
onClick={() => setIsNewTicketModalOpen(false)}
|
||
className="px-5 py-2.5 rounded-xl text-xs font-bold text-gray-600 bg-gray-100 hover:bg-gray-200 cursor-pointer"
|
||
>
|
||
انصراف
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
disabled={isSubmittingTicket}
|
||
className="px-6 py-2.5 rounded-xl text-xs font-black text-white bg-canina-blue hover:bg-canina-dark disabled:opacity-50 transition-all flex items-center gap-2 cursor-pointer"
|
||
>
|
||
{isSubmittingTicket ? 'در حال ارسال...' : 'ارسال تیکت'}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Ticket Details & Chat Modal */}
|
||
{selectedTicket && (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
|
||
<div className="bg-white w-full max-w-2xl rounded-3xl p-6 sm:p-8 shadow-2xl space-y-6 max-h-[90vh] flex flex-col" dir="rtl">
|
||
<div className="flex items-center justify-between pb-4 border-b border-medical-gray-100">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-10 h-10 bg-canina-blue/10 text-canina-blue rounded-xl flex items-center justify-center">
|
||
<MessageSquare className="w-5 h-5" />
|
||
</div>
|
||
<div>
|
||
<h3 className="text-base font-black text-gray-900">{selectedTicket.subject}</h3>
|
||
<div className="flex items-center gap-2 text-xs text-gray-400 font-medium mt-0.5">
|
||
<span>شناسه: {selectedTicket.ticketNumber}</span>
|
||
{selectedTicket.pet && (
|
||
<span className="text-purple-600 font-bold">• 🐾 {selectedTicket.pet.name}</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => setSelectedTicket(null)}
|
||
className="w-8 h-8 rounded-full bg-gray-100 hover:bg-gray-200 text-gray-600 flex items-center justify-center font-bold text-sm cursor-pointer"
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
|
||
{/* Messages Thread */}
|
||
<div className="flex-1 overflow-y-auto space-y-4 p-2 min-h-[220px]">
|
||
{(selectedTicket.messages || []).map((msg) => {
|
||
const isDoctorOrAdmin = msg.senderRole === 'ADMIN' || msg.senderRole === 'VET_DOCTOR';
|
||
return (
|
||
<div
|
||
key={msg.id}
|
||
className={cn(
|
||
"p-4 rounded-2xl max-w-[85%] text-xs font-medium leading-relaxed",
|
||
isDoctorOrAdmin
|
||
? "bg-emerald-50 border border-emerald-200 text-emerald-950 ml-auto"
|
||
: "bg-blue-50 border border-blue-200 text-blue-950 mr-auto"
|
||
)}
|
||
>
|
||
<div className="flex items-center justify-between gap-4 mb-1.5 pb-1 border-b border-black/5">
|
||
<span className="font-black text-[11px] flex items-center gap-1.5">
|
||
{isDoctorOrAdmin ? (
|
||
<>
|
||
<Stethoscope className="w-3.5 h-3.5 text-emerald-600" />
|
||
<span className="text-emerald-700">{msg.senderName || 'پاسخ دامپزشک کنینا'}</span>
|
||
</>
|
||
) : (
|
||
<>
|
||
<UserCircle className="w-3.5 h-3.5 text-blue-600" />
|
||
<span className="text-blue-700">{msg.senderName || 'شما'}</span>
|
||
</>
|
||
)}
|
||
</span>
|
||
<span className="text-[10px] text-gray-400 font-normal">
|
||
{new Date(msg.createdAt).toLocaleTimeString('fa-IR', { hour: '2-digit', minute: '2-digit' })}
|
||
</span>
|
||
</div>
|
||
<p className="whitespace-pre-wrap">{msg.message}</p>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{/* Quick Reply Form */}
|
||
{selectedTicket.status !== 'CLOSED' ? (
|
||
<form onSubmit={handleSendReply} className="pt-3 border-t border-medical-gray-100 flex gap-2">
|
||
<input
|
||
type="text"
|
||
placeholder="پاسخ خود را بنویسید..."
|
||
value={replyMessage}
|
||
onChange={(e) => setReplyMessage(e.target.value)}
|
||
className="flex-1 px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-xs font-medium focus:border-canina-blue outline-none"
|
||
/>
|
||
<button
|
||
type="submit"
|
||
disabled={isSendingReply || !replyMessage.trim()}
|
||
className="px-5 py-3 bg-canina-blue text-white rounded-xl text-xs font-black hover:bg-canina-dark disabled:opacity-50 transition-all flex items-center gap-1.5 shrink-0 cursor-pointer"
|
||
>
|
||
<Send className="w-3.5 h-3.5" />
|
||
<span>ارسال</span>
|
||
</button>
|
||
</form>
|
||
) : (
|
||
<div className="p-3 bg-gray-50 rounded-xl text-center text-xs text-gray-500 font-bold">
|
||
این تیکت بسته شده است. در صورت نیاز میتوانید تیکت جدیدی ثبت فرمایید.
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Wallet Withdrawal Request Modal */}
|
||
{isWithdrawModalOpen && (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in font-vazir">
|
||
<div className="bg-white w-full max-w-md rounded-3xl p-6 sm:p-8 shadow-2xl space-y-6" dir="rtl">
|
||
<div className="flex items-center justify-between pb-4 border-b border-medical-gray-100">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-10 h-10 bg-amber-50 text-amber-600 rounded-xl flex items-center justify-center">
|
||
<ArrowDownCircle className="w-6 h-6" />
|
||
</div>
|
||
<div>
|
||
<h3 className="text-base font-black text-gray-900">درخواست برداشت و تسویه وجه</h3>
|
||
<p className="text-xs text-gray-400 font-bold mt-0.5">
|
||
موجودی قابل برداشت: {toPersian((profile.walletBalance || 0).toLocaleString())} تومان
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => setIsWithdrawModalOpen(false)}
|
||
className="w-8 h-8 rounded-full bg-gray-100 hover:bg-gray-200 text-gray-600 flex items-center justify-center font-bold text-sm cursor-pointer"
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
|
||
<form onSubmit={handleWithdrawSubmit} className="space-y-4">
|
||
<div>
|
||
<label className="block text-xs font-black text-gray-700 mb-1">
|
||
مبلغ درخواستی (تومان) *
|
||
</label>
|
||
<input
|
||
type="text"
|
||
required
|
||
placeholder="حداقل ۵۰,۰۰۰ تومان"
|
||
value={withdrawAmount}
|
||
onChange={(e) => setWithdrawAmount(e.target.value)}
|
||
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-sm font-bold text-gray-900 focus:border-canina-blue outline-none"
|
||
dir="ltr"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-xs font-black text-gray-700 mb-1">
|
||
شماره شبا یا شماره کارت جهت واریز *
|
||
</label>
|
||
<input
|
||
type="text"
|
||
required
|
||
placeholder="IR... یا شماره ۱۶ رقمی کارت"
|
||
value={withdrawIban}
|
||
onChange={(e) => setWithdrawIban(e.target.value)}
|
||
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-xs font-mono font-bold text-gray-900 focus:border-canina-blue outline-none"
|
||
dir="ltr"
|
||
/>
|
||
<p className="text-[10px] text-gray-400 font-medium mt-1">
|
||
شماره حساب باید به نام صاحب حساب کاربری ({profile.firstName} {profile.lastName}) باشد.
|
||
</p>
|
||
</div>
|
||
|
||
<div className="p-3 bg-amber-50 rounded-xl text-[11px] text-amber-800 font-medium leading-relaxed">
|
||
واریز مبالغ تسویه پس از تایید واحد مالی کنینا، طی ۲۴ الی ۴۸ ساعت کاری از طریق سامانه پایا انجام خواهد شد.
|
||
</div>
|
||
|
||
<div className="pt-2 flex justify-end gap-3">
|
||
<button
|
||
type="button"
|
||
onClick={() => setIsWithdrawModalOpen(false)}
|
||
className="px-5 py-2.5 rounded-xl text-xs font-bold text-gray-600 bg-gray-100 hover:bg-gray-200 cursor-pointer"
|
||
>
|
||
انصراف
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
disabled={isSubmittingWithdrawal}
|
||
className="px-6 py-2.5 rounded-xl text-xs font-black text-white bg-canina-blue hover:bg-canina-dark disabled:opacity-50 transition-all flex items-center gap-2 cursor-pointer shadow-md shadow-canina-blue/20"
|
||
>
|
||
{isSubmittingWithdrawal ? 'در حال ثبت...' : 'ثبت درخواست برداشت'}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Order Details & Retry Payment Modal */}
|
||
<OrderDetailsModal
|
||
isOpen={!!selectedOrder}
|
||
onClose={() => setSelectedOrder(null)}
|
||
order={selectedOrder}
|
||
onOrderUpdated={async () => {
|
||
try {
|
||
await fetchProfile();
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
}}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|