diff --git a/src/api/workspaces.ts b/src/api/workspaces.ts index cef95c9..519f61e 100644 --- a/src/api/workspaces.ts +++ b/src/api/workspaces.ts @@ -19,13 +19,15 @@ export interface PaginatedResponse { export interface WorkspaceMembership { id: string; workspace: string; - user: { - id: string; - email: string; - first_name?: string; - last_name?: string; - [key: string]: any; - }; + user: { + id: string; + email?: string; + first_name?: string; + last_name?: string; + mobile?: string; + profile_picture?: string | null; + [key: string]: any; + }; role: 'owner' | 'admin' | 'member' | 'guest'; is_active: boolean; joined_at?: string; diff --git a/src/locales/en.ts b/src/locales/en.ts index 09c7bbe..4eb6d22 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -151,30 +151,31 @@ export const en = { emptyState: "You are not a member of any workspace.", createTitle: "Create Workspace", editTitle: "Edit Workspace", - detailTitle: "Workspace Details", - save: "Save", - create: "Create", - noWorkspaceTitle: "Welcome!", - noWorkspaceDesc: "Please create your first workspace.", - back: "Back to Workspaces", - roleLabel: "Your Role", - openReports: "Open reports", - statsMembers: "Members", - statsRates: "Rates set", - statsOwnersAdmins: "Owners & admins", - statsGuests: "Guests", - membersSectionTitle: "Members", + detailTitle: "Workspace Details", + save: "Save", + create: "Create", + noWorkspaceTitle: "Welcome!", + noWorkspaceDesc: "Please create your first workspace.", + back: "Back to Workspaces", + roleLabel: "Your Role", + openReports: "Open reports", + statsMembers: "Members", + statsRates: "Rates set", + statsOwnersAdmins: "Owners & admins", + statsGuests: "Guests", + membersSectionTitle: "Members", membersSectionSubtitle: "People in this workspace and their current roles.", membersLocked: "Only owners and admins can view the full member list.", manageMembers: "Manage members", - joinedLabel: "Joined", + mobileNumber: "Mobile Number", + youLabel: "You", resourcesTitle: "Resources", - resourceOpen: "Open", - roleDistributionTitle: "Role distribution", - unknownMember: "Unknown member", - roles: { - owner: "Owner", - admin: "Admin", + resourceOpen: "Open", + roleDistributionTitle: "Role distribution", + unknownMember: "Unknown member", + roles: { + owner: "Owner", + admin: "Admin", member: "Member", guest: "Guest", }, @@ -258,16 +259,16 @@ export const en = { next: "Next", }, - sidebar: { - timesheet: "Timesheet", - reports: "Reports", - workspaces: 'Workspaces', - clients: 'Clients', - projects: "Projects", - tags: "Tags", - expand: 'Expand', - collapse: 'Collapse', - }, + sidebar: { + timesheet: "Timesheet", + reports: "Reports", + workspaces: 'Workspaces', + clients: 'Clients', + projects: "Projects", + tags: "Tags", + expand: 'Expand', + collapse: 'Collapse', + }, ordering: { createdAtDesc: "Newest First", @@ -277,7 +278,7 @@ export const en = { nameDesc: "Name (Z-A)", }, - projects: { + projects: { title: "Projects", description: (workspaceName: string) => `Manage projects for ${workspaceName}`, active: "Active Projects", @@ -314,12 +315,12 @@ export const en = { addAllWorkspaceMembers: "Add all workspace members", confirmDeleteTitle: "Remove Member", confirmDeleteDesc: "Are you sure you want to remove this member from the project?", - createSuccess: "Project created successfully.", - createError: "Failed to create project.", - updateSuccess: "Project updated successfully.", - updateError: "Failed to update project.", - edit: "Edit Project", - memberAlreadyAdded: "This user is already on the project team.", + createSuccess: "Project created successfully.", + createError: "Failed to create project.", + updateSuccess: "Project updated successfully.", + updateError: "Failed to update project.", + edit: "Edit Project", + memberAlreadyAdded: "This user is already on the project team.", roles: { member: "Member", manager: "Manager" @@ -330,173 +331,173 @@ export const en = { userNotFound: "No user found with this mobile number.", alreadyInProject: "Already Added", addToProject: "Add to Project", - noWorkspaceMembers: "No members found.", - }, - - tags: { - title: "Tags", - description: (workspaceName: string) => `Manage tags for ${workspaceName}`, - create: "Create Tag", - createTitle: "Create Tag", - editTitle: "Edit Tag", - deleteTitle: "Delete Tag", - deleteConfirmMessage: (name: string) => `Are you sure you want to delete ${name}?`, - searchPlaceholder: "Search tags...", - nameLabel: "Tag Name", - namePlaceholder: "e.g. Design", - colorLabel: "Color", - emptyState: "No tags found", - selectWorkspace: "Please select a workspace first.", - fetchError: "Failed to load tags", - createSuccess: "Tag created successfully.", - updateSuccess: "Tag updated successfully.", - saveError: "Failed to save tag.", - deleteSuccess: "Tag deleted successfully.", - deleteError: "Failed to delete tag.", - }, - - rates: { - workspaceSectionTitle: "Workspace User Rates", - projectSectionTitle: "Project User Rates", - workspaceRate: "Workspace rate", - projectOverride: "Project override", - inheritsWorkspaceRate: "Inherits workspace rate", - noRate: "No rate", - hourlyRatePlaceholder: "0.00", - currencyPlaceholder: "USD", - searchUnitPlaceholder: "Search unit...", - removeRate: "Remove rate", - workspaceSaveSuccess: "Workspace user rate saved.", - workspaceSaveError: "Failed to save workspace user rate.", - workspaceRemoveSuccess: "Workspace user rate removed.", - workspaceRemoveError: "Failed to remove workspace user rate.", - projectSaveSuccess: "Project user rate saved.", - projectSaveError: "Failed to save project user rate.", - projectRemoveSuccess: "Project user rate removed.", - projectRemoveError: "Failed to remove project user rate.", - }, - - timesheet: { - title: "Timesheet", - description: (workspaceName: string) => `Track time inside ${workspaceName}`, - selectWorkspace: "Please select a workspace first.", - addEntry: "Add Entry", - startTimer: "Start Timer", - stopTimer: "Stop Timer", - timerRunning: "Timer Running", - runningLabel: "Current timer", - runningBadge: "Running", - noRunningEntry: "No running entry", - searchPlaceholder: "Search time entries...", - orderingNewest: "Newest first", - orderingOldest: "Oldest first", - emptyState: "No time entries found", - emptyDescription: "No description", - createTitle: "Add Time Entry", - startTitle: "Start Timer", - editTitle: "Edit Time Entry", - createSuccess: "Time entry created successfully.", - startSuccess: "Timer started successfully.", - updateSuccess: "Time entry updated successfully.", - saveError: "Failed to save time entry.", - stopSuccess: "Timer stopped successfully.", - stopError: "Failed to stop timer.", - deleteSuccess: "Time entry deleted successfully.", - deleteError: "Failed to delete time entry.", - fetchError: "Failed to load time entries.", - optionsError: "Failed to load projects and tags.", - descriptionLabel: "Description", - descriptionPlaceholder: "What are you working on?", - projectLabel: "Project", - noProject: "No project", - startLabel: "Start", - endLabel: "End", - billable: "Billable", - noTagsHint: "Create tags first from the Tags page.", - clearFilters: "Clear filters", - customFromLabel: "From date", - customToLabel: "To date", - allClientsLabel: "All clients", - allProjectsLabel: "All projects", - allTagsLabel: "All tags", - showFiltersLabel: "Show filters", - hideFiltersLabel: "Hide filters", - applyFiltersLabel: "Apply", - clientFilterPrefix: "Client", - projectFilterPrefix: "Project", - tagFilterPrefix: "Tag", - fromFilterPrefix: "From", - toFilterPrefix: "To", - deletedProjectLabel: "Deleted project", - deletedTagLabel: "Deleted tag", - }, - - reports: { - title: "Reports", - description: (workspaceName: string) => `Review activity reports for ${workspaceName}`, - selectWorkspace: "Please select a workspace first.", - chartTab: "Chart", - tableTab: "Table", - period: "Period", - periodThisWeek: "This week", - periodThisMonth: "This month", - periodThisYear: "This year", - periodFirstHalf: "First half of year", - periodSecondHalf: "Second half of year", - periodCustom: "Custom period", - fromDate: "From date", - toDate: "To date", - user: "User", - allUsers: "All users", - searchUsers: "Search users...", - client: "Client", - allClients: "All clients", - searchClients: "Search clients...", - project: "Project", - allProjects: "All projects", - searchProjects: "Search projects...", - tags: "Tags", - allTags: "All tags", - searchTags: "Search tags...", - name: "Name", - clear: "Clear", - apply: "Apply", - totalHours: "Total hours", - billableHours: "Billable hours", - nonBillableHours: "Non-billable hours", - totalIncome: "Total income", - chartTitle: "Activity chart", - totalSeconds: "Total seconds", - exportExcel: "Export Excel", - exportPdf: "Export PDF", - date: "Date", - details: "Details", - total: "Total", - clientsTable: "Clients", - projectsTable: "Projects", - tagsTable: "Tags", - loadError: "Failed to load reports.", - loadDayDetailsError: "Failed to load day details.", - loadFiltersError: "Failed to load report filters.", - exportQueued: "Export queued. You will receive a notification with the download link.", - exportError: "Failed to queue report export.", - }, - - notifications: { - title: "Notifications", - open: "Open notifications", - empty: "No notifications yet.", - loading: "Loading notifications...", - loadingMore: "Loading more...", - loadMore: "Load more", - markAllRead: "Mark all as read", - markSeenError: "Failed to update notification", - markAllError: "Failed to update notifications", - deleteError: "Failed to delete notification", - loadError: "Failed to load notifications", - openError: "Failed to open notification", - newTitle: "New notification", - openAction: "Open", - summary: (total: number, unread: number) => `${total} total, ${unread} unread`, - }, -} + noWorkspaceMembers: "No members found.", + }, + + tags: { + title: "Tags", + description: (workspaceName: string) => `Manage tags for ${workspaceName}`, + create: "Create Tag", + createTitle: "Create Tag", + editTitle: "Edit Tag", + deleteTitle: "Delete Tag", + deleteConfirmMessage: (name: string) => `Are you sure you want to delete ${name}?`, + searchPlaceholder: "Search tags...", + nameLabel: "Tag Name", + namePlaceholder: "e.g. Design", + colorLabel: "Color", + emptyState: "No tags found", + selectWorkspace: "Please select a workspace first.", + fetchError: "Failed to load tags", + createSuccess: "Tag created successfully.", + updateSuccess: "Tag updated successfully.", + saveError: "Failed to save tag.", + deleteSuccess: "Tag deleted successfully.", + deleteError: "Failed to delete tag.", + }, + + rates: { + workspaceSectionTitle: "Workspace User Rates", + projectSectionTitle: "Project User Rates", + workspaceRate: "Workspace rate", + projectOverride: "Project override", + inheritsWorkspaceRate: "Inherits workspace rate", + noRate: "No rate", + hourlyRatePlaceholder: "0.00", + currencyPlaceholder: "USD", + searchUnitPlaceholder: "Search unit...", + removeRate: "Remove rate", + workspaceSaveSuccess: "Workspace user rate saved.", + workspaceSaveError: "Failed to save workspace user rate.", + workspaceRemoveSuccess: "Workspace user rate removed.", + workspaceRemoveError: "Failed to remove workspace user rate.", + projectSaveSuccess: "Project user rate saved.", + projectSaveError: "Failed to save project user rate.", + projectRemoveSuccess: "Project user rate removed.", + projectRemoveError: "Failed to remove project user rate.", + }, + + timesheet: { + title: "Timesheet", + description: (workspaceName: string) => `Track time inside ${workspaceName}`, + selectWorkspace: "Please select a workspace first.", + addEntry: "Add Entry", + startTimer: "Start Timer", + stopTimer: "Stop Timer", + timerRunning: "Timer Running", + runningLabel: "Current timer", + runningBadge: "Running", + noRunningEntry: "No running entry", + searchPlaceholder: "Search time entries...", + orderingNewest: "Newest first", + orderingOldest: "Oldest first", + emptyState: "No time entries found", + emptyDescription: "No description", + createTitle: "Add Time Entry", + startTitle: "Start Timer", + editTitle: "Edit Time Entry", + createSuccess: "Time entry created successfully.", + startSuccess: "Timer started successfully.", + updateSuccess: "Time entry updated successfully.", + saveError: "Failed to save time entry.", + stopSuccess: "Timer stopped successfully.", + stopError: "Failed to stop timer.", + deleteSuccess: "Time entry deleted successfully.", + deleteError: "Failed to delete time entry.", + fetchError: "Failed to load time entries.", + optionsError: "Failed to load projects and tags.", + descriptionLabel: "Description", + descriptionPlaceholder: "What are you working on?", + projectLabel: "Project", + noProject: "No project", + startLabel: "Start", + endLabel: "End", + billable: "Billable", + noTagsHint: "Create tags first from the Tags page.", + clearFilters: "Clear filters", + customFromLabel: "From date", + customToLabel: "To date", + allClientsLabel: "All clients", + allProjectsLabel: "All projects", + allTagsLabel: "All tags", + showFiltersLabel: "Show filters", + hideFiltersLabel: "Hide filters", + applyFiltersLabel: "Apply", + clientFilterPrefix: "Client", + projectFilterPrefix: "Project", + tagFilterPrefix: "Tag", + fromFilterPrefix: "From", + toFilterPrefix: "To", + deletedProjectLabel: "Deleted project", + deletedTagLabel: "Deleted tag", + }, + + reports: { + title: "Reports", + description: (workspaceName: string) => `Review activity reports for ${workspaceName}`, + selectWorkspace: "Please select a workspace first.", + chartTab: "Chart", + tableTab: "Table", + period: "Period", + periodThisWeek: "This week", + periodThisMonth: "This month", + periodThisYear: "This year", + periodFirstHalf: "First half of year", + periodSecondHalf: "Second half of year", + periodCustom: "Custom period", + fromDate: "From date", + toDate: "To date", + user: "User", + allUsers: "All users", + searchUsers: "Search users...", + client: "Client", + allClients: "All clients", + searchClients: "Search clients...", + project: "Project", + allProjects: "All projects", + searchProjects: "Search projects...", + tags: "Tags", + allTags: "All tags", + searchTags: "Search tags...", + name: "Name", + clear: "Clear", + apply: "Apply", + totalHours: "Total hours", + billableHours: "Billable hours", + nonBillableHours: "Non-billable hours", + totalIncome: "Total income", + chartTitle: "Activity chart", + totalSeconds: "Total seconds", + exportExcel: "Export Excel", + exportPdf: "Export PDF", + date: "Date", + details: "Details", + total: "Total", + clientsTable: "Clients", + projectsTable: "Projects", + tagsTable: "Tags", + loadError: "Failed to load reports.", + loadDayDetailsError: "Failed to load day details.", + loadFiltersError: "Failed to load report filters.", + exportQueued: "Export queued. You will receive a notification with the download link.", + exportError: "Failed to queue report export.", + }, + + notifications: { + title: "Notifications", + open: "Open notifications", + empty: "No notifications yet.", + loading: "Loading notifications...", + loadingMore: "Loading more...", + loadMore: "Load more", + markAllRead: "Mark all as read", + markSeenError: "Failed to update notification", + markAllError: "Failed to update notifications", + deleteError: "Failed to delete notification", + loadError: "Failed to load notifications", + openError: "Failed to open notification", + newTitle: "New notification", + openAction: "Open", + summary: (total: number, unread: number) => `${total} total, ${unread} unread`, + }, +} diff --git a/src/locales/fa.ts b/src/locales/fa.ts index 7a40a06..6be13a3 100644 --- a/src/locales/fa.ts +++ b/src/locales/fa.ts @@ -165,11 +165,12 @@ export const fa = { statsOwnersAdmins: "مالکان و ادمین‌ها", statsGuests: "مهمان‌ها", membersSectionTitle: "اعضا", - membersSectionSubtitle: "اعضای این ورک‌اسپیس و نقش فعلی آن‌ها.", - membersLocked: "فهرست کامل اعضا فقط برای مالک و ادمین قابل مشاهده است.", - manageMembers: "مدیریت اعضا", - joinedLabel: "زمان عضویت", - resourcesTitle: "منابع", + membersSectionSubtitle: "اعضای این ورک‌اسپیس و نقش فعلی آن‌ها.", + membersLocked: "فهرست کامل اعضا فقط برای مالک و ادمین قابل مشاهده است.", + manageMembers: "مدیریت اعضا", + mobileNumber: "شماره تماس", + youLabel: "شما", + resourcesTitle: "منابع", resourceOpen: "مشاهده", roleDistributionTitle: "توزیع نقش‌ها", unknownMember: "عضو ناشناس", @@ -421,12 +422,12 @@ export const fa = { applyFiltersLabel: "اعمال", clientFilterPrefix: "مشتری", projectFilterPrefix: "پروژه", - tagFilterPrefix: "تگ", - fromFilterPrefix: "از", - toFilterPrefix: "تا", - deletedProjectLabel: "پروژه حذف‌شده", - deletedTagLabel: "تگ حذف‌شده", - }, + tagFilterPrefix: "تگ", + fromFilterPrefix: "از", + toFilterPrefix: "تا", + deletedProjectLabel: "پروژه حذف‌شده", + deletedTagLabel: "تگ حذف‌شده", + }, reports: { title: "گزارش‌ها", description: (workspaceName: string) => `مرور گزارش فعالیت برای ${workspaceName}`, diff --git a/src/pages/WorkspaceDetail.tsx b/src/pages/WorkspaceDetail.tsx index ffa14da..1008b34 100644 --- a/src/pages/WorkspaceDetail.tsx +++ b/src/pages/WorkspaceDetail.tsx @@ -23,6 +23,7 @@ import { type Workspace, type WorkspaceMembership, } from '../api/workspaces'; +import { useAppContext } from '../context/AppContext'; import { useWorkspace } from '../context/WorkspaceContext'; import { useTranslation } from '../hooks/useTranslation'; import { @@ -53,6 +54,7 @@ export default function WorkspaceDetail() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const { t, lang } = useTranslation(); + const { user } = useAppContext(); const { setActiveWorkspace } = useWorkspace(); const [workspace, setWorkspace] = useState(null); @@ -74,7 +76,7 @@ export default function WorkspaceDetail() { const data = await getWorkspace(id!); 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 canViewProjects = canWorkspace(data.my_role, PROJECTS_VIEW); const canViewTags = canWorkspace(data.my_role, TAGS_VIEW); @@ -180,6 +182,12 @@ export default function WorkspaceDetail() { 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) => { if (!rate) return t.rates?.noRate || 'No rate'; const unitLabel = @@ -192,7 +200,8 @@ export default function WorkspaceDetail() { const workspaceRole = workspace?.my_role; const canEdit = canWorkspace(workspaceRole, WORKSPACE_EDIT); 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); if (isLoading || !workspace) { @@ -368,11 +377,11 @@ export default function WorkspaceDetail() {

{t.workspace?.membersSectionTitle || t.workspace?.members || 'Members'}

-

- {canViewMembers - ? t.workspace?.membersSectionSubtitle || 'People in this workspace and their current roles.' - : t.workspace?.membersLocked || 'Only owners and admins can view the full member list.'} -

+ {canViewMembers + ?

+ { t.workspace?.membersSectionSubtitle || 'People in this workspace and their current roles.' } +

+ : ''} {canEdit && (