fix(reports): add controlled fetching + change chart buckets to localized weekday names
This commit is contained in:
@@ -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") {
|
if (lang === "fa") {
|
||||||
return toPersianDigits(String(getPersianDateParts(date).day));
|
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 map = new Map(existing.map((bucket) => [bucket.bucket_key, bucket]));
|
||||||
const result: ReportChartBucket[] = [];
|
const result: ReportChartBucket[] = [];
|
||||||
const cursor = parseIsoDate(fromDate);
|
const cursor = parseIsoDate(fromDate);
|
||||||
@@ -152,7 +164,7 @@ const buildDailyBuckets = (fromDate: string, toDate: string, existing: ReportCha
|
|||||||
result.push(
|
result.push(
|
||||||
existingBucket ?? {
|
existingBucket ?? {
|
||||||
bucket_key: key,
|
bucket_key: key,
|
||||||
bucket_label: getDailyAxisLabel(cursor, lang),
|
bucket_label: getDailyAxisLabel(cursor, lang, period),
|
||||||
total_seconds: 0,
|
total_seconds: 0,
|
||||||
total_duration: "00:00:00",
|
total_duration: "00:00:00",
|
||||||
},
|
},
|
||||||
@@ -162,7 +174,7 @@ const buildDailyBuckets = (fromDate: string, toDate: string, existing: ReportCha
|
|||||||
|
|
||||||
return result.map((bucket) => ({
|
return result.map((bucket) => ({
|
||||||
...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
|
const buckets = useMonthlyBuckets
|
||||||
? buildMonthlyBuckets(data.scope.from_date, data.scope.to_date, data.buckets, lang)
|
? 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 chartMinWidth = Math.max(640, buckets.length * (useMonthlyBuckets ? 92 : 44));
|
||||||
const interval = useMonthlyBuckets ? 0 : buckets.length > 20 ? Math.ceil(buckets.length / 10) - 1 : 0;
|
const interval = useMonthlyBuckets ? 0 : buckets.length > 20 ? Math.ceil(buckets.length / 10) - 1 : 0;
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ export function ReportsFilterBar({
|
|||||||
tags,
|
tags,
|
||||||
users,
|
users,
|
||||||
canSelectUsers,
|
canSelectUsers,
|
||||||
|
isLoadingUsers,
|
||||||
labels,
|
labels,
|
||||||
}: {
|
}: {
|
||||||
value: ReportsFilterDraft;
|
value: ReportsFilterDraft;
|
||||||
@@ -125,6 +126,7 @@ export function ReportsFilterBar({
|
|||||||
tags: Tag[];
|
tags: Tag[];
|
||||||
users: WorkspaceMembership[];
|
users: WorkspaceMembership[];
|
||||||
canSelectUsers: boolean;
|
canSelectUsers: boolean;
|
||||||
|
isLoadingUsers: boolean;
|
||||||
labels: Record<string, string>;
|
labels: Record<string, string>;
|
||||||
}) {
|
}) {
|
||||||
const [draft, setDraft] = useState(value);
|
const [draft, setDraft] = useState(value);
|
||||||
@@ -200,30 +202,36 @@ export function ReportsFilterBar({
|
|||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{canSelectUsers ? (
|
{canSelectUsers || isLoadingUsers ? (
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-[0.14em] text-slate-500 dark:text-slate-400">
|
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-[0.14em] text-slate-500 dark:text-slate-400">
|
||||||
{labels.user}
|
{labels.user}
|
||||||
</label>
|
</label>
|
||||||
<SearchableSelect
|
{isLoadingUsers ? (
|
||||||
value={draft.user}
|
<div className="flex h-10 w-full items-center rounded-2xl border border-slate-200 bg-slate-50 px-3 text-sm text-slate-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-400">
|
||||||
onChange={(user) => setDraft((current) => ({ ...current, user }))}
|
{labels.searchUsers || "Loading users..."}
|
||||||
options={[
|
</div>
|
||||||
{ value: "", label: labels.allUsers },
|
) : (
|
||||||
...users.map((membership) => ({
|
<SearchableSelect
|
||||||
value: membership.user.id,
|
value={draft.user}
|
||||||
label:
|
onChange={(user) => setDraft((current) => ({ ...current, user }))}
|
||||||
`${membership.user.first_name || ""} ${membership.user.last_name || ""}`.trim() ||
|
options={[
|
||||||
membership.user.email ||
|
{ value: "", label: labels.allUsers },
|
||||||
membership.user.id,
|
...users.map((membership) => ({
|
||||||
searchText: membership.user.mobile || "",
|
value: membership.user.id,
|
||||||
})),
|
label:
|
||||||
]}
|
`${membership.user.first_name || ""} ${membership.user.last_name || ""}`.trim() ||
|
||||||
placeholder={labels.allUsers}
|
membership.user.email ||
|
||||||
searchPlaceholder={labels.searchUsers}
|
membership.user.id,
|
||||||
className="w-full"
|
searchText: membership.user.mobile || "",
|
||||||
buttonClassName="h-10 rounded-2xl border border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900"
|
})),
|
||||||
/>
|
]}
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
@@ -164,12 +164,12 @@ export const en = {
|
|||||||
statsOwnersAdmins: "Owners & admins",
|
statsOwnersAdmins: "Owners & admins",
|
||||||
statsGuests: "Guests",
|
statsGuests: "Guests",
|
||||||
membersSectionTitle: "Members",
|
membersSectionTitle: "Members",
|
||||||
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",
|
||||||
mobileNumber: "Mobile Number",
|
mobileNumber: "Mobile Number",
|
||||||
youLabel: "You",
|
youLabel: "You",
|
||||||
resourcesTitle: "Resources",
|
resourcesTitle: "Resources",
|
||||||
resourceOpen: "Open",
|
resourceOpen: "Open",
|
||||||
roleDistributionTitle: "Role distribution",
|
roleDistributionTitle: "Role distribution",
|
||||||
unknownMember: "Unknown member",
|
unknownMember: "Unknown member",
|
||||||
|
|||||||
@@ -96,12 +96,15 @@ export default function Reports() {
|
|||||||
const [dayDetails, setDayDetails] = useState<DayDetailsResponse | null>(null);
|
const [dayDetails, setDayDetails] = useState<DayDetailsResponse | null>(null);
|
||||||
const [openDay, setOpenDay] = useState<string | null>(null);
|
const [openDay, setOpenDay] = useState<string | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isLoadingUsers, setIsLoadingUsers] = useState(false);
|
||||||
const [exportState, setExportState] = useState({
|
const [exportState, setExportState] = useState({
|
||||||
excel: { pending: false, cooldownSeconds: 0 },
|
excel: { pending: false, cooldownSeconds: 0 },
|
||||||
pdf: { pending: false, cooldownSeconds: 0 },
|
pdf: { pending: false, cooldownSeconds: 0 },
|
||||||
});
|
});
|
||||||
|
|
||||||
const canSelectUsers = canWorkspace(activeWorkspace?.my_role, WORKSPACE_MEMBERS_VIEW);
|
const canSelectUsers = canWorkspace(activeWorkspace?.my_role, WORKSPACE_MEMBERS_VIEW);
|
||||||
|
const isWorkspaceRoleResolved = Boolean(activeWorkspace?.my_role);
|
||||||
|
const showUserFilterLoading = !isWorkspaceRoleResolved || (canSelectUsers && isLoadingUsers);
|
||||||
|
|
||||||
const [filters, setFilters] = useState<ReportsFilterDraft>({
|
const [filters, setFilters] = useState<ReportsFilterDraft>({
|
||||||
period: "this_month",
|
period: "this_month",
|
||||||
@@ -117,16 +120,14 @@ export default function Reports() {
|
|||||||
if (!activeWorkspace?.id) return;
|
if (!activeWorkspace?.id) return;
|
||||||
const loadOptions = async () => {
|
const loadOptions = async () => {
|
||||||
try {
|
try {
|
||||||
const [projectsResponse, clientsResponse, tagsResponse, membersResponse] = await Promise.all([
|
const [projectsResponse, clientsResponse, tagsResponse] = await Promise.all([
|
||||||
getProjects(activeWorkspace.id, { limit: 300, offset: 0 }),
|
getProjects(activeWorkspace.id, { limit: 300, offset: 0 }),
|
||||||
getClients(activeWorkspace.id, "", "", 300, 0),
|
getClients(activeWorkspace.id, "", "", 300, 0),
|
||||||
getTags(activeWorkspace.id, { limit: 300, offset: 0 }),
|
getTags(activeWorkspace.id, { limit: 300, offset: 0 }),
|
||||||
fetchWorkspaceMemberships({ workspace: activeWorkspace.id }),
|
|
||||||
]);
|
]);
|
||||||
setProjects(projectsResponse.results || []);
|
setProjects(projectsResponse.results || []);
|
||||||
setClients((clientsResponse.results || []).map((client: { id: string; name: string }) => ({ id: client.id, name: client.name })));
|
setClients((clientsResponse.results || []).map((client: { id: string; name: string }) => ({ id: client.id, name: client.name })));
|
||||||
setTags(tagsResponse.results || []);
|
setTags(tagsResponse.results || []);
|
||||||
setMemberships(membersResponse.results || []);
|
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(t.reports?.loadFiltersError || "Failed to load report filters.");
|
toast.error(t.reports?.loadFiltersError || "Failed to load report filters.");
|
||||||
}
|
}
|
||||||
@@ -134,6 +135,31 @@ export default function Reports() {
|
|||||||
void loadOptions();
|
void loadOptions();
|
||||||
}, [activeWorkspace?.id, t.reports?.loadFiltersError]);
|
}, [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 => {
|
const buildApiFilters = (draft: ReportsFilterDraft): ReportFilters | null => {
|
||||||
if (!activeWorkspace?.id) return null;
|
if (!activeWorkspace?.id) return null;
|
||||||
|
|
||||||
@@ -296,6 +322,7 @@ export default function Reports() {
|
|||||||
tags={tags}
|
tags={tags}
|
||||||
users={memberships}
|
users={memberships}
|
||||||
canSelectUsers={canSelectUsers}
|
canSelectUsers={canSelectUsers}
|
||||||
|
isLoadingUsers={showUserFilterLoading}
|
||||||
labels={{
|
labels={{
|
||||||
period: t.reports?.period || "Period",
|
period: t.reports?.period || "Period",
|
||||||
thisWeek: t.reports?.periodThisWeek || "This week",
|
thisWeek: t.reports?.periodThisWeek || "This week",
|
||||||
|
|||||||
Reference in New Issue
Block a user