Vitron-Front/app/routes/store.$storeId.setting.tsx
Arda Samadi a642fc0fae feat(seller-team): multi-user dashboard — store switcher, role gating, team UI
- 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 <noreply@anthropic.com>
2026-07-01 16:43:53 +03:30

301 lines
11 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { MetaFunction, useNavigate, useParams } from "@remix-run/react";
import { useSellerData } from "../requestHandler/use-seller-hooks";
import {
ChevronLeft,
LogOut,
LayoutDashboard,
ShoppingCart,
Truck,
PackagePlus,
TableProperties,
PenLine,
Code,
Instagram,
MessageCircle,
Users,
} from "lucide-react";
import HeaderWithSupport from "../components/HeaderWithSupport";
import { useStoreOwnership } from "../hooks/useStoreOwnership";
import { useState } from "react";
import { SunMoonToggle } from "~/components/SunMoonToggle";
import { useRootData } from "~/hooks/use-root-data";
import { PushSettingToggle } from "~/components/notifications/PushSettingToggle";
// Import the InstagramSyncDrawer component
import { InstagramSyncDrawer } from "./store.$storeId._index";
import CallDialog from "~/components/CallDialog";
interface MenuItemType {
title: string;
link?: string;
pageName: string;
hasBorder?: boolean;
ownerOnly?: boolean;
image: React.ReactNode;
}
export default function StoreSetting() {
const { storeId } = useParams();
const navigate = useNavigate();
const { data } = useSellerData(storeId);
const { theme: initialTheme } = useRootData();
// 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);
const [isCallModalOpen, setIsCallModalOpen] = useState(false);
// Show loading while checking ownership
if (isOwnershipLoading) {
return (
<div className="flex items-center justify-center min-h-[500px]">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
);
}
// Menu items for store management. `ownerOnly` items are hidden from staff.
const allStoreMenuItems: MenuItemType[] = [
{
title: "وارد کردن پست های اینستاگرام",
pageName: "instagram-sync",
hasBorder: true,
ownerOnly: true,
image: <Instagram size={20} />,
},
{
title: "سفارش‌های دریافتی",
pageName: "seller-orders",
link: `/store/${storeId}/orders`,
image: <ShoppingCart size={20} />,
},
{
title: "پیام های دریافتی",
pageName: "seller-messages",
link: `/store/${storeId}/threads`,
image: <MessageCircle size={20} />,
},
{
title: "داشبورد مالی",
pageName: "financial-dashboard",
link: `/store/${storeId}/financial-dashboard`,
ownerOnly: true,
image: <LayoutDashboard size={20} />,
},
{
title: "تعیین نحوه ارسال",
pageName: "shipping-method",
link: `/store/${storeId}/shipping-method`,
ownerOnly: true,
image: <Truck size={20} />,
},
{
title: "افزودن محصول جدید",
pageName: "add-product",
link: `/store/add/manual/${storeId}`,
image: <PackagePlus size={20} />,
},
{
title: "لیست محصولات",
pageName: "products-list",
link: `/store/${storeId}/products`,
image: <TableProperties size={20} />,
},
{
title: "مدیریت اعضای تیم",
pageName: "team",
link: `/store/${storeId}/team`,
ownerOnly: true,
image: <Users size={20} />,
},
{
title: "ویرایش اطلاعات فروشگاه",
pageName: "edit-store",
link: `/store/${storeId}/edit`,
ownerOnly: true,
image: <PenLine size={20} />,
},
{
title: "خروج",
pageName: "logout",
link: `/store/${storeId}/logout`,
hasBorder: true,
image: <LogOut size={20} />,
},
];
const storeMenuItems = allStoreMenuItems.filter(
(item) => !item.ownerOnly || role === "owner"
);
const handleBack = () => {
navigate(`/store/${storeId}`);
};
const handleItemClick = (item: MenuItemType) => {
// Special handling for "instagram-sync" item
if (item.pageName === "instagram-sync") {
// Open Instagram sync drawer instead of navigating
setInstagramSyncDrawerOpen(true);
return;
}
// Special handling for "logout" item
if (item.pageName === "logout") {
// Handle logout logic
navigate("/");
return;
}
// Default rendering for other items with Link if they have a link
if (item.link) {
navigate(item.link);
return;
}
};
return (
<div className="flex flex-col bg-WHITE">
{/* Header */}
<div className="lg:hidden">
<HeaderWithSupport title="تنظیمات فروشگاه" onBack={handleBack} />
</div>
<h2 className="hidden lg:block text-[24px] font-extrabold mb-6">
تنظیمات فروشگاه
</h2>
{/* Status + Theme cards */}
<div className="lg:grid lg:grid-cols-3 lg:gap-4 lg:mb-6">
{/* Store Status */}
<div className="p-4 bg-WHITE border-b border-inner-border lg:border lg:border-WHITE3 lg:rounded-2xl lg:p-6">
<div className="flex w-full items-center justify-between">
<div className="flex flex-col gap-1 w-full">
<div className="flex w-full gap-1 items-center">
<Code size={20} />
<p className="text-R12 lg:text-B16 font-bold">
وضعیت فروشگاه (فعال)
</p>
</div>
<p className="text-R10 lg:text-R12 text-GRAY">
در حالت فعال، فروشگاه برای مخاطبان نمایش داده میشود.
</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={data?.isVerified || false}
className="sr-only peer"
aria-label="وضعیت فروشگاه"
/>
<div className="w-11 h-6 bg-WHITE2 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-WHITE after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-WHITE after:border-GRAY3 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-VITROWN_BLUE border"></div>
</label>
</div>
</div>
{/* Theme Toggle */}
<div className="p-4 bg-WHITE border-b border-inner-border lg:border lg:border-WHITE3 lg:rounded-2xl lg:p-6">
<div className="flex w-full items-center justify-between">
<div className="flex flex-col gap-1">
<p className="text-R12 lg:text-B16 font-bold">
{initialTheme === "dark" ? "حالت تاریک" : "حالت روشن"}
</p>
<p className="text-R10 lg:text-R12 text-GRAY">
تغییر حالت نمایش به تاریک یا روشن
</p>
</div>
<SunMoonToggle theme={initialTheme} />
</div>
</div>
{/* Push notifications */}
<PushSettingToggle className="p-4 bg-WHITE border-b border-inner-border lg:border lg:border-WHITE3 lg:rounded-2xl lg:p-6" />
</div>
{/* Menu Items */}
<div className="flex flex-col gap-0 w-full bg-WHITE lg:grid lg:grid-cols-3 lg:gap-4">
{storeMenuItems.map((item, index) => {
const isLogout = item.pageName === "logout";
return (
<div
key={index}
className={`flex flex-row gap-2 items-center justify-between px-[16px] active:bg-hover transition cursor-pointer ${
item.hasBorder && "border-t-[2px]"
} h-[55px] w-full lg:h-auto lg:px-0 lg:border-t-0 lg:border lg:border-WHITE3 lg:rounded-2xl lg:p-5 lg:items-center lg:hover:shadow-md lg:transition-shadow ${
isLogout ? "lg:border-RED/40 lg:text-RED" : ""
}`}
onClick={() => handleItemClick(item)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
handleItemClick(item);
}
}}
>
<div className="flex flex-row gap-2 items-center">
<span
className={`lg:w-11 lg:h-11 lg:rounded-xl lg:flex lg:items-center lg:justify-center ${
isLogout ? "lg:bg-RED/10" : "lg:bg-WHITE2"
}`}
>
{item.image}
</span>
<p className="text-R12 lg:text-B16 font-bold">{item.title}</p>
</div>
<ChevronLeft className="size-4 lg:hidden" />
</div>
);
})}
</div>
{/* Instagram Sync Drawer */}
<InstagramSyncDrawer
setIsCallModalOpen={setIsCallModalOpen}
isOpen={instagramSyncDrawerOpen}
onOpenChange={setInstagramSyncDrawerOpen}
storeId={storeId || ""}
/>
<CallDialog
isCallModalOpen={isCallModalOpen}
setIsCallModalOpen={setIsCallModalOpen}
handleCallConfirm={() => {
window.location.href = `tel:${import.meta.env.VITE_SUPPORT_PHONE}`;
}}
/>
</div>
);
}
export const meta: MetaFunction = () => {
return [
{ title: "تنظیمات فروشگاه - ویترون" },
{
name: "description",
content:
"مدیریت و مشاهده تمام تنظیمات فروشگاه در فروشگاه شما. فیلتر بر اساس وضعیت و جستجو در تنظیمات فروشگاه.",
},
{
name: "keywords",
content:
"تنظیمات فروشگاه، مدیریت تنظیمات فروشگاه، فروشگاه، مشتری، ویترون",
},
{ name: "robots", content: "noindex, follow" },
{ property: "og:title", content: "تنظیمات فروشگاه - ویترون" },
{
property: "og:description",
content:
"مدیریت و مشاهده تمام تنظیمات فروشگاه در فروشگاه شما. فیلتر بر اساس وضعیت و جستجو در تنظیمات فروشگاه.",
},
{ property: "og:type", content: "website" },
{ name: "twitter:card", content: "summary" },
{ name: "twitter:title", content: "تنظیمات فروشگاه - ویترون" },
{
name: "twitter:description",
content:
"مدیریت و مشاهده تمام تنظیمات فروشگاه در فروشگاه شما. فیلتر بر اساس وضعیت و جستجو در تنظیمات فروشگاه.",
},
];
};