Vitron-Front/app/routes/store.$storeId.team.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

186 lines
6.4 KiB
TypeScript
Raw Permalink 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 { 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" },
];