From 28acdc19da4fc658ed3e8189194b7d31e2683962 Mon Sep 17 00:00:00 2001 From: SasmithaDilshan Date: Thu, 18 Jun 2026 01:26:16 +0530 Subject: [PATCH 1/4] add created/updated date range filters to SR, change requests, and engagements Co-Authored-By: Claude Sonnet 4.6 --- .../components/EngagementsListSection.tsx | 25 ++++++++++- .../features/engagements/utils/engagements.ts | 4 ++ .../operations/pages/ChangeRequestsPage.tsx | 43 ++++++++++++++----- .../operations/types/changeRequests.ts | 8 ++++ .../operations/utils/operationsPages.ts | 8 ++++ 5 files changed, 77 insertions(+), 11 deletions(-) diff --git a/apps/customer-portal/webapp/src/features/engagements/components/EngagementsListSection.tsx b/apps/customer-portal/webapp/src/features/engagements/components/EngagementsListSection.tsx index 10db2a491f..47f664c319 100644 --- a/apps/customer-portal/webapp/src/features/engagements/components/EngagementsListSection.tsx +++ b/apps/customer-portal/webapp/src/features/engagements/components/EngagementsListSection.tsx @@ -29,6 +29,7 @@ import { } from "@wso2/oxygen-ui"; import type { SelectChangeEvent } from "@wso2/oxygen-ui"; import ListSearchBar from "@components/list-view/ListSearchBar"; +import DateRangeFilter from "@components/list-view/DateRangeFilter"; import ListResultsBar from "@components/list-view/ListResultsBar"; import ListPagination from "@components/list-view/ListPagination"; import ListItems from "@components/list-view/ListItems"; @@ -79,6 +80,10 @@ export default function EngagementsListSection({ const activeFiltersCount = countListSearchAndFilters(searchTerm, { statusIds: filters.statusIds, engagementTypeKey: filters.engagementTypeKey, + startCreatedDate: filters.startCreatedDate, + endCreatedDate: filters.endCreatedDate, + startUpdatedDate: filters.startUpdatedDate, + endUpdatedDate: filters.endUpdatedDate, }); return ( @@ -98,7 +103,7 @@ export default function EngagementsListSection({ isLoading={isProjectContextLoading} actionsBeforeClearFilters={actionsBeforeClearFilters} filtersContent={ - + {filterMetadata?.caseStates && ( @@ -189,6 +194,24 @@ export default function EngagementsListSection({ )} + + onFilterChange("startCreatedDate", val ?? "")} + onEndChange={(val) => onFilterChange("endCreatedDate", val ?? "")} + /> + + + onFilterChange("startUpdatedDate", val ?? "")} + onEndChange={(val) => onFilterChange("endUpdatedDate", val ?? "")} + /> + } /> diff --git a/apps/customer-portal/webapp/src/features/engagements/utils/engagements.ts b/apps/customer-portal/webapp/src/features/engagements/utils/engagements.ts index 1f84fc5d9b..bc8fc1933a 100644 --- a/apps/customer-portal/webapp/src/features/engagements/utils/engagements.ts +++ b/apps/customer-portal/webapp/src/features/engagements/utils/engagements.ts @@ -86,6 +86,10 @@ export function buildEngagementSearchRequest( engagementTypeKeys: filters.engagementTypeKey?.length ? filters.engagementTypeKey.flatMap((k) => k.split(",").map(Number).filter(Boolean)) : undefined, + startCreatedDate: filters.startCreatedDate, + endCreatedDate: filters.endCreatedDate, + startUpdatedDate: filters.startUpdatedDate, + endUpdatedDate: filters.endUpdatedDate, }, sortBy: { field: normalizedSortField, diff --git a/apps/customer-portal/webapp/src/features/operations/pages/ChangeRequestsPage.tsx b/apps/customer-portal/webapp/src/features/operations/pages/ChangeRequestsPage.tsx index 253d460df6..d11f09d71c 100644 --- a/apps/customer-portal/webapp/src/features/operations/pages/ChangeRequestsPage.tsx +++ b/apps/customer-portal/webapp/src/features/operations/pages/ChangeRequestsPage.tsx @@ -24,7 +24,8 @@ import { type ChangeEvent, } from "react"; import { useSessionState } from "@hooks/useSessionState"; -import { Box, Divider, Stack } from "@wso2/oxygen-ui"; +import { Box, Divider, Grid, Stack } from "@wso2/oxygen-ui"; +import DateRangeFilter from "@components/list-view/DateRangeFilter"; import type { ChangeRequestFilterValues, ChangeRequestItem } from "@features/operations/types/changeRequests"; import useGetProjectDetails from "@api/useGetProjectDetails"; import useGetProjectFilters from "@api/useGetProjectFilters"; @@ -323,15 +324,37 @@ export default function ChangeRequestsPage(): JSX.Element { activeFiltersCount={countListSearchAndFilters("", filters)} onClearFilters={handleClearFilters} filtersContent={ - - resolveChangeRequestFilterListOptions(def, filterMetadata) - } - onFilterChange={handleFilterChange} - gridSize={{ xs: 12, sm: 6, md: 4 }} - /> + <> + + resolveChangeRequestFilterListOptions(def, filterMetadata) + } + onFilterChange={handleFilterChange} + gridSize={{ xs: 12, sm: 6, md: 4 }} + /> + + + handleFilterChange("startCreatedDate", val ?? "")} + onEndChange={(val) => handleFilterChange("endCreatedDate", val ?? "")} + /> + + + handleFilterChange("startUpdatedDate", val ?? "")} + onEndChange={(val) => handleFilterChange("endUpdatedDate", val ?? "")} + /> + + + } /> )} diff --git a/apps/customer-portal/webapp/src/features/operations/types/changeRequests.ts b/apps/customer-portal/webapp/src/features/operations/types/changeRequests.ts index 74a24ffd18..321f65892b 100644 --- a/apps/customer-portal/webapp/src/features/operations/types/changeRequests.ts +++ b/apps/customer-portal/webapp/src/features/operations/types/changeRequests.ts @@ -100,6 +100,10 @@ export type PatchChangeRequestResponse = AuditMetadata & { export type ChangeRequestFilterValues = { stateIds?: string[]; impactIds?: string[]; + startCreatedDate?: string; + endCreatedDate?: string; + startUpdatedDate?: string; + endUpdatedDate?: string; }; /** Change request list sort field for search API `sortBy.field`. */ @@ -115,6 +119,10 @@ export type ChangeRequestSearchFilters = { stateKeys?: number[]; closedStartDate?: string; closedEndDate?: string; + startCreatedDate?: string; + endCreatedDate?: string; + startUpdatedDate?: string; + endUpdatedDate?: string; }; // Request type for searching change requests. diff --git a/apps/customer-portal/webapp/src/features/operations/utils/operationsPages.ts b/apps/customer-portal/webapp/src/features/operations/utils/operationsPages.ts index bc42e7671a..db162ffb36 100644 --- a/apps/customer-portal/webapp/src/features/operations/utils/operationsPages.ts +++ b/apps/customer-portal/webapp/src/features/operations/utils/operationsPages.ts @@ -228,6 +228,10 @@ export function buildChangeRequestSearchRequest( searchQuery: searchTerm.trim() || undefined, stateKeys, impactKeys: selectedImpactIds.length > 0 ? selectedImpactIds : undefined, + startCreatedDate: filters.startCreatedDate, + endCreatedDate: filters.endCreatedDate, + startUpdatedDate: filters.startUpdatedDate, + endUpdatedDate: filters.endUpdatedDate, }, sortBy: { field: sortField, @@ -314,6 +318,10 @@ export function buildServiceRequestsPageCaseSearchRequest( : undefined, searchQuery: searchTerm.trim() || undefined, createdByMe: createdByMe || undefined, + startCreatedDate: filters.startCreatedDate, + endCreatedDate: filters.endCreatedDate, + startUpdatedDate: filters.startUpdatedDate, + endUpdatedDate: filters.endUpdatedDate, }, sortBy: { field: normalizedSortField, From 93380f73b3139c0cd75c94104f2650c41b0ee8ce Mon Sep 17 00:00:00 2001 From: SasmithaDilshan Date: Thu, 18 Jun 2026 11:40:03 +0530 Subject: [PATCH 2/4] remove date range filters from change requests (API not ready) Co-Authored-By: Claude Sonnet 4.6 --- .../operations/pages/ChangeRequestsPage.tsx | 43 +++++-------------- .../operations/types/changeRequests.ts | 8 ---- .../operations/utils/operationsPages.ts | 4 -- 3 files changed, 10 insertions(+), 45 deletions(-) diff --git a/apps/customer-portal/webapp/src/features/operations/pages/ChangeRequestsPage.tsx b/apps/customer-portal/webapp/src/features/operations/pages/ChangeRequestsPage.tsx index d11f09d71c..253d460df6 100644 --- a/apps/customer-portal/webapp/src/features/operations/pages/ChangeRequestsPage.tsx +++ b/apps/customer-portal/webapp/src/features/operations/pages/ChangeRequestsPage.tsx @@ -24,8 +24,7 @@ import { type ChangeEvent, } from "react"; import { useSessionState } from "@hooks/useSessionState"; -import { Box, Divider, Grid, Stack } from "@wso2/oxygen-ui"; -import DateRangeFilter from "@components/list-view/DateRangeFilter"; +import { Box, Divider, Stack } from "@wso2/oxygen-ui"; import type { ChangeRequestFilterValues, ChangeRequestItem } from "@features/operations/types/changeRequests"; import useGetProjectDetails from "@api/useGetProjectDetails"; import useGetProjectFilters from "@api/useGetProjectFilters"; @@ -324,37 +323,15 @@ export default function ChangeRequestsPage(): JSX.Element { activeFiltersCount={countListSearchAndFilters("", filters)} onClearFilters={handleClearFilters} filtersContent={ - <> - - resolveChangeRequestFilterListOptions(def, filterMetadata) - } - onFilterChange={handleFilterChange} - gridSize={{ xs: 12, sm: 6, md: 4 }} - /> - - - handleFilterChange("startCreatedDate", val ?? "")} - onEndChange={(val) => handleFilterChange("endCreatedDate", val ?? "")} - /> - - - handleFilterChange("startUpdatedDate", val ?? "")} - onEndChange={(val) => handleFilterChange("endUpdatedDate", val ?? "")} - /> - - - + + resolveChangeRequestFilterListOptions(def, filterMetadata) + } + onFilterChange={handleFilterChange} + gridSize={{ xs: 12, sm: 6, md: 4 }} + /> } /> )} diff --git a/apps/customer-portal/webapp/src/features/operations/types/changeRequests.ts b/apps/customer-portal/webapp/src/features/operations/types/changeRequests.ts index 321f65892b..74a24ffd18 100644 --- a/apps/customer-portal/webapp/src/features/operations/types/changeRequests.ts +++ b/apps/customer-portal/webapp/src/features/operations/types/changeRequests.ts @@ -100,10 +100,6 @@ export type PatchChangeRequestResponse = AuditMetadata & { export type ChangeRequestFilterValues = { stateIds?: string[]; impactIds?: string[]; - startCreatedDate?: string; - endCreatedDate?: string; - startUpdatedDate?: string; - endUpdatedDate?: string; }; /** Change request list sort field for search API `sortBy.field`. */ @@ -119,10 +115,6 @@ export type ChangeRequestSearchFilters = { stateKeys?: number[]; closedStartDate?: string; closedEndDate?: string; - startCreatedDate?: string; - endCreatedDate?: string; - startUpdatedDate?: string; - endUpdatedDate?: string; }; // Request type for searching change requests. diff --git a/apps/customer-portal/webapp/src/features/operations/utils/operationsPages.ts b/apps/customer-portal/webapp/src/features/operations/utils/operationsPages.ts index db162ffb36..ddd68fcfe3 100644 --- a/apps/customer-portal/webapp/src/features/operations/utils/operationsPages.ts +++ b/apps/customer-portal/webapp/src/features/operations/utils/operationsPages.ts @@ -228,10 +228,6 @@ export function buildChangeRequestSearchRequest( searchQuery: searchTerm.trim() || undefined, stateKeys, impactKeys: selectedImpactIds.length > 0 ? selectedImpactIds : undefined, - startCreatedDate: filters.startCreatedDate, - endCreatedDate: filters.endCreatedDate, - startUpdatedDate: filters.startUpdatedDate, - endUpdatedDate: filters.endUpdatedDate, }, sortBy: { field: sortField, From 78edc423d77f5b4512ea8dfd495faff3bef6fd60 Mon Sep 17 00:00:00 2001 From: SasmithaDilshan Date: Thu, 18 Jun 2026 14:46:43 +0530 Subject: [PATCH 3/4] add CSV and PDF export to project time tracking Co-Authored-By: Claude Sonnet 4.6 --- .../time-tracking/ProjectTimeTracking.tsx | 15 ++- .../TimeCardsCsvExportButton.tsx | 125 ++++++++++++++++++ .../utils/timeCardsCsvExport.ts | 94 +++++++++++++ .../api/fetchAllCaseTimeCards.ts | 82 ++++++++++++ 4 files changed, 315 insertions(+), 1 deletion(-) create mode 100644 apps/customer-portal/webapp/src/features/project-details/components/time-tracking/TimeCardsCsvExportButton.tsx create mode 100644 apps/customer-portal/webapp/src/features/project-details/utils/timeCardsCsvExport.ts create mode 100644 apps/customer-portal/webapp/src/features/usage-metrics/api/fetchAllCaseTimeCards.ts diff --git a/apps/customer-portal/webapp/src/features/project-details/components/time-tracking/ProjectTimeTracking.tsx b/apps/customer-portal/webapp/src/features/project-details/components/time-tracking/ProjectTimeTracking.tsx index b8ee0ccbe9..11f474ff37 100644 --- a/apps/customer-portal/webapp/src/features/project-details/components/time-tracking/ProjectTimeTracking.tsx +++ b/apps/customer-portal/webapp/src/features/project-details/components/time-tracking/ProjectTimeTracking.tsx @@ -28,6 +28,7 @@ import TimeTrackingCard from "@time-tracking/TimeTrackingCard"; import TimeTrackingCardSkeleton from "@time-tracking/TimeTrackingCardSkeleton"; import TimeTrackingErrorState from "@time-tracking/TimeTrackingErrorState"; import EmptyState from "@components/empty-state/EmptyState"; +import TimeCardsCsvExportButton from "@time-tracking/TimeCardsCsvExportButton"; import type { ProjectTimeTrackingProps } from "@features/project-details/types/projectDetailsComponents"; import { paginateList } from "@features/project-details/utils/timeTrackingPage"; @@ -119,10 +120,22 @@ export default function ProjectTimeTracking({ /> - + Showing {paginatedTimeCards.length} of {totalItems} time cards + {isTimeCardsError ? ( diff --git a/apps/customer-portal/webapp/src/features/project-details/components/time-tracking/TimeCardsCsvExportButton.tsx b/apps/customer-portal/webapp/src/features/project-details/components/time-tracking/TimeCardsCsvExportButton.tsx new file mode 100644 index 0000000000..0d71d5efe4 --- /dev/null +++ b/apps/customer-portal/webapp/src/features/project-details/components/time-tracking/TimeCardsCsvExportButton.tsx @@ -0,0 +1,125 @@ +// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { Button, CircularProgress, Menu, MenuItem } from "@wso2/oxygen-ui"; +import { ChevronDown, Download } from "@wso2/oxygen-ui-icons-react"; +import { useCallback, useRef, useState, type JSX, type MouseEvent } from "react"; +import { useAuthApiClient } from "@hooks/useAuthApiClient"; +import { useErrorBanner } from "@context/error-banner/ErrorBannerContext"; +import { fetchAllCaseTimeCards } from "@features/usage-metrics/api/fetchAllCaseTimeCards"; +import { downloadTimeCardsCsv, downloadTimeCardsPdf } from "@features/project-details/utils/timeCardsCsvExport"; +import type { CaseTimeCard } from "@features/usage-metrics/types/timeTracking"; +import type { TimeCardSearchRequest } from "@features/usage-metrics/types/timeTracking"; + +type ExportFormat = "csv" | "pdf"; + +export type TimeCardsCsvExportButtonProps = { + projectId: string; + projectName?: string; + filters: TimeCardSearchRequest["filters"]; + prefetchedCards?: CaseTimeCard[]; + totalRecords?: number; + disabled?: boolean; +}; + +/** + * Button that exports the current time card list as CSV or PDF. + * + * @param {TimeCardsCsvExportButtonProps} props - Export configuration. + * @returns {JSX.Element} Export button with CSV / PDF options. + */ +export default function TimeCardsCsvExportButton({ + projectId, + projectName, + filters, + prefetchedCards = [], + totalRecords = 0, + disabled = false, +}: TimeCardsCsvExportButtonProps): JSX.Element { + const authFetch = useAuthApiClient(); + const { showError } = useErrorBanner(); + const [exportingFormat, setExportingFormat] = useState(null); + const isExportingRef = useRef(false); + const [anchorEl, setAnchorEl] = useState(null); + + const isExporting = exportingFormat !== null; + const filenamePrefix = projectName?.replace(/\s+/g, "-").toLowerCase() ?? projectId; + + const handleOpen = (event: MouseEvent) => { + if (!isExporting) setAnchorEl(event.currentTarget); + }; + + const handleClose = () => setAnchorEl(null); + + const handleExport = useCallback( + async (format: ExportFormat) => { + if (!projectId || isExportingRef.current) return; + handleClose(); + isExportingRef.current = true; + setExportingFormat(format); + try { + const needsFullFetch = totalRecords > 0 && prefetchedCards.length < totalRecords; + const cards = + needsFullFetch || prefetchedCards.length === 0 + ? await fetchAllCaseTimeCards(authFetch, projectId, filters) + : prefetchedCards; + + if (cards.length === 0) { + showError("No time cards to export for the current filters."); + return; + } + + if (format === "csv") { + downloadTimeCardsCsv(cards, filenamePrefix); + } else { + downloadTimeCardsPdf(cards, filenamePrefix, projectName); + } + } catch (error) { + showError(error instanceof Error ? error.message : "Failed to export time cards."); + } finally { + isExportingRef.current = false; + setExportingFormat(null); + } + }, + [authFetch, filters, filenamePrefix, prefetchedCards, projectId, projectName, showError, totalRecords], + ); + + return ( + <> + + + void handleExport("csv")}>Export to CSV + void handleExport("pdf")}>Export to PDF + + + ); +} diff --git a/apps/customer-portal/webapp/src/features/project-details/utils/timeCardsCsvExport.ts b/apps/customer-portal/webapp/src/features/project-details/utils/timeCardsCsvExport.ts new file mode 100644 index 0000000000..892a5b7ba0 --- /dev/null +++ b/apps/customer-portal/webapp/src/features/project-details/utils/timeCardsCsvExport.ts @@ -0,0 +1,94 @@ +// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import type { CaseTimeCard } from "@features/usage-metrics/types/timeTracking"; +import { formatMinutesAsHrMin } from "@features/project-details/utils/projectDetails"; +import { buildCsvContent, downloadCsvFile } from "@utils/csv"; +import { downloadPdfFile } from "@utils/pdf"; + +export const TIME_CARDS_CSV_HEADERS = [ + "Case Number", + "Case Title", + "Billable Hours", + "Non-billable Hours", + "Total Hours", +] as const; + +function buildTimeCardRows(cards: CaseTimeCard[]): string[][] { + const rows = cards.map((card) => [ + card.case.number, + card.case.name, + formatMinutesAsHrMin(card.billable.totalTime), + formatMinutesAsHrMin(card.nonBillable.totalTime), + formatMinutesAsHrMin(card.totalTime), + ]); + + const totalBillable = cards.reduce((sum, c) => sum + c.billable.totalTime, 0); + const totalNonBillable = cards.reduce((sum, c) => sum + c.nonBillable.totalTime, 0); + const totalAll = cards.reduce((sum, c) => sum + c.totalTime, 0); + + rows.push([ + "Total", + "", + formatMinutesAsHrMin(totalBillable), + formatMinutesAsHrMin(totalNonBillable), + formatMinutesAsHrMin(totalAll), + ]); + + return rows; +} + +function buildFilename(prefix: string, ext: "csv" | "pdf"): string { + const date = new Date().toISOString().slice(0, 10); + return `${prefix}-time-cards-${date}.${ext}`; +} + +/** + * Builds and downloads a CSV file of case time cards. + */ +export function downloadTimeCardsCsv( + cards: CaseTimeCard[], + filenamePrefix: string, +): void { + const rows = buildTimeCardRows(cards); + const content = buildCsvContent([...TIME_CARDS_CSV_HEADERS], rows); + downloadCsvFile(buildFilename(filenamePrefix, "csv"), content); +} + +/** + * Builds and downloads a PDF file of case time cards. + */ +export function downloadTimeCardsPdf( + cards: CaseTimeCard[], + filenamePrefix: string, + projectName?: string, +): void { + const rows = buildTimeCardRows(cards); + const title = projectName ? `Time Tracking — ${projectName}` : "Time Tracking"; + downloadPdfFile( + buildFilename(filenamePrefix, "pdf"), + title, + [...TIME_CARDS_CSV_HEADERS], + rows, + { + 0: { cellWidth: 30 }, + 1: { cellWidth: 80 }, + 2: { cellWidth: 35, halign: "right" }, + 3: { cellWidth: 40, halign: "right" }, + 4: { cellWidth: 30, halign: "right" }, + }, + ); +} diff --git a/apps/customer-portal/webapp/src/features/usage-metrics/api/fetchAllCaseTimeCards.ts b/apps/customer-portal/webapp/src/features/usage-metrics/api/fetchAllCaseTimeCards.ts new file mode 100644 index 0000000000..6406573744 --- /dev/null +++ b/apps/customer-portal/webapp/src/features/usage-metrics/api/fetchAllCaseTimeCards.ts @@ -0,0 +1,82 @@ +// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import type { + CaseTimeCard, + CaseTimeCardSearchResponse, + TimeCardSearchRequest, +} from "@features/usage-metrics/types/timeTracking"; +import type { AuthFetchFn } from "@features/support/api/fetchProjectCaseSearchResults"; + +const PAGE_SIZE = 100; + +/** + * Fetches all pages of case time cards for a project, used for CSV/PDF export. + * + * @param authFetch - Authenticated fetch from `useAuthApiClient`. + * @param projectId - Project ID. + * @param filters - Optional date range and state filters. + * @returns All case time cards matching the filters. + */ +export async function fetchAllCaseTimeCards( + authFetch: AuthFetchFn, + projectId: string, + filters: TimeCardSearchRequest["filters"], +): Promise { + const baseUrl = window.config?.CUSTOMER_PORTAL_BACKEND_BASE_URL; + if (!baseUrl) { + throw new Error("CUSTOMER_PORTAL_BACKEND_BASE_URL is not configured"); + } + + const requestUrl = `${baseUrl}/projects/${projectId}/cases/time-cards/search`; + const allCards: CaseTimeCard[] = []; + let offset = 0; + + for (;;) { + const body: TimeCardSearchRequest = { + filters, + pagination: { limit: PAGE_SIZE, offset }, + }; + + const response = await authFetch(requestUrl, { + method: "POST", + body: JSON.stringify(body), + }); + + if (!response.ok) { + const errorBody = await response.text().catch(() => ""); + throw new Error( + errorBody.trim() || + `Error fetching time cards (${response.status} ${response.statusText})`, + ); + } + + const data: CaseTimeCardSearchResponse = await response.json(); + const page = data.caseTimeCards ?? []; + allCards.push(...page); + + const total = Number.isFinite(data.totalRecords) ? data.totalRecords : allCards.length; + const responseLimit = Number.isFinite(data.limit) && data.limit > 0 ? data.limit : PAGE_SIZE; + const nextOffset = (Number.isFinite(data.offset) ? data.offset : offset) + responseLimit; + + if (page.length === 0 || allCards.length >= total || nextOffset <= offset) { + break; + } + offset = nextOffset; + } + + return allCards; +} From 5e7d97f2fe3c7dd7e10bf1695664eb720f0607de Mon Sep 17 00:00:00 2001 From: SasmithaDilshan Date: Thu, 18 Jun 2026 17:59:31 +0530 Subject: [PATCH 4/4] fix: filename prefix fallback and local date in time card export Co-Authored-By: Claude Sonnet 4.6 --- .../components/time-tracking/TimeCardsCsvExportButton.tsx | 4 +++- .../features/project-details/utils/timeCardsCsvExport.ts | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/customer-portal/webapp/src/features/project-details/components/time-tracking/TimeCardsCsvExportButton.tsx b/apps/customer-portal/webapp/src/features/project-details/components/time-tracking/TimeCardsCsvExportButton.tsx index 0d71d5efe4..a2405e67a0 100644 --- a/apps/customer-portal/webapp/src/features/project-details/components/time-tracking/TimeCardsCsvExportButton.tsx +++ b/apps/customer-portal/webapp/src/features/project-details/components/time-tracking/TimeCardsCsvExportButton.tsx @@ -56,7 +56,9 @@ export default function TimeCardsCsvExportButton({ const [anchorEl, setAnchorEl] = useState(null); const isExporting = exportingFormat !== null; - const filenamePrefix = projectName?.replace(/\s+/g, "-").toLowerCase() ?? projectId; + const filenamePrefix = projectName?.trim() + ? projectName.trim().replace(/\s+/g, "-").toLowerCase() + : projectId; const handleOpen = (event: MouseEvent) => { if (!isExporting) setAnchorEl(event.currentTarget); diff --git a/apps/customer-portal/webapp/src/features/project-details/utils/timeCardsCsvExport.ts b/apps/customer-portal/webapp/src/features/project-details/utils/timeCardsCsvExport.ts index 892a5b7ba0..bfedcbefcf 100644 --- a/apps/customer-portal/webapp/src/features/project-details/utils/timeCardsCsvExport.ts +++ b/apps/customer-portal/webapp/src/features/project-details/utils/timeCardsCsvExport.ts @@ -52,8 +52,11 @@ function buildTimeCardRows(cards: CaseTimeCard[]): string[][] { } function buildFilename(prefix: string, ext: "csv" | "pdf"): string { - const date = new Date().toISOString().slice(0, 10); - return `${prefix}-time-cards-${date}.${ext}`; + const now = new Date(); + const y = now.getFullYear(); + const m = String(now.getMonth() + 1).padStart(2, "0"); + const d = String(now.getDate()).padStart(2, "0"); + return `${prefix}-time-cards-${y}-${m}-${d}.${ext}`; } /**