feat(workspaces): expand detail page member list
This commit is contained in:
@@ -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';
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -168,7 +168,8 @@ export const fa = {
|
|||||||
membersSectionSubtitle: "اعضای این ورکاسپیس و نقش فعلی آنها.",
|
membersSectionSubtitle: "اعضای این ورکاسپیس و نقش فعلی آنها.",
|
||||||
membersLocked: "فهرست کامل اعضا فقط برای مالک و ادمین قابل مشاهده است.",
|
membersLocked: "فهرست کامل اعضا فقط برای مالک و ادمین قابل مشاهده است.",
|
||||||
manageMembers: "مدیریت اعضا",
|
manageMembers: "مدیریت اعضا",
|
||||||
joinedLabel: "زمان عضویت",
|
mobileNumber: "شماره تماس",
|
||||||
|
youLabel: "شما",
|
||||||
resourcesTitle: "منابع",
|
resourcesTitle: "منابع",
|
||||||
resourceOpen: "مشاهده",
|
resourceOpen: "مشاهده",
|
||||||
roleDistributionTitle: "توزیع نقشها",
|
roleDistributionTitle: "توزیع نقشها",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user