diff --git a/frontend/admin-panel/src/components/ui/MediaSelector.tsx b/frontend/admin-panel/src/components/ui/MediaSelector.tsx index dcbcb7e..7f7733b 100644 --- a/frontend/admin-panel/src/components/ui/MediaSelector.tsx +++ b/frontend/admin-panel/src/components/ui/MediaSelector.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, useCallback } from 'react'; import { X, Upload, Image as ImageIcon, Trash2, CheckCircle2, Clipboard } from 'lucide-react'; import { toast } from 'react-hot-toast'; import api, { BASE_DOMAIN } from '../../services/api'; @@ -190,7 +190,7 @@ export default function MediaSelector({ isOpen, onClose, onSelect, multiple = fa return (
-
تصویر را اینجا رها کنید تا آپلود شود

)} - + {/* Header */}
@@ -234,14 +234,14 @@ export default function MediaSelector({ isOpen, onClose, onSelect, multiple = fa Ctrl+V برای چسباندن تصویر - - -
- +
); } + diff --git a/frontend/admin-panel/src/pages/Blogs.tsx b/frontend/admin-panel/src/pages/Blogs.tsx index 7529877..08834a2 100644 --- a/frontend/admin-panel/src/pages/Blogs.tsx +++ b/frontend/admin-panel/src/pages/Blogs.tsx @@ -8,6 +8,10 @@ import MediaSelector from '../components/ui/MediaSelector'; import ConfirmModal from '../components/ui/ConfirmModal'; export interface BlogPost { + author?: { + firstName?: string; + lastName?: string; + }; id: string; title: string; slug: string; @@ -130,7 +134,7 @@ export default function Blogs() {

مدیریت محتوای آموزشی و مقالات علمی سایت

- - +
- setFormData({...formData, title: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" /> + setFormData({ ...formData, title: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" />
- setFormData({...formData, slug: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-sm" /> + setFormData({ ...formData, slug: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-sm" />
- +
@@ -253,31 +257,31 @@ export default function Blogs() {
- +

تنظیمات سئو (SEO)

- setFormData({...formData, metaTitle: e.target.value})} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm" /> + setFormData({ ...formData, metaTitle: e.target.value })} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm" />
- setFormData({...formData, keywords: e.target.value})} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm" /> + setFormData({ ...formData, keywords: e.target.value })} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm" />
- +
- +
@@ -286,7 +290,7 @@ export default function Blogs() {
)} - setIsMediaSelectorOpen(false)} onSelect={(url) => setFormData({...formData, imageUrl: url})} /> + setIsMediaSelectorOpen(false)} onSelect={(url) => setFormData({ ...formData, imageUrl: url })} />
- { setSearch(e.target.value); setPage(1); }} className="w-full pl-4 pr-12 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none transition-all" @@ -127,7 +137,7 @@ export default function Pets() {
{pet.user?.mobile || pet.user?.email}
- {pet.age} ساله، {pet.weight} کیلوگرم
+ {pet.age} ساله، {pet.weight} کیلوگرم
تحرک: {pet.activityLevel} diff --git a/frontend/admin-panel/src/pages/Products.tsx b/frontend/admin-panel/src/pages/Products.tsx index 7c990ac..495957c 100644 --- a/frontend/admin-panel/src/pages/Products.tsx +++ b/frontend/admin-panel/src/pages/Products.tsx @@ -11,9 +11,12 @@ export interface Category { id: string; nameFa: string; nameEn?: string; + name?: string; } export interface Product { + name?: string; + category?: Category | string; id: string; artNo: string; nameFa: string; @@ -46,7 +49,7 @@ export default function Products() { const [products, setProducts] = useState([]); const [categories, setCategories] = useState([]); const [isLoading, setIsLoading] = useState(true); - + // URL and String Helper Functions const getProductImageUrl = (url: string) => { if (!url) return ''; @@ -254,7 +257,7 @@ export default function Products() {

افزودن و ویرایش پیشرفته محصولات با تنظیمات سئو

- - - ))} - { - if (e.key === 'Enter') { - e.preventDefault(); - const input = e.currentTarget; - const value = input.value.trim(); - if (value && !formData.symptoms.includes(value)) { - setFormData({ - ...formData, - symptoms: [...(formData.symptoms || []), value] - }); - input.value = ''; - } - } - }} - /> - -

پس از نوشتن هر علامت درمانی (مثلاً «درد مفاصل»)، کلید Enter را فشار دهید.

- - - +
+ {formData.symptoms && formData.symptoms.map((symptom, idx) => ( + + {symptom} + + + ))} + { + if (e.key === 'Enter') { + e.preventDefault(); + const input = e.currentTarget; + const value = input.value.trim(); + if (value && !formData.symptoms.includes(value)) { + setFormData({ + ...formData, + symptoms: [...(formData.symptoms || []), value] + }); + input.value = ''; + } + } + }} + /> +
+

پس از نوشتن هر علامت درمانی (مثلاً «درد مفاصل»)، کلید Enter را فشار دهید.

+ + + {/* Pricing Tab */}
@@ -608,15 +611,15 @@ export default function Products() { قیمت اختصاصی B2B (تومان) (اختیاری) - setFormData({...formData, wholesalePrice: e.target.value ? Number(e.target.value) : ''})} className="w-full px-4 py-3 rounded-xl border border-purple-200 focus:border-purple-600 outline-none font-bold bg-purple-50/20" dir="ltr" /> + setFormData({ ...formData, wholesalePrice: e.target.value ? Number(e.target.value) : '' })} className="w-full px-4 py-3 rounded-xl border border-purple-200 focus:border-purple-600 outline-none font-bold bg-purple-50/20" dir="ltr" />
- setFormData({...formData, packageSize: Number(e.target.value)})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" dir="ltr" /> + setFormData({ ...formData, packageSize: Number(e.target.value) })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" dir="ltr" />
- setFormData({...formData, unit: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" /> + setFormData({ ...formData, unit: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" />
@@ -644,13 +647,13 @@ export default function Products() { - setFormData({...formData, slug: e.target.value})} - className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" - dir="ltr" - placeholder={derivedSlug} + setFormData({ ...formData, slug: e.target.value })} + className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" + dir="ltr" + placeholder={derivedSlug} /> @@ -664,11 +667,11 @@ export default function Products() { - setFormData({...formData, metaTitle: e.target.value})} - className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" + setFormData({ ...formData, metaTitle: e.target.value })} + className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" placeholder={derivedMetaTitle} /> @@ -683,10 +686,10 @@ export default function Products() { - @@ -702,11 +705,11 @@ export default function Products() { - setFormData({...formData, keywords: e.target.value})} - className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" + setFormData({ ...formData, keywords: e.target.value })} + className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" placeholder="مثال: مکمل سگ, تقویت مفاصل, کنهیدروکس" /> @@ -721,12 +724,12 @@ export default function Products() { - setFormData({...formData, canonicalUrl: e.target.value})} - className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-sm" - dir="ltr" + setFormData({ ...formData, canonicalUrl: e.target.value })} + className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-sm" + dir="ltr" placeholder={derivedCanonicalUrl} /> @@ -735,7 +738,7 @@ export default function Products() { {/* Left Column: Sticky Google SERP Preview */}
پیش‌نمایش نتایج در گوگل (Google SERP Preview) - +
{/* Domain / Breadcrumb */}
@@ -745,12 +748,12 @@ export default function Products() { {derivedSlug}
- + {/* Title */}

{derivedMetaTitle}

- + {/* Description */}

{derivedMetaDescription} @@ -775,19 +778,19 @@ export default function Products() { امکان چسباندن (Ctrl+V) مستقیم آدرس اینترنتی تصویر

- { - setFormData({...formData, imageUrl: e.target.value}); + setFormData({ ...formData, imageUrl: e.target.value }); setMediaImageError(false); - }} + }} placeholder="https://example.com/images/product.png یا /uploads/photo.jpg" className="flex-1 px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-sm" dir="ltr" /> - - +
@@ -839,7 +842,7 @@ export default function Products() { {/* Multimedia Attachments (TASK-2.3 & TASK-2.4) */}

پیوست‌های مالتی‌مدیا و مستندات علمی

- +
@@ -880,7 +883,7 @@ export default function Products() {
- +
- +
-
- {user.email} {isEditing ? ( - ) : ( - + {user.role === 'User_Wholesale' ? 'خریدار عمده' : - user.role?.includes('B2B') ? 'همکار (B2B)' : - (user.role === 'ADMIN' ? 'مدیر سیستم' : 'مشتری عادی')} + user.role?.includes('B2B') ? 'همکار (B2B)' : + (user.role === 'ADMIN' ? 'مدیر سیستم' : 'مشتری عادی')} )} diff --git a/frontend/admin-panel/src/pages/Videos.tsx b/frontend/admin-panel/src/pages/Videos.tsx index aac4ef2..c2c2e36 100644 --- a/frontend/admin-panel/src/pages/Videos.tsx +++ b/frontend/admin-panel/src/pages/Videos.tsx @@ -433,3 +433,8 @@ export default function Videos() {
); } + +function fetchVideosList() { + throw new Error('Function not implemented.'); +} + diff --git a/frontend/admin-panel/src/pages/WholesaleApplications.tsx b/frontend/admin-panel/src/pages/WholesaleApplications.tsx index 18fc0c2..fc3a256 100644 --- a/frontend/admin-panel/src/pages/WholesaleApplications.tsx +++ b/frontend/admin-panel/src/pages/WholesaleApplications.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { toast } from 'react-hot-toast'; import api from '../services/api'; import ConfirmModal from '../components/ui/ConfirmModal'; @@ -99,9 +99,8 @@ export default function WholesaleApplications() {
{u.email || u.phoneNumber}
- + {u.role === 'User_Wholesale' ? 'خریدار عمده تاییدشده' : 'در انتظار بررسی B2B'} diff --git a/frontend/admin-panel/src/pages/Wiki.tsx b/frontend/admin-panel/src/pages/Wiki.tsx index 56ef373..bfc6090 100644 --- a/frontend/admin-panel/src/pages/Wiki.tsx +++ b/frontend/admin-panel/src/pages/Wiki.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { Search, Plus, Edit2, Trash2, BookOpen, XCircle } from 'lucide-react'; import { toast } from 'react-hot-toast'; import api from '../services/api'; diff --git a/frontend/application/app/ClientLayout.tsx b/frontend/application/app/ClientLayout.tsx index e3bc006..1a98fcc 100644 --- a/frontend/application/app/ClientLayout.tsx +++ b/frontend/application/app/ClientLayout.tsx @@ -103,7 +103,7 @@ export default function ClientLayout({ children }: { children: React.ReactNode } onLogin={() => { setLoginModalOpen(false); }} - petName={advisorData?.name} + petName={advisorData?.name as string | undefined} isAdvisorContext={!!advisorData} /> diff --git a/frontend/application/components/PetProfile.tsx b/frontend/application/components/PetProfile.tsx index a5a0baa..3c896ae 100644 --- a/frontend/application/components/PetProfile.tsx +++ b/frontend/application/components/PetProfile.tsx @@ -117,7 +117,7 @@ export default function PetProfile({ initialView, advisorNeed }: { const recommendedProducts = useMemo(() => { if (!activePet || products.length === 0) return []; - let picks: { product: Product, reason?: string }[] = []; + const picks: { product: Product, reason?: string }[] = []; // Priority 1: Match activePet.medicalConditions directly against product.symptoms if (activePet.medicalConditions && activePet.medicalConditions.length > 0) { diff --git a/frontend/application/components/UserDashboard.tsx b/frontend/application/components/UserDashboard.tsx index 380f089..030c5d1 100644 --- a/frontend/application/components/UserDashboard.tsx +++ b/frontend/application/components/UserDashboard.tsx @@ -1,11 +1,11 @@ "use client"; import React, { useState } from "react"; -import { motion, AnimatePresence } from "motion/react"; +import { motion } 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 } from "lucide-react"; import { OrderRowSkeleton } from "./Skeleton"; import { useUserStore, Address } from "../lib/store/userStore"; import { useCartStore } from "../lib/store/cartStore"; -import { toPersian, cn } from "../lib/utils"; +import { toPersian, toEnglishDigits, cn } from "../lib/utils"; import { toast } from "sonner"; import OrderDetailsModal from "./OrderDetailsModal"; import AddressModal from "./AddressModal"; @@ -16,7 +16,7 @@ import { useRouter } from 'next/navigation'; export default function UserDashboard() { const router = useRouter(); - const { profile, isLoggedIn, logout, addAddress, updateAddress, deleteAddress, setDefaultAddress, topUpWallet } = useUserStore(); + const { profile, isLoggedIn, logout, updateProfile, addAddress, updateAddress, deleteAddress, setDefaultAddress, topUpWallet } = useUserStore(); React.useEffect(() => { if (!isLoggedIn && typeof window !== "undefined") { @@ -29,12 +29,19 @@ export default function UserDashboard() { const [activeTab, setActiveTab] = React.useState<"profile" | "orders" | "wallet" | "addresses" | "tickets">("profile"); const [isLoadingOrders, setIsLoadingOrders] = useState(false); - // Fetch fresh profile data (which includes orders) whenever orders tab is opened React.useEffect(() => { if (activeTab === "orders") { - setIsLoadingOrders(true); - useUserStore.getState().fetchProfile().finally(() => setIsLoadingOrders(false)); + 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(null); @@ -51,21 +58,29 @@ export default function UserDashboard() { // Profile State const [isEditing, setIsEditing] = useState(false); const [isSavingProfile, setIsSavingProfile] = useState(false); - const [profileForm, setProfileForm] = useState({ + const [profileForm, setProfileForm] = useState(() => ({ firstName: profile.firstName || "", lastName: profile.lastName || "", email: profile.email || "", mobile: profile.mobile || "" - }); + })); // Sync profile data on change React.useEffect(() => { - setProfileForm({ - firstName: profile.firstName || "", - lastName: profile.lastName || "", - email: profile.email || "", - mobile: profile.mobile || "" + 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) { @@ -74,17 +89,10 @@ export default function UserDashboard() { const handleSaveProfile = async (e: React.FormEvent) => { e.preventDefault(); - if (!isEditing) return; - - const cleanPhone = profileForm.mobile.trim(); - if (!/^09\d{9}$/.test(cleanPhone)) { - toast.error("شماره موبایل وارد شده معتبر نیست (باید ۱۱ رقم باشد و با ۰۹ شروع شود)"); - return; - } - setIsSavingProfile(true); try { - await useUserStore.getState().updateProfile({ + const cleanPhone = toEnglishDigits(profileForm.mobile); + await updateProfile({ firstName: profileForm.firstName, lastName: profileForm.lastName, email: profileForm.email, @@ -92,8 +100,8 @@ export default function UserDashboard() { }); setIsEditing(false); toast.success("اطلاعات کاربری با موفقیت ویرایش شد"); - } catch (err: any) { - toast.error(err.message || "خطا در ویرایش اطلاعات"); + } catch { + toast.error("خطا در ویرایش اطلاعات"); } finally { setIsSavingProfile(false); } @@ -111,7 +119,7 @@ export default function UserDashboard() { } else { await addAddress(addr); } - } catch (err) { + } catch { toast.error("خطا در ثبت آدرس"); } setEditingAddress(null); @@ -127,7 +135,7 @@ export default function UserDashboard() { try { await deleteAddress(addressToDelete.id); toast.success("آدرس با موفقیت حذف شد"); - } catch (err) { + } catch { toast.error("خطا در حذف آدرس"); } setAddressToDelete(null); @@ -370,7 +378,7 @@ export default function UserDashboard() {
سفارش {toPersian(order.trackingNumber || order.id?.substring(0, 8))}
- {toPersian(new Date(order.date || (order as any).createdAt).toLocaleDateString("fa-IR"))} - ساعت {toPersian(new Date(order.date || (order as any).createdAt).toLocaleTimeString("fa-IR", { hour: '2-digit', minute: '2-digit' }))} + {toPersian(new Date(order.date || (order as unknown as Record).createdAt || 0).toLocaleDateString("fa-IR"))} - ساعت {toPersian(new Date(order.date || (order as unknown as Record).createdAt || 0).toLocaleTimeString("fa-IR", { hour: '2-digit', minute: '2-digit' }))}
@@ -477,7 +485,7 @@ export default function UserDashboard() { try { await setDefaultAddress(addr.id); toast.success("آدرس پیش‌فرض با موفقیت تغییر کرد"); - } catch (err) { + } catch { toast.error("خطا در تغییر آدرس پیش‌فرض"); } }} diff --git a/frontend/application/components/VetGallery.tsx b/frontend/application/components/VetGallery.tsx index b6d7e99..5a4ff5b 100644 --- a/frontend/application/components/VetGallery.tsx +++ b/frontend/application/components/VetGallery.tsx @@ -1,13 +1,24 @@ "use client"; import React, { useState } from "react"; -import { Play, PlayCircle, Star, ShieldCheck, X } from "lucide-react"; +import { Play, Star, ShieldCheck, X } from "lucide-react"; import { motion, AnimatePresence } from "motion/react"; import Link from "next/link"; -import { useRouter } from "next/navigation"; import SafeImage from "./SafeImage"; import { useSettingsStore } from "../lib/store/settingsStore"; import { videoService, Video } from "../lib/services/videoService"; +export interface TestimonialItem { + id?: string; + clinicName?: string; + vetName?: string; + imageUrl?: string; + quote?: string; + title?: string; + thumbnail?: string; + videoUrl?: string; + description?: string; +} + const FALLBACK_VIDEOS = [ { id: "v1", @@ -38,11 +49,10 @@ const FALLBACK_VIDEOS = [ } ]; -export default function VetGallery({ testimonials = [] }: { testimonials?: any[] }) { - const router = useRouter(); +export default function VetGallery({ testimonials = [] }: { testimonials?: TestimonialItem[] }) { const getText = useSettingsStore(state => state.getText); const [apiVideos, setApiVideos] = useState([]); - const [selectedVideo, setSelectedVideo] = useState(null); + const [selectedVideo, setSelectedVideo] = useState(null); React.useEffect(() => { videoService.getVideos({ featured: true, limit: 3 }).then((data) => { @@ -55,13 +65,13 @@ export default function VetGallery({ testimonials = [] }: { testimonials?: any[] const displayItems = testimonials.length > 0 ? testimonials.map((t, idx) => ({ id: t.id || `t-${idx}`, - title: t.clinicName ? `${t.vetName} — ${t.clinicName}` : t.vetName, - doctor: t.vetName, + title: t.clinicName ? `${t.vetName} — ${t.clinicName}` : (t.vetName || ''), + doctor: t.vetName || '', duration: "۰۲:۰۰", thumbnail: t.imageUrl || "https://images.unsplash.com/photo-1576091160550-217359f42f8c?auto=format&fit=crop&q=80&w=400", videoUrl: "https://www.w3schools.com/html/mov_bbb.mp4", - description: t.quote, - quote: t.quote + description: t.quote || '', + quote: t.quote || '' })) : (apiVideos.length > 0 ? apiVideos : FALLBACK_VIDEOS); @@ -102,7 +112,7 @@ export default function VetGallery({ testimonials = [] }: { testimonials?: any[]
@@ -117,7 +127,7 @@ export default function VetGallery({ testimonials = [] }: { testimonials?: any[]

- {video.title || video.quote?.substring(0, 40) + '...'} + {video.title || (video.quote ? video.quote.substring(0, 40) + '...' : '')}

diff --git a/frontend/application/components/VideosPage.tsx b/frontend/application/components/VideosPage.tsx index 2008503..0d24550 100644 --- a/frontend/application/components/VideosPage.tsx +++ b/frontend/application/components/VideosPage.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useState } from "react"; import { motion, AnimatePresence } from "motion/react"; -import { ChevronRight, PlayCircle, Star, ShieldCheck, X, Search, Clock, User } from "lucide-react"; +import { ChevronRight, PlayCircle, ShieldCheck, X, Search, Clock, User } from "lucide-react"; import SafeImage from "./SafeImage"; diff --git a/frontend/application/components/__tests__/CartDrawer.test.tsx b/frontend/application/components/__tests__/CartDrawer.test.tsx index ee109c7..18ae1af 100644 --- a/frontend/application/components/__tests__/CartDrawer.test.tsx +++ b/frontend/application/components/__tests__/CartDrawer.test.tsx @@ -44,7 +44,7 @@ describe('CartDrawer', () => { coupon: null, applyCoupon: vi.fn(), addItem: vi.fn(), - } as any); + } as unknown as ReturnType); render(); @@ -64,7 +64,7 @@ describe('CartDrawer', () => { coupon: null, applyCoupon: vi.fn(), addItem: vi.fn(), - } as any); + } as unknown as ReturnType); render(); @@ -86,7 +86,7 @@ describe('CartDrawer', () => { coupon: null, applyCoupon: vi.fn(), addItem: vi.fn(), - } as any); + } as unknown as ReturnType); render(); diff --git a/frontend/application/components/__tests__/FeaturedProducts.test.tsx b/frontend/application/components/__tests__/FeaturedProducts.test.tsx index 53b115c..dbd75c9 100644 --- a/frontend/application/components/__tests__/FeaturedProducts.test.tsx +++ b/frontend/application/components/__tests__/FeaturedProducts.test.tsx @@ -37,7 +37,7 @@ describe('FeaturedProducts', () => { vi.clearAllMocks(); vi.mocked(usePetStore).mockReturnValue({ getActivePet: () => null, - } as any); + } as unknown as ReturnType); }); it('renders loading skeletons initially', () => { @@ -48,7 +48,7 @@ describe('FeaturedProducts', () => { }); it('renders products once loaded', async () => { - vi.mocked(productService.getFeaturedProducts).mockResolvedValue(mockProducts as any); + vi.mocked(productService.getFeaturedProducts).mockResolvedValue(mockProducts as unknown as ReturnType); render(); await waitFor(() => { @@ -59,7 +59,7 @@ describe('FeaturedProducts', () => { it('calls onProductClick when product card is clicked', async () => { const handleProductClick = vi.fn(); - vi.mocked(productService.getFeaturedProducts).mockResolvedValue(mockProducts as any); + vi.mocked(productService.getFeaturedProducts).mockResolvedValue(mockProducts as unknown as ReturnType); render(); await waitFor(() => { @@ -72,7 +72,7 @@ describe('FeaturedProducts', () => { it('calls onShopNavigate when navigation link is clicked', async () => { const handleShopNavigate = vi.fn(); - vi.mocked(productService.getFeaturedProducts).mockResolvedValue(mockProducts as any); + vi.mocked(productService.getFeaturedProducts).mockResolvedValue(mockProducts as unknown as ReturnType); render(); const navBtn = screen.getByText('مشاهده تمامی محصولات'); diff --git a/frontend/application/components/__tests__/Header.test.tsx b/frontend/application/components/__tests__/Header.test.tsx index 1113e2c..284db03 100644 --- a/frontend/application/components/__tests__/Header.test.tsx +++ b/frontend/application/components/__tests__/Header.test.tsx @@ -25,21 +25,21 @@ describe('Header', () => { vi.mocked(useCartStore).mockReturnValue({ getTotalItems: () => 3, - } as any); + } as unknown as ReturnType); vi.mocked(usePetStore).mockReturnValue({ pets: [], activePetId: null, setActivePet: vi.fn(), getActivePet: () => null, - } as any); + } as unknown as ReturnType); vi.mocked(useUserStore).mockReturnValue({ role: 'User_Guest', isLoggedIn: false, logout: vi.fn(), profile: { firstName: '' }, - } as any); + } as unknown as ReturnType); }); it('renders brand name and login button when guest', () => { @@ -65,14 +65,14 @@ describe('Header', () => { isLoggedIn: true, logout: vi.fn(), profile: { firstName: 'کوروش' }, - } as any); + } as unknown as ReturnType); vi.mocked(usePetStore).mockReturnValue({ pets: [{ id: 'pet-1', name: 'ملوس', type: 'گربه' }], activePetId: 'pet-1', setActivePet: vi.fn(), getActivePet: () => ({ id: 'pet-1', name: 'ملوس', type: 'گربه' }), - } as any); + } as unknown as ReturnType); render(
{ return fallback; }); - vi.mocked(useSettingsStore).mockImplementation((selector: any) => selector({ getText: mockGetText })); + vi.mocked(useSettingsStore).mockImplementation((selector: (state: { getText: (key: string, fallback: string) => string }) => unknown) => selector({ getText: mockGetText }) as ReturnType); render(); @@ -31,8 +31,8 @@ describe('Hero', () => { }); it('calls onShopNavigate when "مشاهده محصولات" button is clicked', () => { - const mockGetText = vi.fn().mockImplementation((key, fallback) => fallback); - vi.mocked(useSettingsStore).mockImplementation((selector: any) => selector({ getText: mockGetText })); + const mockGetText = vi.fn().mockImplementation((_key, fallback) => fallback); + vi.mocked(useSettingsStore).mockImplementation((selector: (state: { getText: (key: string, fallback: string) => string }) => unknown) => selector({ getText: mockGetText }) as ReturnType); const handleShopNavigate = vi.fn(); render(); diff --git a/frontend/application/lib/data/products.ts b/frontend/application/lib/data/products.ts index d8f4806..d98e42e 100644 --- a/frontend/application/lib/data/products.ts +++ b/frontend/application/lib/data/products.ts @@ -756,7 +756,7 @@ export const PRODUCTS: Product[] = [ contraindications: ["مصرف همزمان ویتامین D2 مجاز نیست"], calculateDosage: (weight, isPregnant) => { const rate = isPregnant ? 3 : 2; - let qty = (weight / 10) * rate; + const qty = (weight / 10) * rate; return { quantity: Math.round(qty), unit: "قرص", description: isPregnant ? "دوز بارداری/شیردهی" : "دوز معمول" }; }, analysis: { "کلسیم": "۲۲٪" }, @@ -788,7 +788,7 @@ export const PRODUCTS: Product[] = [ symptoms: ["خستگی مزمن", "رشد نامتناسب", "استرس"], suitableFor: "هر دو", calculateDosage: (weight) => { - let qty = Math.max(1, weight / 10); + const qty = Math.max(1, weight / 10); return { quantity: Math.round(qty), unit: "قرص", description: "روزانه" }; }, analysis: { "ویتامین‌ها": "A تا K3 کامل" }, @@ -853,7 +853,7 @@ export const PRODUCTS: Product[] = [ symptoms: ["بینی روشن", "پوشش کدر", "کمبود ید"], suitableFor: "هر دو", calculateDosage: (weight) => { - let tablets = (weight / 10) * 2; + const tablets = (weight / 10) * 2; return { quantity: Math.round(tablets), unit: "قرص", description: "روزانه" }; }, analysis: { "ید": "غنی", "لیزین": "+" }, @@ -1024,7 +1024,7 @@ export const PRODUCTS: Product[] = [ suitableFor: "گربه", onSetOfAction: "هفته دوم", calculateDosage: (weight) => { - let tsp = weight > 6 ? 1 : 0.5; + const tsp = weight > 6 ? 1 : 0.5; return { quantity: tsp, unit: "قاشق چایخوری", description: "روزانه با غذا مخلوط شود" }; }, analysis: { "تورین": "۷۰۰ گرم/kg" }, diff --git a/frontend/application/lib/hooks/useNetworkStatus.ts b/frontend/application/lib/hooks/useNetworkStatus.ts index 615b296..1117c8d 100644 --- a/frontend/application/lib/hooks/useNetworkStatus.ts +++ b/frontend/application/lib/hooks/useNetworkStatus.ts @@ -1,13 +1,11 @@ import { useState, useEffect } from 'react'; export function useNetworkStatus() { - const [isOnline, setIsOnline] = useState(true); + const [isOnline, setIsOnline] = useState(() => + typeof navigator !== 'undefined' ? navigator.onLine : true + ); useEffect(() => { - if (typeof window !== 'undefined' && typeof navigator !== 'undefined') { - setIsOnline(navigator.onLine); - } - const handleOnline = () => setIsOnline(true); const handleOffline = () => setIsOnline(false); diff --git a/frontend/application/lib/services/api.ts b/frontend/application/lib/services/api.ts index abbd281..97b4ee4 100644 --- a/frontend/application/lib/services/api.ts +++ b/frontend/application/lib/services/api.ts @@ -1,5 +1,6 @@ import axios from 'axios'; import { toast } from 'sonner'; +import { useUserStore } from '../store/userStore'; const baseURL = process.env.NEXT_PUBLIC_API_URL || (process.env.NODE_ENV === 'development' ? 'http://localhost:4001/api' : '/api'); @@ -8,7 +9,7 @@ export interface ApiErrorPayload { statusCode: number; message: string; code: string; - details?: Array<{ field: string; message: string }> | Record; + details?: Array<{ field: string; message: string }> | Record; timestamp?: string; path?: string; } @@ -50,7 +51,6 @@ api.interceptors.response.use( localStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); try { - const { useUserStore } = require('../store/userStore'); useUserStore.getState().logout(); } catch { // Ignore if store not ready diff --git a/frontend/application/lib/services/authService.ts b/frontend/application/lib/services/authService.ts index b7fd07f..0d072a3 100644 --- a/frontend/application/lib/services/authService.ts +++ b/frontend/application/lib/services/authService.ts @@ -11,8 +11,8 @@ export interface User { charityDonationTotal: number; createdAt: string; updatedAt: string; - pets?: any[]; - orders?: any[]; + pets?: unknown[]; + orders?: unknown[]; } export interface AuthResponse { @@ -23,6 +23,15 @@ export interface AuthResponse { }; } +interface ApiErr { + response?: { + status?: number; + data?: { + message?: string | string[]; + }; + }; +} + export class AuthService { private static instance: AuthService; @@ -42,8 +51,9 @@ export class AuthService { try { const response = await api.post('/auth/send-otp', { phoneNumber }); return response.data; - } catch (error: any) { - const message = error.response?.data?.message || 'خطا در ارسال کد تایید'; + } catch (error) { + const err = error as ApiErr; + const message = err.response?.data?.message || 'خطا در ارسال کد تایید'; throw new Error(Array.isArray(message) ? message[0] : message); } } @@ -59,8 +69,9 @@ export class AuthService { localStorage.setItem('accessToken', data.accessToken); } return response.data; - } catch (error: any) { - const message = error.response?.data?.message || 'کد تایید نامعتبر است'; + } catch (error) { + const err = error as ApiErr; + const message = err.response?.data?.message || 'کد تایید نامعتبر است'; throw new Error(Array.isArray(message) ? message[0] : message); } } @@ -68,7 +79,7 @@ export class AuthService { /** * Register with email/mobile and password */ - public async register(data: any): Promise { + public async register(data: Record): Promise { try { const response = await api.post('/auth/register', data); const { data: resData } = response.data; @@ -76,8 +87,9 @@ export class AuthService { localStorage.setItem('accessToken', resData.accessToken); } return response.data; - } catch (error: any) { - const message = error.response?.data?.message || 'خطا در ثبت‌نام'; + } catch (error) { + const err = error as ApiErr; + const message = err.response?.data?.message || 'خطا در ثبت‌نام'; throw new Error(Array.isArray(message) ? message[0] : message); } } @@ -93,8 +105,9 @@ export class AuthService { localStorage.setItem('accessToken', data.accessToken); } return response.data; - } catch (error: any) { - const message = error.response?.data?.message || 'نام کاربری یا رمز عبور اشتباه است'; + } catch (error) { + const err = error as ApiErr; + const message = err.response?.data?.message || 'نام کاربری یا رمز عبور اشتباه است'; throw new Error(Array.isArray(message) ? message[0] : message); } } @@ -106,11 +119,12 @@ export class AuthService { try { const response = await api.get('/users/profile'); return response.data; - } catch (error: any) { - if (error.response?.status === 401 || error.response?.status === 403) { + } catch (error) { + const err = error as ApiErr; + if (err.response?.status === 401 || err.response?.status === 403) { return null; } - const message = error.response?.data?.message || 'خطا در دریافت اطلاعات کاربری'; + const message = err.response?.data?.message || 'خطا در دریافت اطلاعات کاربری'; throw new Error(Array.isArray(message) ? message[0] : message); } } @@ -122,8 +136,9 @@ export class AuthService { try { const response = await api.patch('/users/profile', profileData); return response.data; - } catch (error: any) { - const message = error.response?.data?.message || 'خطا در ویرایش اطلاعات کاربری'; + } catch (error) { + const err = error as ApiErr; + const message = err.response?.data?.message || 'خطا در ویرایش اطلاعات کاربری'; throw new Error(Array.isArray(message) ? message[0] : message); } } diff --git a/frontend/application/lib/services/orderService.ts b/frontend/application/lib/services/orderService.ts index 047a90d..ec75669 100644 --- a/frontend/application/lib/services/orderService.ts +++ b/frontend/application/lib/services/orderService.ts @@ -1,12 +1,11 @@ import api from './api'; -import { CartItem } from '../store/cartStore'; export interface OrderItem { id: string; orderId: string; productId: string | null; quantity: number; - product?: any; + product?: Record; } export interface Order { @@ -21,6 +20,14 @@ export interface Order { orderItems: OrderItem[]; } +interface ApiErr { + response?: { + data?: { + message?: string | string[]; + }; + }; +} + export class OrderService { private static instance: OrderService; @@ -50,8 +57,9 @@ export class OrderService { try { const response = await api.post('/orders', orderData); return response.data; - } catch (error: any) { - const message = error.response?.data?.message || 'خطا در ثبت سفارش'; + } catch (error) { + const err = error as ApiErr; + const message = err.response?.data?.message || 'خطا در ثبت سفارش'; throw new Error(Array.isArray(message) ? message[0] : message); } } @@ -63,8 +71,9 @@ export class OrderService { try { const response = await api.get('/orders'); return response.data.data; - } catch (error: any) { - const message = error.response?.data?.message || 'خطا در دریافت لیست سفارش‌ها'; + } catch (error) { + const err = error as ApiErr; + const message = err.response?.data?.message || 'خطا در دریافت لیست سفارش‌ها'; throw new Error(Array.isArray(message) ? message[0] : message); } } @@ -76,8 +85,9 @@ export class OrderService { try { const response = await api.get(`/orders/${id}`); return response.data; - } catch (error: any) { - const message = error.response?.data?.message || 'خطا در دریافت جزئیات سفارش'; + } catch (error) { + const err = error as ApiErr; + const message = err.response?.data?.message || 'خطا در دریافت جزئیات سفارش'; throw new Error(Array.isArray(message) ? message[0] : message); } } diff --git a/frontend/application/lib/services/productService.ts b/frontend/application/lib/services/productService.ts index e429d18..36d284a 100644 --- a/frontend/application/lib/services/productService.ts +++ b/frontend/application/lib/services/productService.ts @@ -23,18 +23,19 @@ export class ProductService { return ProductService.instance; } - private mapBackendToFrontend(data: any): Product { + private mapBackendToFrontend(inputData: Record): Product { + const data = inputData as any; // Find the local static product to inherit functions like calculateDosage const local = PRODUCTS.find(p => p.artNo === data.artNo || p.id === data.productGroup || - (data.slug && data.slug.includes(p.id)) + (typeof data.slug === 'string' && data.slug.includes(p.id)) ); - const safeParse = (val: any, fallback: any) => { + const safeParse = (val: unknown, fallback: unknown) => { if (!val) return fallback; if (typeof val === 'string') { - try { return JSON.parse(val); } catch (e) { return fallback; } + try { return JSON.parse(val); } catch { return fallback; } } return val; }; @@ -68,8 +69,8 @@ export class ProductService { ? data.imageUrl : (local?.image || (data.imageUrl ? `/api${data.imageUrl}` : data.image || '')), - main_ingredients: data.ingredientList?.map((i: any) => i.ingredient) || data.ingredients?.split(/[،,-]/).map((s: string) => s.trim()).filter(Boolean) || [], - symptoms: data.symptoms?.map((s: any) => s.symptom || s) || [], + main_ingredients: data.ingredientList?.map((i: { ingredient: string }) => i.ingredient) || data.ingredients?.split(/[،,-]/).map((s: string) => s.trim()).filter(Boolean) || [], + symptoms: data.symptoms?.map((s: { symptom?: string }) => s.symptom || s) || [], keyBenefits: safeParse(data.keyBenefits, []), expectedResults: safeParse(data.expectedResults, []), benefitsList: safeParse(data.benefitsList, []), @@ -102,7 +103,7 @@ export class ProductService { try { const response = await api.get(`/products?${params.toString()}`); return { - data: response.data.data.map((item: any) => this.mapBackendToFrontend(item)), + data: response.data.data.map((item: Record) => this.mapBackendToFrontend(item)), meta: response.data.meta, }; } catch (error) { @@ -134,7 +135,7 @@ export class ProductService { public async getFeaturedProducts(): Promise { try { const response = await api.get('/products?limit=30'); - const all = response.data.data.map((item: any) => this.mapBackendToFrontend(item)); + const all = response.data.data.map((item: Record) => this.mapBackendToFrontend(item)); const featured: Product[] = []; const categoriesSeen = new Set(); diff --git a/frontend/application/lib/store/__tests__/cartStore.test.ts b/frontend/application/lib/store/__tests__/cartStore.test.ts index 9aa1012..f3cd22a 100644 --- a/frontend/application/lib/store/__tests__/cartStore.test.ts +++ b/frontend/application/lib/store/__tests__/cartStore.test.ts @@ -32,11 +32,11 @@ const mockProduct = { dosage_logic: '1', benefits: '', symptoms: [], - suitableFor: 'سگ' as any, + suitableFor: 'سگ' as never, calculateDosage: () => ({ quantity: 1, unit: 'tablet', description: '' }), analysis: {}, feedingAdvice: '', - specialist: null as any, + specialist: null as never, image: '', }; diff --git a/frontend/application/lib/store/cartStore.ts b/frontend/application/lib/store/cartStore.ts index 9afa28f..539d7ff 100644 --- a/frontend/application/lib/store/cartStore.ts +++ b/frontend/application/lib/store/cartStore.ts @@ -44,7 +44,7 @@ interface CartStore { addOrder: (orderData: Omit) => Promise; clearCart: () => void; removeCoupon: () => void; - setOrders: (orders: any[]) => void; + setOrders: (orders: Record[]) => void; getTotalItems: () => number; getSubtotal: () => number; getDiscount: () => number; @@ -120,6 +120,7 @@ export const useCartStore = create()( try { const backendOrder = await orderService.createOrder(payload); + const bo = backendOrder as unknown as Record; const newOrder: Order = { ...orderData, @@ -127,15 +128,15 @@ export const useCartStore = create()( date: backendOrder.createdAt, total: Number(backendOrder.totalAmount), charityDonation: Number(backendOrder.charityDonation), - status: backendOrder.status as any, - isRefill: (backendOrder as any).isRefill !== undefined ? Boolean((backendOrder as any).isRefill) : Boolean(orderData.isRefill || get().isSubscribed), - paymentMethod: (backendOrder as any).paymentMethod || orderData.paymentMethod, + status: (['processing', 'shipped', 'delivered'].includes(backendOrder.status) ? backendOrder.status : 'processing') as Order['status'], + isRefill: bo.isRefill !== undefined ? Boolean(bo.isRefill) : Boolean(orderData.isRefill || get().isSubscribed), + paymentMethod: (bo.paymentMethod as string) || orderData.paymentMethod, trackingNumber: backendOrder.trackingNumber || `CN-${Math.floor(Math.random() * 90000) + 10000}` }; set({ orders: [newOrder, ...get().orders] }); return backendOrder.id; - } catch (error: any) { + } catch (error) { console.error("Order creation failed on backend:", error); throw error; } @@ -144,20 +145,20 @@ export const useCartStore = create()( removeCoupon: () => set({ coupon: null }), setOrders: (backendOrders) => { const mappedOrders: Order[] = backendOrders.map(bo => ({ - id: bo.id, - date: bo.createdAt, - items: bo.orderItems?.map((oi: any) => ({ + id: String(bo.id), + date: String(bo.createdAt), + items: (bo.orderItems as Array<{ product: Product; quantity: number }>)?.map((oi) => ({ product: oi.product, quantity: oi.quantity })) || [], total: Number(bo.totalAmount), charityDonation: Number(bo.charityDonation), - status: bo.status, - isRefill: Boolean((bo as any).isRefill), - paymentMethod: (bo as any).paymentMethod, - petId: bo.petId || bo.pet?.id, - shippingAddress: bo.shippingAddress, - trackingNumber: bo.trackingNumber || `CN-${Math.floor(Math.random() * 90000) + 10000}` + status: (['processing', 'shipped', 'delivered'].includes(String(bo.status)) ? bo.status : 'processing') as Order['status'], + isRefill: Boolean(bo.isRefill), + paymentMethod: bo.paymentMethod as string | undefined, + petId: (bo.petId as string) || ((bo.pet as { id?: string })?.id), + shippingAddress: bo.shippingAddress as string | undefined, + trackingNumber: (bo.trackingNumber as string) || `CN-${Math.floor(Math.random() * 90000) + 10000}` })); set({ orders: mappedOrders }); }, diff --git a/frontend/application/lib/store/uiStore.ts b/frontend/application/lib/store/uiStore.ts index e243f70..fa3afe1 100644 --- a/frontend/application/lib/store/uiStore.ts +++ b/frontend/application/lib/store/uiStore.ts @@ -4,9 +4,9 @@ interface UIState { isCartOpen: boolean; isLoginModalOpen: boolean; isB2BPortalOpen: boolean; - advisorData: any; + advisorData: Record | null; setCartOpen: (open: boolean) => void; - setLoginModalOpen: (open: boolean, advisorData?: any) => void; + setLoginModalOpen: (open: boolean, advisorData?: Record | null) => void; setB2BPortalOpen: (open: boolean) => void; clearAdvisorData: () => void; } diff --git a/frontend/application/lib/store/usePetStore.ts b/frontend/application/lib/store/usePetStore.ts index cb25a52..442dcaf 100644 --- a/frontend/application/lib/store/usePetStore.ts +++ b/frontend/application/lib/store/usePetStore.ts @@ -3,6 +3,7 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; import api from "../services/api"; import { toast } from "sonner"; +import { useUserStore } from "./userStore"; export interface Reminder { id: string; @@ -54,7 +55,7 @@ interface PetStore { addReminder: (petId: string, reminder: Omit) => Promise; toggleReminder: (petId: string, reminderId: string, date: string) => Promise; addHealthLog: (petId: string, log: Omit) => Promise; - setPets: (pets: any[]) => void; + setPets: (pets: Record[]) => void; reset: () => void; } @@ -125,7 +126,6 @@ export const usePetStore = create()( getActivePet: () => { const { pets, activePetId } = get(); try { - const { useUserStore } = require("./userStore"); if (!useUserStore.getState().isLoggedIn) { return null; } @@ -195,29 +195,29 @@ export const usePetStore = create()( const mappedPets: PetProfile[] = backendPets.map(bp => { const existingLocal = currentLocalPets.find(lp => lp.id === bp.id); return { - id: bp.id, - name: bp.name, - type: bp.type as any, - breed: bp.breed, - age: Number(bp.age), - weight: Number(bp.weight), - activityLevel: bp.activityLevel as any, - medicalConditions: bp.medicalConditions?.map((mc: any) => mc.condition) || [], - image: bp.imageUrl || undefined, - reminders: bp.reminders?.map((r: any) => ({ + id: String(bp.id), + name: String(bp.name), + type: (bp.type as PetProfile['type']) || "سگ", + breed: String(bp.breed || ''), + age: Number(bp.age || 0), + weight: Number(bp.weight || 0), + activityLevel: (bp.activityLevel as PetProfile['activityLevel']) || "متوسط", + medicalConditions: (bp.medicalConditions as Array<{ condition: string }>)?.map((mc) => mc.condition) || [], + image: (bp.imageUrl as string) || undefined, + reminders: (bp.reminders as Array<{ id: string; title: string; time: string; frequency: Reminder['frequency']; productId?: string; completions?: Array<{ completedDate: string }> }>)?.map((r) => ({ id: r.id, title: r.title, time: r.time, - frequency: r.frequency as any, + frequency: r.frequency, productId: r.productId, - completedDates: r.completions?.map((c: any) => c.completedDate) || [] + completedDates: r.completions?.map((c) => c.completedDate) || [] })) || [], - logs: bp.healthLogs?.map((hl: any) => ({ + logs: (bp.healthLogs as Array<{ id: string; loggedDate: string; appetite: HealthLog['appetite']; energy: HealthLog['energy']; digestion: HealthLog['digestion']; note?: string }>)?.map((hl) => ({ id: hl.id, date: hl.loggedDate, - appetite: hl.appetite as any, - energy: hl.energy as any, - digestion: hl.digestion as any, + appetite: hl.appetite, + energy: hl.energy, + digestion: hl.digestion, note: hl.note || undefined })) || [], consumptions: existingLocal?.consumptions || [] diff --git a/frontend/application/lib/store/userStore.ts b/frontend/application/lib/store/userStore.ts index 7d813c5..ac45baf 100644 --- a/frontend/application/lib/store/userStore.ts +++ b/frontend/application/lib/store/userStore.ts @@ -94,7 +94,16 @@ export const useUserStore = create()( addAddress: async (address) => { try { // Remove ID so backend generates UUID - const { id, ...addressData } = address; + const addressData = { + title: address.title, + receptorName: address.receptorName, + phone: address.phone, + province: address.province, + city: address.city, + detail: address.detail, + zipCode: address.zipCode, + isDefault: address.isDefault, + }; await api.post('/users/addresses', addressData); await useUserStore.getState().fetchProfile(); } catch (error) { @@ -104,7 +113,16 @@ export const useUserStore = create()( }, updateAddress: async (id, updated) => { try { - const { id: _, ...addressData } = updated; + const addressData = { + title: updated.title, + receptorName: updated.receptorName, + phone: updated.phone, + province: updated.province, + city: updated.city, + detail: updated.detail, + zipCode: updated.zipCode, + isDefault: updated.isDefault, + }; await api.patch(`/users/addresses/${id}`, addressData); await useUserStore.getState().fetchProfile(); } catch (error) { @@ -201,14 +219,15 @@ export const useUserStore = create()( return; } set((state) => { + const pd = profileData as unknown as Record; const backendWallet = Number(profileData.walletBalance || 0); const backendCharity = Number(profileData.charityDonationTotal || 0); - const backendTransactions = (profileData as any).walletTransactions?.map((t: any) => ({ + const backendTransactions = (pd.walletTransactions as Array<{ id: string; type: string; amount: number; createdAt: string; status: string }>)?.map((t) => ({ id: t.id, - type: t.type === 'deposit' ? 'top_up' : 'purchase', + type: (t.type === 'deposit' ? 'top_up' : 'purchase') as Transaction['type'], amount: Number(t.amount), date: t.createdAt, - status: t.status === 'completed' ? 'success' : t.status === 'failed' ? 'failed' : 'pending' + status: (t.status === 'completed' ? 'success' : t.status === 'failed' ? 'failed' : 'pending') as Transaction['status'] })) || []; return { @@ -221,7 +240,7 @@ export const useUserStore = create()( mobile: profileData.mobile || "", walletBalance: backendWallet, charityDonationTotal: backendCharity, - addresses: (profileData as any).addresses?.length > 0 ? (profileData as any).addresses : state.profile.addresses, + addresses: Array.isArray(pd.addresses) && pd.addresses.length > 0 ? (pd.addresses as Address[]) : state.profile.addresses, transactions: backendTransactions.length > 0 ? backendTransactions : state.profile.transactions } }; @@ -230,11 +249,11 @@ export const useUserStore = create()( // Sync orders list to CartStore while preserving local petId & address mappings if (profileData.orders) { const currentLocalOrders = useCartStore.getState().orders; - const mergedOrders = profileData.orders.map((bo: any) => { + const mergedOrders = (profileData.orders as Record[]).map((bo) => { const localMatch = currentLocalOrders.find(lo => lo.id === bo.id); return { ...bo, - petId: bo.petId || bo.pet?.id || localMatch?.petId, + petId: bo.petId || (bo.pet as { id?: string })?.id || localMatch?.petId, shippingAddress: bo.shippingAddress || localMatch?.shippingAddress }; }); @@ -242,7 +261,7 @@ export const useUserStore = create()( } // Sync pets list to PetStore if (profileData.pets) { - usePetStore.getState().setPets(profileData.pets); + usePetStore.getState().setPets(profileData.pets as Record[]); } } catch (error) { console.error("Failed to fetch user profile:", error); diff --git a/frontend/application/lib/utils.ts b/frontend/application/lib/utils.ts index c1d3216..a94c4b1 100644 --- a/frontend/application/lib/utils.ts +++ b/frontend/application/lib/utils.ts @@ -3,6 +3,12 @@ export const toPersian = (n: number | string | undefined | null) => { return n.toString().replace(/\d/g, d => '۰۱۲۳۴۵۶۷۸۹'[parseInt(d)]); }; +export const toEnglishDigits = (str: string) => { + if (!str) return ""; + return str.replace(/[۰-۹]/g, d => '۰۱۲۳۴۵۶۷۸۹'.indexOf(d).toString()); +}; + export function cn(...classes: (string | boolean | undefined)[]) { return classes.filter(Boolean).join(" "); } +