- 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>
186 lines
6.4 KiB
TypeScript
186 lines
6.4 KiB
TypeScript
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 (
|
||
<div className="flex flex-col bg-WHITE">
|
||
<div className="lg:hidden">
|
||
<HeaderWithSupport
|
||
title="مدیریت اعضای تیم"
|
||
onBack={() => navigate(`/store/${storeId}/setting`)}
|
||
/>
|
||
</div>
|
||
<h2 className="hidden lg:block text-[24px] font-extrabold mb-6">
|
||
مدیریت اعضای تیم
|
||
</h2>
|
||
|
||
<div className="flex flex-col gap-6 p-4 lg:max-w-[720px]">
|
||
{/* Invite by phone */}
|
||
<div className="flex flex-col gap-2">
|
||
<p className="text-R14">دعوت عضو جدید با شماره موبایل</p>
|
||
<div className="flex items-end gap-2">
|
||
<div className="flex-1">
|
||
<AppInput
|
||
inputTitle=""
|
||
inputDir="ltr"
|
||
inputValue={phone}
|
||
inputPlaceholder="09xxxxxxxxx"
|
||
inputOnChange={(e) => setPhone(e.target.value)}
|
||
onKeyDown={(e) => {
|
||
if (e.key === "Enter") handleInvite();
|
||
}}
|
||
/>
|
||
</div>
|
||
<Button
|
||
variant="dark"
|
||
size="lg"
|
||
className="shrink-0 !h-12"
|
||
onClick={handleInvite}
|
||
disabled={inviteMember.isPending || !phone.trim()}
|
||
>
|
||
{inviteMember.isPending ? (
|
||
<Loader2 className="w-4 h-4 animate-spin" />
|
||
) : (
|
||
<>
|
||
<UserPlus size={18} />
|
||
دعوت
|
||
</>
|
||
)}
|
||
</Button>
|
||
</div>
|
||
<p className="text-R10 text-GRAY leading-5">
|
||
عضو دعوتشده میتواند محصولات، سفارشها و پیامهای فروشگاه را مدیریت
|
||
کند اما به بخش مالی، تنظیمات فروشگاه و مدیریت تیم دسترسی ندارد. اگر
|
||
حساب کاربری با این شماره وجود نداشته باشد، دعوت تا اولین ورود او در
|
||
حالت «در انتظار» میماند.
|
||
</p>
|
||
</div>
|
||
|
||
{/* Members list */}
|
||
<div className="flex flex-col gap-2">
|
||
<p className="text-R12 font-bold text-GRAY">اعضای تیم</p>
|
||
<UiProvider
|
||
isLoading={isLoading}
|
||
isError={isError}
|
||
isEmpty={!!members && members.length === 0}
|
||
onRetry={refetch}
|
||
type="following"
|
||
>
|
||
<div className="flex flex-col divide-y divide-WHITE2 border border-WHITE3 rounded-2xl overflow-hidden">
|
||
{members?.map((m) => (
|
||
<div
|
||
key={m.id}
|
||
className="flex items-center justify-between gap-3 p-4"
|
||
>
|
||
<div className="flex flex-col gap-0.5">
|
||
<p className="text-R14 font-bold">
|
||
{m.name || m.phone}
|
||
</p>
|
||
<p className="text-R10 text-GRAY" dir="ltr">
|
||
{m.phone}
|
||
</p>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
{m.status === "pending" ? (
|
||
<span className="text-R10 font-semibold text-amber-600 bg-amber-50 rounded-full px-2 py-1">
|
||
در انتظار
|
||
</span>
|
||
) : (
|
||
<span className="text-R10 font-semibold text-green-600 bg-green-50 rounded-full px-2 py-1">
|
||
فعال
|
||
</span>
|
||
)}
|
||
<button
|
||
type="button"
|
||
aria-label="حذف عضو"
|
||
onClick={() => handleRemove(m.id)}
|
||
disabled={removeMember.isPending}
|
||
className="text-RED disabled:opacity-50"
|
||
>
|
||
<Trash2 size={20} />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</UiProvider>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export const meta: MetaFunction = () => [
|
||
{ title: "مدیریت اعضای تیم - ویترون" },
|
||
{ name: "robots", content: "noindex, nofollow" },
|
||
];
|