feat(workspaces): expand detail page member list

This commit is contained in:
2026-04-28 10:46:15 +03:30
parent b1ad372474
commit 581cfab1ac
4 changed files with 292 additions and 248 deletions

View File

@@ -21,9 +21,11 @@ export interface WorkspaceMembership {
workspace: string; workspace: string;
user: { user: {
id: string; id: string;
email: string; email?: string;
first_name?: string; first_name?: string;
last_name?: string; last_name?: string;
mobile?: string;
profile_picture?: string | null;
[key: string]: any; [key: string]: any;
}; };
role: 'owner' | 'admin' | 'member' | 'guest'; role: 'owner' | 'admin' | 'member' | 'guest';

View File

@@ -167,7 +167,8 @@ export const en = {
membersSectionSubtitle: "People in this workspace and their current roles.", membersSectionSubtitle: "People in this workspace and their current roles.",
membersLocked: "Only owners and admins can view the full member list.", membersLocked: "Only owners and admins can view the full member list.",
manageMembers: "Manage members", manageMembers: "Manage members",
joinedLabel: "Joined", mobileNumber: "Mobile Number",
youLabel: "You",
resourcesTitle: "Resources", resourcesTitle: "Resources",
resourceOpen: "Open", resourceOpen: "Open",
roleDistributionTitle: "Role distribution", roleDistributionTitle: "Role distribution",

View File

@@ -168,7 +168,8 @@ export const fa = {
membersSectionSubtitle: "اعضای این ورک‌اسپیس و نقش فعلی آن‌ها.", membersSectionSubtitle: "اعضای این ورک‌اسپیس و نقش فعلی آن‌ها.",
membersLocked: "فهرست کامل اعضا فقط برای مالک و ادمین قابل مشاهده است.", membersLocked: "فهرست کامل اعضا فقط برای مالک و ادمین قابل مشاهده است.",
manageMembers: "مدیریت اعضا", manageMembers: "مدیریت اعضا",
joinedLabel: "زمان عضویت", mobileNumber: "شماره تماس",
youLabel: "شما",
resourcesTitle: "منابع", resourcesTitle: "منابع",
resourceOpen: "مشاهده", resourceOpen: "مشاهده",
roleDistributionTitle: "توزیع نقش‌ها", roleDistributionTitle: "توزیع نقش‌ها",

View File

@@ -23,6 +23,7 @@ import {
type Workspace, type Workspace,
type WorkspaceMembership, type WorkspaceMembership,
} from '../api/workspaces'; } from '../api/workspaces';
import { useAppContext } from '../context/AppContext';
import { useWorkspace } from '../context/WorkspaceContext'; import { useWorkspace } from '../context/WorkspaceContext';
import { useTranslation } from '../hooks/useTranslation'; import { useTranslation } from '../hooks/useTranslation';
import { import {
@@ -53,6 +54,7 @@ export default function WorkspaceDetail() {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
const { t, lang } = useTranslation(); const { t, lang } = useTranslation();
const { user } = useAppContext();
const { setActiveWorkspace } = useWorkspace(); const { setActiveWorkspace } = useWorkspace();
const [workspace, setWorkspace] = useState<Workspace | null>(null); const [workspace, setWorkspace] = useState<Workspace | null>(null);
@@ -74,7 +76,7 @@ export default function WorkspaceDetail() {
const data = await getWorkspace(id!); const data = await getWorkspace(id!);
setWorkspace(data); setWorkspace(data);
const canViewMembers = canWorkspace(data.my_role, WORKSPACE_MEMBERS_VIEW); const canViewMembers = canWorkspace(data.my_role, WORKSPACE_VIEW);
const canViewClients = canWorkspace(data.my_role, CLIENTS_VIEW); const canViewClients = canWorkspace(data.my_role, CLIENTS_VIEW);
const canViewProjects = canWorkspace(data.my_role, PROJECTS_VIEW); const canViewProjects = canWorkspace(data.my_role, PROJECTS_VIEW);
const canViewTags = canWorkspace(data.my_role, TAGS_VIEW); const canViewTags = canWorkspace(data.my_role, TAGS_VIEW);
@@ -180,6 +182,12 @@ export default function WorkspaceDetail() {
return member.user?.mobile || member.user?.email || '-'; return member.user?.mobile || member.user?.email || '-';
}; };
const getMemberInitials = (member: WorkspaceMembership) => {
const firstName = member.user?.first_name?.trim()?.charAt(0) || '';
const lastName = member.user?.last_name?.trim()?.charAt(0) || '';
return `${firstName}${lastName}`.trim().toUpperCase() || getMemberName(member).charAt(0).toUpperCase();
};
const formatRateUnit = (rate?: WorkspaceUserRate) => { const formatRateUnit = (rate?: WorkspaceUserRate) => {
if (!rate) return t.rates?.noRate || 'No rate'; if (!rate) return t.rates?.noRate || 'No rate';
const unitLabel = const unitLabel =
@@ -192,7 +200,8 @@ export default function WorkspaceDetail() {
const workspaceRole = workspace?.my_role; const workspaceRole = workspace?.my_role;
const canEdit = canWorkspace(workspaceRole, WORKSPACE_EDIT); const canEdit = canWorkspace(workspaceRole, WORKSPACE_EDIT);
const canDelete = canWorkspace(workspaceRole, WORKSPACE_DELETE); const canDelete = canWorkspace(workspaceRole, WORKSPACE_DELETE);
const canViewMembers = canWorkspace(workspaceRole, WORKSPACE_MEMBERS_VIEW); const canViewMembers = canWorkspace(workspaceRole, WORKSPACE_VIEW);
const canViewMemberSensitiveDetails = canWorkspace(workspaceRole, WORKSPACE_MEMBERS_VIEW);
const canViewReports = canWorkspace(workspaceRole, WORKSPACE_VIEW); const canViewReports = canWorkspace(workspaceRole, WORKSPACE_VIEW);
if (isLoading || !workspace) { if (isLoading || !workspace) {
@@ -368,11 +377,11 @@ export default function WorkspaceDetail() {
<h2 className="text-lg font-semibold text-slate-900 dark:text-white"> <h2 className="text-lg font-semibold text-slate-900 dark:text-white">
{t.workspace?.membersSectionTitle || t.workspace?.members || 'Members'} {t.workspace?.membersSectionTitle || t.workspace?.members || 'Members'}
</h2> </h2>
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
{canViewMembers {canViewMembers
? t.workspace?.membersSectionSubtitle || 'People in this workspace and their current roles.' ? <p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
: t.workspace?.membersLocked || 'Only owners and admins can view the full member list.'} { t.workspace?.membersSectionSubtitle || 'People in this workspace and their current roles.' }
</p> </p>
: ''}
</div> </div>
{canEdit && ( {canEdit && (
<button <button
@@ -394,26 +403,56 @@ export default function WorkspaceDetail() {
) : ( ) : (
activeMembers.map((member) => { activeMembers.map((member) => {
const rate = memberRateMap.get(member.user.id); const rate = memberRateMap.get(member.user.id);
const isCurrentUser = member.user.id === user?.id;
return ( return (
<div key={member.id} className="flex flex-col gap-4 p-5 sm:p-6"> <div
key={member.id}
className={`flex flex-col gap-4 p-5 sm:p-6 ${
isCurrentUser
? 'bg-blue-50/80 ring-1 ring-blue-200 dark:bg-blue-950/20 dark:ring-blue-900/60'
: ''
}`}
>
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"> <div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="min-w-0"> <div className="min-w-0 flex-1">
<div className="flex items-start gap-4">
<div className="flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden rounded-full bg-slate-200 text-sm font-semibold text-slate-600 shadow-sm dark:bg-slate-800 dark:text-slate-300">
{member.user?.profile_picture ? (
<img
src={member.user.profile_picture}
alt={getMemberName(member)}
className="h-full w-full object-cover"
/>
) : (
<span>{getMemberInitials(member)}</span>
)}
</div>
<div className="min-w-0 flex-1">
<div className="mb-2 flex flex-wrap items-center gap-2"> <div className="mb-2 flex flex-wrap items-center gap-2">
<h3 className="text-base font-semibold text-slate-900 dark:text-white"> <h3 className="text-base font-semibold text-slate-900 dark:text-white">
{getMemberName(member)} {getMemberName(member)}
</h3> </h3>
{isCurrentUser && (
<span className="inline-flex rounded-full border border-blue-200 bg-blue-100 px-2.5 py-1 text-xs font-semibold text-blue-700 dark:border-blue-900/60 dark:bg-blue-900/30 dark:text-blue-300">
{t.workspace?.youLabel || 'You'}
</span>
)}
<span className={`inline-flex rounded-full px-2.5 py-1 text-xs font-semibold ${roleBadgeStyles[member.role]}`}> <span className={`inline-flex rounded-full px-2.5 py-1 text-xs font-semibold ${roleBadgeStyles[member.role]}`}>
{t.workspace?.roles[member.role]} {t.workspace?.roles[member.role]}
</span> </span>
</div> </div>
{canViewMemberSensitiveDetails && (
<div className="space-y-1 text-sm text-slate-500 dark:text-slate-400"> <div className="space-y-1 text-sm text-slate-500 dark:text-slate-400">
<p>{getMemberContact(member)}</p>
<p> <p>
{t.workspace?.joinedLabel || 'Joined'}: {formatDate(member.joined_at)} {t.workspace?.mobileNumber || 'Mobile Number'}: {getMemberContact(member)}
</p> </p>
</div> </div>
)}
</div>
</div>
</div> </div>
{canViewMemberSensitiveDetails && (
<div className="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 dark:border-slate-700 dark:bg-slate-800/80 sm:min-w-[210px]"> <div className="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 dark:border-slate-700 dark:bg-slate-800/80 sm:min-w-[210px]">
<p className="mb-1 text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400"> <p className="mb-1 text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
{t.rates?.workspaceRate || 'Workspace rate'} {t.rates?.workspaceRate || 'Workspace rate'}
@@ -422,6 +461,7 @@ export default function WorkspaceDetail() {
{formatRateUnit(rate)} {formatRateUnit(rate)}
</p> </p>
</div> </div>
)}
</div> </div>
</div> </div>
); );
@@ -430,7 +470,7 @@ export default function WorkspaceDetail() {
</div> </div>
) : ( ) : (
<div className="p-6 text-sm text-slate-500 dark:text-slate-400"> <div className="p-6 text-sm text-slate-500 dark:text-slate-400">
{t.workspace?.membersLocked || 'Only owners and admins can view the full member list.'} {t.workspace?.membersLocked || 'This member list is not available for your current role.'}
</div> </div>
)} )}
</section> </section>