diff --git a/src/components/reports/ReportsChartPanel.tsx b/src/components/reports/ReportsChartPanel.tsx index 55b8a23..5fa6352 100644 --- a/src/components/reports/ReportsChartPanel.tsx +++ b/src/components/reports/ReportsChartPanel.tsx @@ -105,7 +105,13 @@ const getPersianDateParts = (value: Date) => { }; }; -const getDailyAxisLabel = (date: Date, lang: "en" | "fa") => { +const getDailyAxisLabel = (date: Date, lang: "en" | "fa", period: string) => { + if (period === "this_week") { + return new Intl.DateTimeFormat(lang === "fa" ? "fa-IR-u-ca-persian" : "en-US", { + weekday: "short", + }).format(date); + } + if (lang === "fa") { return toPersianDigits(String(getPersianDateParts(date).day)); } @@ -140,7 +146,13 @@ const getMonthlyTooltipLabel = (bucketKey: string, lang: "en" | "fa") => { ); }; -const buildDailyBuckets = (fromDate: string, toDate: string, existing: ReportChartBucket[], lang: "en" | "fa") => { +const buildDailyBuckets = ( + fromDate: string, + toDate: string, + existing: ReportChartBucket[], + lang: "en" | "fa", + period: string, +) => { const map = new Map(existing.map((bucket) => [bucket.bucket_key, bucket])); const result: ReportChartBucket[] = []; const cursor = parseIsoDate(fromDate); @@ -152,7 +164,7 @@ const buildDailyBuckets = (fromDate: string, toDate: string, existing: ReportCha result.push( existingBucket ?? { bucket_key: key, - bucket_label: getDailyAxisLabel(cursor, lang), + bucket_label: getDailyAxisLabel(cursor, lang, period), total_seconds: 0, total_duration: "00:00:00", }, @@ -162,7 +174,7 @@ const buildDailyBuckets = (fromDate: string, toDate: string, existing: ReportCha return result.map((bucket) => ({ ...bucket, - bucket_label: getDailyAxisLabel(parseIsoDate(bucket.bucket_key), lang), + bucket_label: getDailyAxisLabel(parseIsoDate(bucket.bucket_key), lang, period), })); }; @@ -282,7 +294,7 @@ export function ReportsChartPanel({ const buckets = useMonthlyBuckets ? buildMonthlyBuckets(data.scope.from_date, data.scope.to_date, data.buckets, lang) - : buildDailyBuckets(data.scope.from_date, data.scope.to_date, data.buckets, lang); + : buildDailyBuckets(data.scope.from_date, data.scope.to_date, data.buckets, lang, data.scope.period); const chartMinWidth = Math.max(640, buckets.length * (useMonthlyBuckets ? 92 : 44)); const interval = useMonthlyBuckets ? 0 : buckets.length > 20 ? Math.ceil(buckets.length / 10) - 1 : 0; diff --git a/src/components/reports/ReportsFilterBar.tsx b/src/components/reports/ReportsFilterBar.tsx index 2013072..47758ae 100644 --- a/src/components/reports/ReportsFilterBar.tsx +++ b/src/components/reports/ReportsFilterBar.tsx @@ -116,6 +116,7 @@ export function ReportsFilterBar({ tags, users, canSelectUsers, + isLoadingUsers, labels, }: { value: ReportsFilterDraft; @@ -125,6 +126,7 @@ export function ReportsFilterBar({ tags: Tag[]; users: WorkspaceMembership[]; canSelectUsers: boolean; + isLoadingUsers: boolean; labels: Record; }) { const [draft, setDraft] = useState(value); @@ -200,30 +202,36 @@ export function ReportsFilterBar({ ) : null} - {canSelectUsers ? ( + {canSelectUsers || isLoadingUsers ? (
- setDraft((current) => ({ ...current, user }))} - options={[ - { value: "", label: labels.allUsers }, - ...users.map((membership) => ({ - value: membership.user.id, - label: - `${membership.user.first_name || ""} ${membership.user.last_name || ""}`.trim() || - membership.user.email || - membership.user.id, - searchText: membership.user.mobile || "", - })), - ]} - placeholder={labels.allUsers} - searchPlaceholder={labels.searchUsers} - className="w-full" - buttonClassName="h-10 rounded-2xl border border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900" - /> + {isLoadingUsers ? ( +
+ {labels.searchUsers || "Loading users..."} +
+ ) : ( + setDraft((current) => ({ ...current, user }))} + options={[ + { value: "", label: labels.allUsers }, + ...users.map((membership) => ({ + value: membership.user.id, + label: + `${membership.user.first_name || ""} ${membership.user.last_name || ""}`.trim() || + membership.user.email || + membership.user.id, + searchText: membership.user.mobile || "", + })), + ]} + placeholder={labels.allUsers} + searchPlaceholder={labels.searchUsers} + className="w-full" + buttonClassName="h-10 rounded-2xl border border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900" + /> + )}
) : null} diff --git a/src/locales/en.ts b/src/locales/en.ts index 4eb6d22..8ff1948 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -164,12 +164,12 @@ export const en = { 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", - mobileNumber: "Mobile Number", - youLabel: "You", - resourcesTitle: "Resources", + membersSectionSubtitle: "People in this workspace and their current roles.", + membersLocked: "Only owners and admins can view the full member list.", + manageMembers: "Manage members", + mobileNumber: "Mobile Number", + youLabel: "You", + resourcesTitle: "Resources", resourceOpen: "Open", roleDistributionTitle: "Role distribution", unknownMember: "Unknown member", diff --git a/src/pages/Reports.tsx b/src/pages/Reports.tsx index 2fd4aa3..40c6e71 100644 --- a/src/pages/Reports.tsx +++ b/src/pages/Reports.tsx @@ -96,12 +96,15 @@ export default function Reports() { const [dayDetails, setDayDetails] = useState(null); const [openDay, setOpenDay] = useState(null); const [isLoading, setIsLoading] = useState(false); + const [isLoadingUsers, setIsLoadingUsers] = useState(false); const [exportState, setExportState] = useState({ excel: { pending: false, cooldownSeconds: 0 }, pdf: { pending: false, cooldownSeconds: 0 }, }); const canSelectUsers = canWorkspace(activeWorkspace?.my_role, WORKSPACE_MEMBERS_VIEW); + const isWorkspaceRoleResolved = Boolean(activeWorkspace?.my_role); + const showUserFilterLoading = !isWorkspaceRoleResolved || (canSelectUsers && isLoadingUsers); const [filters, setFilters] = useState({ period: "this_month", @@ -117,16 +120,14 @@ export default function Reports() { if (!activeWorkspace?.id) return; const loadOptions = async () => { try { - const [projectsResponse, clientsResponse, tagsResponse, membersResponse] = await Promise.all([ + const [projectsResponse, clientsResponse, tagsResponse] = await Promise.all([ getProjects(activeWorkspace.id, { limit: 300, offset: 0 }), getClients(activeWorkspace.id, "", "", 300, 0), getTags(activeWorkspace.id, { limit: 300, offset: 0 }), - fetchWorkspaceMemberships({ workspace: activeWorkspace.id }), ]); setProjects(projectsResponse.results || []); setClients((clientsResponse.results || []).map((client: { id: string; name: string }) => ({ id: client.id, name: client.name }))); setTags(tagsResponse.results || []); - setMemberships(membersResponse.results || []); } catch { toast.error(t.reports?.loadFiltersError || "Failed to load report filters."); } @@ -134,6 +135,31 @@ export default function Reports() { void loadOptions(); }, [activeWorkspace?.id, t.reports?.loadFiltersError]); + useEffect(() => { + if (!activeWorkspace?.id || !isWorkspaceRoleResolved) return; + + if (!canSelectUsers) { + setMemberships([]); + setIsLoadingUsers(false); + return; + } + + const loadUsers = async () => { + setIsLoadingUsers(true); + try { + const membersResponse = await fetchWorkspaceMemberships({ workspace: activeWorkspace.id }); + setMemberships(membersResponse.results || []); + } catch { + toast.error(t.reports?.loadFiltersError || "Failed to load report filters."); + setMemberships([]); + } finally { + setIsLoadingUsers(false); + } + }; + + void loadUsers(); + }, [activeWorkspace?.id, canSelectUsers, isWorkspaceRoleResolved, t.reports?.loadFiltersError]); + const buildApiFilters = (draft: ReportsFilterDraft): ReportFilters | null => { if (!activeWorkspace?.id) return null; @@ -296,6 +322,7 @@ export default function Reports() { tags={tags} users={memberships} canSelectUsers={canSelectUsers} + isLoadingUsers={showUserFilterLoading} labels={{ period: t.reports?.period || "Period", thisWeek: t.reports?.periodThisWeek || "This week",