feat(seller): multi-user seller dashboard (owner + staff) #2
@ -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" };
|
||||
}
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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<string | null>(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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user