From 3502168a8b17aa20ed377fcde0595c8b64451603 Mon Sep 17 00:00:00 2001 From: Arda Samadi Date: Wed, 1 Jul 2026 16:00:23 +0330 Subject: [PATCH 1/3] feat(seller-team): split buyer inbox vs store inbox in chat UI Seller dashboard threads (type="store") now fetch the store's inbox via useGetStoreThreads(storeId) -> /threads/list/?store_id=; the profile threads page (type="user") keeps the personal buyer inbox. Depends on the seller-team backend (store-aware threads + store_id-by-username), so it ships on this branch, not main. Co-Authored-By: Claude Opus 4.8 --- app/components/thread/ThreadsPage.tsx | 16 ++++++++++++++-- app/requestHandler/use-chat-hooks.tsx | 24 ++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/app/components/thread/ThreadsPage.tsx b/app/components/thread/ThreadsPage.tsx index c207772..eb949c3 100644 --- a/app/components/thread/ThreadsPage.tsx +++ b/app/components/thread/ThreadsPage.tsx @@ -1,7 +1,10 @@ import { Link } from "@remix-run/react"; import AppInput from "../AppInput"; import { useState } from "react"; -import { useGetUserThreads } from "../../requestHandler/use-chat-hooks"; +import { + useGetUserThreads, + useGetStoreThreads, +} from "../../requestHandler/use-chat-hooks"; import { ThreadList, ThreadParticipant, @@ -20,7 +23,16 @@ interface ThreadsPageProps { export default function ThreadsPage({ type, storeId }: ThreadsPageProps) { const [searchValue, setSearchValue] = useState(""); - const { data: threads, isLoading, isError, refetch } = useGetUserThreads(); + // Seller dashboard shows the STORE's inbox; the profile page shows the user's + // personal (buyer) chats. Two different endpoints so the two never mix. + const userThreads = useGetUserThreads(); + const storeThreads = useGetStoreThreads(type === "store" ? storeId : undefined); + const { + data: threads, + isLoading, + isError, + refetch, + } = type === "store" ? storeThreads : userThreads; const sortedThreads = threads?.sort((a, b) => { const dateA = new Date(a.lastMessageTime || "2025-01-18T00:00:00.000Z"); const dateB = new Date(b.lastMessageTime || "2025-01-18T00:00:00.000Z"); diff --git a/app/requestHandler/use-chat-hooks.tsx b/app/requestHandler/use-chat-hooks.tsx index 5425ea4..a92b739 100644 --- a/app/requestHandler/use-chat-hooks.tsx +++ b/app/requestHandler/use-chat-hooks.tsx @@ -31,6 +31,30 @@ export function useGetUserThreads() { }); } +/** + * List a STORE's inbox (owner + staff), scoped to the store's own threads. + * Distinct from useGetUserThreads(), which returns the user's personal buyer chats. + */ +export function useGetStoreThreads(storeId?: string) { + const token = useAuthToken(); + + return useQuery({ + queryKey: ["getStoreThreads", storeId], + queryFn: async () => { + if (!token) { + throw new Error("Authentication token is required"); + } + const chatApi = createChatApi(token); + return await chatApi.apiChatV1ThreadsListList({ storeId }); + }, + enabled: !!token && !!storeId, + retry: 1, + retryDelay: 3000, + refetchOnWindowFocus: false, + refetchInterval: 20 * 1000, + }); +} + /** * Server-side function to get thread details */ From a642fc0faeaf5446a91e9d97bba5d03ebfbd82d2 Mon Sep 17 00:00:00 2001 From: Arda Samadi Date: Wed, 1 Jul 2026 16:43:53 +0330 Subject: [PATCH 2/3] =?UTF-8?q?feat(seller-team):=20multi-user=20dashboard?= =?UTF-8?q?=20=E2=80=94=20store=20switcher,=20role=20gating,=20team=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useMyStores + team hooks (invite by phone / list / remove) in use-seller-hooks. - useStoreOwnership rewritten to allow staff (not just the owner) via my-stores; exposes `role` so callers can gate owner-only UI, and no longer redirects members away from a store they help manage. - Store settings: owner-only items (financial, shipping, edit store, instagram sync, team) hidden from staff; new "مدیریت اعضای تیم" entry (owner-only). - New /store/:storeId/team page: invite by phone + members list with pending/ active status + remove (owner-only, redirects staff). MyLayout keeps store mode on the team route. - StoreForm edit is owner-only (redirects staff). Profile "enter my store" now routes owners to their store and staff to the store they manage. - Chat already split earlier: seller inbox vs personal buyer inbox. Depends on the seller-team backend; ships on this branch, not main. Co-Authored-By: Claude Opus 4.8 --- app/components/MyLayout.tsx | 1 + app/components/store/StoreForm.tsx | 9 +- app/hooks/useStoreOwnership.ts | 54 ++++---- app/requestHandler/use-seller-hooks.ts | 100 +++++++++++++ app/routes/profile._index.tsx | 16 ++- app/routes/store.$storeId.setting.tsx | 25 +++- app/routes/store.$storeId.team.tsx | 185 +++++++++++++++++++++++++ 7 files changed, 350 insertions(+), 40 deletions(-) create mode 100644 app/routes/store.$storeId.team.tsx diff --git a/app/components/MyLayout.tsx b/app/components/MyLayout.tsx index 8454816..d7354af 100644 --- a/app/components/MyLayout.tsx +++ b/app/components/MyLayout.tsx @@ -139,6 +139,7 @@ const MobileBottomNavigation = ({ path.match(/^\/store\/[^/]+\/products$/) || // Products list: /store/{id}/products path.match(/^\/store\/[^/]+\/product\/[^/]+$/) || // Product detail: /store/{id}/products/{id} path.match(/^\/store\/[^/]+\/edit$/) || // Edit store: /store/{id}/edit + path.match(/^\/store\/[^/]+\/team$/) || // Team management: /store/{id}/team path.match(/^\/store\/add\/manual\/[^/]+$/) || // Add product: /store/add/manual/{id} path.match(/^\/store\/create$/) || // Create store: /store/create path.match(/^\/store\/authenticate\/instagram\/[^/]+$/) || // Instagram auth: /store/authenticate/instagram/{id} diff --git a/app/components/store/StoreForm.tsx b/app/components/store/StoreForm.tsx index 79739c1..12b4f13 100644 --- a/app/components/store/StoreForm.tsx +++ b/app/components/store/StoreForm.tsx @@ -116,11 +116,18 @@ export default function StoreForm({ mode, storeId }: StoreFormProps) { ); // Check store ownership for edit mode - const { isLoading: isOwnershipLoading } = useStoreOwnership( + const { isLoading: isOwnershipLoading, role } = useStoreOwnership( mode === "edit" ? storeId : undefined, mode ); + // Editing store info is owner-only; send staff back to the dashboard. + useEffect(() => { + if (mode === "edit" && !isOwnershipLoading && role && role !== "owner") { + navigate(`/store/${storeId}`); + } + }, [mode, isOwnershipLoading, role, storeId, navigate]); + // Form state const [selectedImage, setSelectedImage] = useState(null); const [imagePreviewUrl, setImagePreviewUrl] = useState(null); diff --git a/app/hooks/useStoreOwnership.ts b/app/hooks/useStoreOwnership.ts index 29867aa..48d10ad 100644 --- a/app/hooks/useStoreOwnership.ts +++ b/app/hooks/useStoreOwnership.ts @@ -1,46 +1,40 @@ import { useEffect } from "react"; import { useNavigate } from "@remix-run/react"; -import { useServiceGetProfile } from "../requestHandler/use-profile-hooks"; +import { useMyStores } from "../requestHandler/use-seller-hooks"; +/** + * Guards a store dashboard route. A user may enter a store they OWN or are an + * active STAFF member of. Non-members are redirected away. Returns the user's + * role for this store so callers can gate owner-only UI. + */ export function useStoreOwnership(storeId: string | undefined, mode?: string) { const navigate = useNavigate(); - const { data: profileData, isLoading } = useServiceGetProfile(); + const { data: stores, isLoading } = useMyStores(); + + const current = stores?.find((s) => s.username === storeId); + useEffect(() => { - // Wait for profile data to load - if (isLoading) return; - - // If no storeId provided, redirect to home - if (!storeId && mode !== "create") { + if (isLoading || mode === "create") return; + if (!storeId) { navigate("/"); return; } - - // If user doesn't have profile data, redirect to home - if (!profileData) { - navigate("/"); - return; + if (stores === undefined) return; // not loaded yet + if (!current) { + // Not a member of this store → send them to a store they can manage, else home. + if (stores.length > 0) navigate(`/store/${stores[0].username}`); + else navigate("/"); } + }, [stores, current, storeId, isLoading, navigate, mode]); - // If user doesn't have a seller account, redirect to home - if (!profileData.sellerUsername && mode !== "create") { - navigate("/"); - return; - } - - // If the storeId doesn't match the user's seller username, redirect to their own store - if ( - profileData.sellerUsername !== storeId && - mode !== "create" && - storeId - ) { - navigate(`/store/${profileData.sellerUsername}`); - return; - } - }, [profileData, storeId, isLoading, navigate, mode]); + const ownedOrFirst = stores?.find((s) => s.role === "owner") ?? stores?.[0]; return { - isOwner: profileData?.sellerUsername === storeId, isLoading, - userStoreId: profileData?.sellerUsername, + isMember: !!current, + role: current?.role ?? null, + isOwner: current?.role === "owner", + userStoreId: ownedOrFirst?.username, + stores: stores ?? [], }; } diff --git a/app/requestHandler/use-seller-hooks.ts b/app/requestHandler/use-seller-hooks.ts index e6f62d1..8cf39a9 100644 --- a/app/requestHandler/use-seller-hooks.ts +++ b/app/requestHandler/use-seller-hooks.ts @@ -15,6 +15,106 @@ import { createFollowsManagementApi, createSellerManagementApi, } from "~/utils/api-client-factory"; +import { getApiBaseUrl } from "../utils/api-config"; + +// ── Multi-user team ───────────────────────────────────────────────────────── +export interface MyStore { + username: string; + storeName: string; + storeLogo: string | null; + role: "owner" | "staff" | null; +} + +export interface StoreMember { + id: string; + userId: string | null; + name: string | null; + phone: string; + role: string; + status: "active" | "pending"; + createdAt: string; +} + +/** Stores the current user can manage (owned + memberships), with their role. */ +export function useMyStores() { + const token = useAuthToken(); + return useQuery({ + queryKey: ["myStores"], + queryFn: async (): Promise => { + if (!token) return []; + const res = await fetch(`${getApiBaseUrl()}/api/seller/my-stores/`, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (!res.ok) return []; + return res.json(); + }, + enabled: !!token, + staleTime: 60 * 1000, + }); +} + +/** A store's team members (owner only). */ +export function useStoreMembers(username?: string) { + const token = useAuthToken(); + return useQuery({ + queryKey: ["storeMembers", username], + queryFn: async (): Promise => { + const res = await fetch( + `${getApiBaseUrl()}/api/seller/members/${username}/`, + { headers: { Authorization: `Bearer ${token}` } } + ); + if (!res.ok) throw new Error("failed to load members"); + return res.json(); + }, + enabled: !!token && !!username, + }); +} + +/** Invite a team member by phone number (owner only). */ +export function useInviteMember(username?: string) { + const token = useAuthToken(); + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (phone: string) => { + const res = await fetch( + `${getApiBaseUrl()}/api/seller/members/${username}/`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ phone }), + } + ); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err.error || "خطا در دعوت عضو جدید"); + } + return res.json(); + }, + onSuccess: () => + queryClient.invalidateQueries({ queryKey: ["storeMembers", username] }), + }); +} + +/** Remove a team member (owner only). */ +export function useRemoveMember(username?: string) { + const token = useAuthToken(); + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (memberId: string) => { + const res = await fetch( + `${getApiBaseUrl()}/api/seller/members/${username}/${memberId}/`, + { method: "DELETE", headers: { Authorization: `Bearer ${token}` } } + ); + if (!res.ok) throw new Error("خطا در حذف عضو"); + return res.json(); + }, + onSuccess: () => + queryClient.invalidateQueries({ queryKey: ["storeMembers", username] }), + }); +} /** * Custom hook to fetch seller data by sellerId using React Query diff --git a/app/routes/profile._index.tsx b/app/routes/profile._index.tsx index 1216d85..bdbe81d 100644 --- a/app/routes/profile._index.tsx +++ b/app/routes/profile._index.tsx @@ -19,6 +19,7 @@ import { } from "lucide-react"; import { Link, useNavigate } from "@remix-run/react"; import { useServiceGetProfile } from "~/utils/RequestHandler"; +import { useMyStores } from "~/requestHandler/use-seller-hooks"; import { useServiceGetWallet } from "~/requestHandler/use-wallet-hooks"; import { useBadgeCounts } from "~/hooks/useBadgeCounts"; import { Skeleton } from "~/components/ui/skeleton"; @@ -57,6 +58,7 @@ export default function Profile() { const [loginModalOpen, setLoginModalOpen] = useState(false); const [logoutModalOpen, setLogoutModalOpen] = useState(false); const { data: profileData } = useServiceGetProfile(); + const { data: myStores } = useMyStores(); const { data: walletData } = useServiceGetWallet(); const { data: badgeCounts } = useBadgeCounts(); const { data: followedSellers, isLoading: isFollowedSellersLoading } = @@ -269,14 +271,18 @@ export default function Profile() { !quickPages.has(m.pageName))} onStoreClick={() => { + // Enter a store the user can manage: their own if they own one, + // otherwise the first store they're a staff member of. + const owned = myStores?.find((s) => s.role === "owner"); + const target = owned || myStores?.[0]; if (profileData?.sellerUsername) { navigate(`/store/${profileData.sellerUsername}`); + } else if (target) { + navigate(`/store/${target.username}`); + } else if (user?.access_token) { + setStoreModalOpen(true); } else { - if (user?.access_token) { - setStoreModalOpen(true); - } else { - setLoginModalOpen(true); - } + setLoginModalOpen(true); } }} onLogoutClick={() => { diff --git a/app/routes/store.$storeId.setting.tsx b/app/routes/store.$storeId.setting.tsx index d903acd..53f43e5 100644 --- a/app/routes/store.$storeId.setting.tsx +++ b/app/routes/store.$storeId.setting.tsx @@ -12,6 +12,7 @@ import { Code, Instagram, MessageCircle, + Users, } from "lucide-react"; import HeaderWithSupport from "../components/HeaderWithSupport"; import { useStoreOwnership } from "../hooks/useStoreOwnership"; @@ -29,6 +30,7 @@ interface MenuItemType { link?: string; pageName: string; hasBorder?: boolean; + ownerOnly?: boolean; image: React.ReactNode; } @@ -38,8 +40,8 @@ export default function StoreSetting() { const { data } = useSellerData(storeId); const { theme: initialTheme } = useRootData(); - // Check store ownership - this will redirect if user doesn't own the store - const { isLoading: isOwnershipLoading } = useStoreOwnership(storeId); + // Store access guard (owner or staff). role gates owner-only menu items. + const { isLoading: isOwnershipLoading, role } = useStoreOwnership(storeId); // State for Instagram sync drawer const [instagramSyncDrawerOpen, setInstagramSyncDrawerOpen] = useState(false); @@ -54,12 +56,13 @@ export default function StoreSetting() { ); } - // Menu items for store management - const storeMenuItems: MenuItemType[] = [ + // Menu items for store management. `ownerOnly` items are hidden from staff. + const allStoreMenuItems: MenuItemType[] = [ { title: "وارد کردن پست های اینستاگرام", pageName: "instagram-sync", hasBorder: true, + ownerOnly: true, image: , }, { @@ -78,12 +81,14 @@ export default function StoreSetting() { title: "داشبورد مالی", pageName: "financial-dashboard", link: `/store/${storeId}/financial-dashboard`, + ownerOnly: true, image: , }, { title: "تعیین نحوه ارسال", pageName: "shipping-method", link: `/store/${storeId}/shipping-method`, + ownerOnly: true, image: , }, { @@ -98,10 +103,18 @@ export default function StoreSetting() { link: `/store/${storeId}/products`, image: , }, + { + title: "مدیریت اعضای تیم", + pageName: "team", + link: `/store/${storeId}/team`, + ownerOnly: true, + image: , + }, { title: "ویرایش اطلاعات فروشگاه", pageName: "edit-store", link: `/store/${storeId}/edit`, + ownerOnly: true, image: , }, { @@ -113,6 +126,10 @@ export default function StoreSetting() { }, ]; + const storeMenuItems = allStoreMenuItems.filter( + (item) => !item.ownerOnly || role === "owner" + ); + const handleBack = () => { navigate(`/store/${storeId}`); }; diff --git a/app/routes/store.$storeId.team.tsx b/app/routes/store.$storeId.team.tsx new file mode 100644 index 0000000..b74bffa --- /dev/null +++ b/app/routes/store.$storeId.team.tsx @@ -0,0 +1,185 @@ +import { MetaFunction, useNavigate, useParams } from "@remix-run/react"; +import { useEffect, useState } from "react"; +import { Trash2, UserPlus, Loader2 } from "lucide-react"; +import HeaderWithSupport from "../components/HeaderWithSupport"; +import AppInput from "../components/AppInput"; +import { Button } from "../components/ui/button"; +import { useStoreOwnership } from "../hooks/useStoreOwnership"; +import { + useStoreMembers, + useInviteMember, + useRemoveMember, +} from "../requestHandler/use-seller-hooks"; +import { useToast } from "../hooks/use-toast"; +import UiProvider from "../components/UiProvider"; + +export default function StoreTeam() { + const { storeId } = useParams(); + const navigate = useNavigate(); + const { toast } = useToast(); + + // Team management is owner-only. + const { isLoading: isAccessLoading, role } = useStoreOwnership(storeId); + useEffect(() => { + if (!isAccessLoading && role && role !== "owner") { + navigate(`/store/${storeId}`); + } + }, [isAccessLoading, role, storeId, navigate]); + + const { + data: members, + isLoading, + isError, + refetch, + } = useStoreMembers(storeId); + const inviteMember = useInviteMember(storeId); + const removeMember = useRemoveMember(storeId); + + const [phone, setPhone] = useState(""); + + const handleInvite = () => { + const trimmed = phone.trim(); + if (!trimmed) return; + inviteMember.mutate(trimmed, { + onSuccess: () => { + setPhone(""); + toast({ + title: "دعوت انجام شد", + description: "عضو جدید به تیم فروشگاه اضافه شد.", + }); + }, + onError: (e: unknown) => { + toast({ + title: "خطا", + description: e instanceof Error ? e.message : "دعوت عضو ناموفق بود.", + variant: "destructive", + }); + }, + }); + }; + + const handleRemove = (id: string) => { + removeMember.mutate(id, { + onSuccess: () => + toast({ title: "حذف شد", description: "عضو از تیم حذف شد." }), + onError: () => + toast({ + title: "خطا", + description: "حذف عضو ناموفق بود.", + variant: "destructive", + }), + }); + }; + + return ( +
+
+ navigate(`/store/${storeId}/setting`)} + /> +
+

+ مدیریت اعضای تیم +

+ +
+ {/* Invite by phone */} +
+

دعوت عضو جدید با شماره موبایل

+
+
+ setPhone(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") handleInvite(); + }} + /> +
+ +
+

+ عضو دعوت‌شده می‌تواند محصولات، سفارش‌ها و پیام‌های فروشگاه را مدیریت + کند اما به بخش مالی، تنظیمات فروشگاه و مدیریت تیم دسترسی ندارد. اگر + حساب کاربری با این شماره وجود نداشته باشد، دعوت تا اولین ورود او در + حالت «در انتظار» می‌ماند. +

+
+ + {/* Members list */} +
+

اعضای تیم

+ +
+ {members?.map((m) => ( +
+
+

+ {m.name || m.phone} +

+

+ {m.phone} +

+
+
+ {m.status === "pending" ? ( + + در انتظار + + ) : ( + + فعال + + )} + +
+
+ ))} +
+
+
+
+
+ ); +} + +export const meta: MetaFunction = () => [ + { title: "مدیریت اعضای تیم - ویترون" }, + { name: "robots", content: "noindex, nofollow" }, +]; From bb44ebf0e5b2ae705a8b815c8a2f2266ad91d2a9 Mon Sep 17 00:00:00 2001 From: Arda Samadi Date: Wed, 1 Jul 2026 17:04:06 +0330 Subject: [PATCH 3/3] fix(seller-team): guard owner-only routes + robust useMyStores MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review fixes: - useMyStores throws on a failed response instead of resolving to [] — a transient 401/blip no longer looks like "no stores" and bounces an owner off their own dashboard (query stays in error/retry, keeps prior data). - Add useRequireOwner() guard and apply it to the owner-only routes (financial-dashboard + its cash-funds/reports/transactions sub-routes and shipping-method) so staff who hit those URLs directly are redirected to the dashboard. (Financial data was already backend-protected; this is the matching client-side gate.) Co-Authored-By: Claude Opus 4.8 --- app/hooks/useStoreOwnership.ts | 18 ++++++++++++++++++ app/requestHandler/use-seller-hooks.ts | 6 +++++- ...ore.$storeId.financial-dashboard._index.tsx | 2 ++ ...$storeId.financial-dashboard.cash-funds.tsx | 5 ++++- ...re.$storeId.financial-dashboard.reports.tsx | 5 ++++- ...toreId.financial-dashboard.transactions.tsx | 5 ++++- app/routes/store.$storeId.shipping-method.tsx | 6 +++--- 7 files changed, 40 insertions(+), 7 deletions(-) diff --git a/app/hooks/useStoreOwnership.ts b/app/hooks/useStoreOwnership.ts index 48d10ad..09c475b 100644 --- a/app/hooks/useStoreOwnership.ts +++ b/app/hooks/useStoreOwnership.ts @@ -38,3 +38,21 @@ export function useStoreOwnership(storeId: string | undefined, mode?: string) { stores: stores ?? [], }; } + +/** + * Guard for OWNER-ONLY store pages (financial, shipping, edit, team). Redirects + * non-members (via useStoreOwnership) and staff members back to the dashboard. + * Returns { isLoading, isOwner } so callers can render a spinner meanwhile. + */ +export function useRequireOwner(storeId: string | undefined) { + const navigate = useNavigate(); + const { isLoading, role } = useStoreOwnership(storeId); + + useEffect(() => { + if (!isLoading && role && role !== "owner") { + navigate(`/store/${storeId}`); + } + }, [isLoading, role, storeId, navigate]); + + return { isLoading, isOwner: role === "owner" }; +} diff --git a/app/requestHandler/use-seller-hooks.ts b/app/requestHandler/use-seller-hooks.ts index 8cf39a9..c0df4e7 100644 --- a/app/requestHandler/use-seller-hooks.ts +++ b/app/requestHandler/use-seller-hooks.ts @@ -45,10 +45,14 @@ export function useMyStores() { const res = await fetch(`${getApiBaseUrl()}/api/seller/my-stores/`, { headers: { Authorization: `Bearer ${token}` }, }); - if (!res.ok) return []; + // Throw on failure so a transient error stays an ERROR state (data kept, + // retried) instead of resolving to [] — otherwise a blip would look like + // "no stores" and bounce an owner off their own dashboard. + if (!res.ok) throw new Error(`my-stores failed: ${res.status}`); return res.json(); }, enabled: !!token, + retry: 1, staleTime: 60 * 1000, }); } diff --git a/app/routes/store.$storeId.financial-dashboard._index.tsx b/app/routes/store.$storeId.financial-dashboard._index.tsx index ca26018..96fa2a3 100644 --- a/app/routes/store.$storeId.financial-dashboard._index.tsx +++ b/app/routes/store.$storeId.financial-dashboard._index.tsx @@ -8,6 +8,7 @@ import { HandCoins, } from "lucide-react"; import HeaderWithSupport from "../components/HeaderWithSupport"; +import { useRequireOwner } from "../hooks/useStoreOwnership"; export const meta: MetaFunction = () => { return [ @@ -41,6 +42,7 @@ export const meta: MetaFunction = () => { export default function FinancialDashboard() { const { storeId } = useParams(); + useRequireOwner(storeId); // financial is owner-only const navigate = useNavigate(); const handleBack = () => { safeGoBack(navigate); diff --git a/app/routes/store.$storeId.financial-dashboard.cash-funds.tsx b/app/routes/store.$storeId.financial-dashboard.cash-funds.tsx index c89f7ce..f497b22 100644 --- a/app/routes/store.$storeId.financial-dashboard.cash-funds.tsx +++ b/app/routes/store.$storeId.financial-dashboard.cash-funds.tsx @@ -16,8 +16,9 @@ import { CarouselItem, } from "../components/ui/carousel"; import { useCallback, useState, useRef, useEffect } from "react"; -import { useNavigate } from "@remix-run/react"; +import { useNavigate, useParams } from "@remix-run/react"; import { useCarousel } from "../hooks/useCarousel"; +import { useRequireOwner } from "../hooks/useStoreOwnership"; import { Drawer, DrawerTitle, @@ -50,6 +51,8 @@ import { BankLogo } from "~/components/BankLogo"; export default function CashingFunds() { const navigate = useNavigate(); + const { storeId } = useParams(); + useRequireOwner(storeId); // financial is owner-only const handleBack = () => { safeGoBack(navigate); }; diff --git a/app/routes/store.$storeId.financial-dashboard.reports.tsx b/app/routes/store.$storeId.financial-dashboard.reports.tsx index 8c2820b..3f6e8f0 100644 --- a/app/routes/store.$storeId.financial-dashboard.reports.tsx +++ b/app/routes/store.$storeId.financial-dashboard.reports.tsx @@ -2,14 +2,17 @@ import { Sun, Waves, Loader2 } from "lucide-react"; import HeaderWithSupport from "../components/HeaderWithSupport"; import PersianDatePicker from "../components/PersianDatePicker"; import { useEffect, useState } from "react"; -import { useSearchParams, useNavigate } from "react-router-dom"; +import { useSearchParams, useNavigate, useParams } from "react-router-dom"; import { formatPrice, safeGoBack } from "../utils/helpers"; import { useServiceGetReport } from "../requestHandler/use-wallet-hooks"; +import { useRequireOwner } from "../hooks/useStoreOwnership"; import jMoment from "moment-jalaali"; import { MetaFunction } from "@remix-run/node"; export default function Reports() { const navigate = useNavigate(); + const { storeId } = useParams(); + useRequireOwner(storeId); // financial is owner-only const handleBack = () => { safeGoBack(navigate); }; diff --git a/app/routes/store.$storeId.financial-dashboard.transactions.tsx b/app/routes/store.$storeId.financial-dashboard.transactions.tsx index bcf874e..364cbf9 100644 --- a/app/routes/store.$storeId.financial-dashboard.transactions.tsx +++ b/app/routes/store.$storeId.financial-dashboard.transactions.tsx @@ -12,8 +12,9 @@ import { } from "../components/ui/drawer"; import { RadioGroup, RadioGroupItem } from "../components/ui/radio-group"; import PersianDatePicker from "../components/PersianDatePicker"; -import { useSearchParams } from "react-router-dom"; +import { useSearchParams, useParams } from "react-router-dom"; import { useServiceGetPayoutRequests } from "../requestHandler/use-wallet-hooks"; +import { useRequireOwner } from "../hooks/useStoreOwnership"; import jMoment from "moment-jalaali"; import filterIcon from "~/assets/icons/filter.png"; import filterIconDark from "~/assets/icons/filter-dark.png"; @@ -51,6 +52,8 @@ export const meta: MetaFunction = () => { export default function Transactions() { const navigate = useNavigate(); + const { storeId } = useParams(); + useRequireOwner(storeId); // financial is owner-only const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false); const [searchParams] = useSearchParams(); const { theme } = useRootData(); diff --git a/app/routes/store.$storeId.shipping-method.tsx b/app/routes/store.$storeId.shipping-method.tsx index e1e5b41..ff8bc51 100644 --- a/app/routes/store.$storeId.shipping-method.tsx +++ b/app/routes/store.$storeId.shipping-method.tsx @@ -15,7 +15,7 @@ import { import { SellerCourierCreate, SellerCourierUpdate } from "../../src/api/types"; import { useToast } from "../hooks/use-toast"; import { useParams, useNavigate } from "@remix-run/react"; -import { useStoreOwnership } from "../hooks/useStoreOwnership"; +import { useRequireOwner } from "../hooks/useStoreOwnership"; import { MetaFunction } from "@remix-run/node"; import { getPersianTextOfPrice, safeGoBack } from "~/utils/helpers"; @@ -37,8 +37,8 @@ export default function ShippingMethod() { const navigate = useNavigate(); const [savingCourierId, setSavingCourierId] = useState(null); - // Check store ownership - this will redirect if user doesn't own the store - const { isLoading: isOwnershipLoading } = useStoreOwnership(storeId); + // Shipping/courier config is owner-only — redirect staff back to the dashboard. + const { isLoading: isOwnershipLoading } = useRequireOwner(storeId); // Fetch all available couriers const {