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") {
|
||||
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;
|
||||
|
||||
@@ -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<string, string>;
|
||||
}) {
|
||||
const [draft, setDraft] = useState(value);
|
||||
@@ -200,11 +202,16 @@ export function ReportsFilterBar({
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{canSelectUsers ? (
|
||||
{canSelectUsers || isLoadingUsers ? (
|
||||
<div>
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-[0.14em] text-slate-500 dark:text-slate-400">
|
||||
{labels.user}
|
||||
</label>
|
||||
{isLoadingUsers ? (
|
||||
<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">
|
||||
{labels.searchUsers || "Loading users..."}
|
||||
</div>
|
||||
) : (
|
||||
<SearchableSelect
|
||||
value={draft.user}
|
||||
onChange={(user) => setDraft((current) => ({ ...current, user }))}
|
||||
@@ -224,6 +231,7 @@ export function ReportsFilterBar({
|
||||
className="w-full"
|
||||
buttonClassName="h-10 rounded-2xl border border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -96,12 +96,15 @@ export default function Reports() {
|
||||
const [dayDetails, setDayDetails] = useState<DayDetailsResponse | null>(null);
|
||||
const [openDay, setOpenDay] = useState<string | null>(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<ReportsFilterDraft>({
|
||||
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",
|
||||
|
||||
Reference in New Issue
Block a user