Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d4113df
Include durationInMinutes in PATCH payload
dileepapeiris Apr 3, 2026
484ae59
Update sort select label to 'Order By'
dileepapeiris Apr 3, 2026
ad079a3
Add helpers to format service hours/minutes
dileepapeiris Apr 3, 2026
fd5688e
Add required variant to MissingTimezoneDialog
dileepapeiris Apr 3, 2026
eea9645
Update tests to expect 'hrs' formatting
dileepapeiris Apr 3, 2026
062a221
Add durationInMinutes to PatchCallRequest
dileepapeiris Apr 3, 2026
b403edd
Add 'Notes Pending' call request status
dileepapeiris Apr 3, 2026
ffe3fec
Make meeting duration editable and required
dileepapeiris Apr 3, 2026
cd7f5ae
Fix TimeTrackingCard test expectations
dileepapeiris Apr 3, 2026
6bf6a35
Tests: CallsPanel timezone refetch & new modal test
dileepapeiris Apr 3, 2026
81c2a7e
Display preferred call time in user's TZ
dileepapeiris Apr 3, 2026
85ec526
Handle missing timezone flow for calls
dileepapeiris Apr 3, 2026
9322a59
Format service hours as hr/min
dileepapeiris Apr 3, 2026
dae8336
Add tests for hiding actions in certain states
dileepapeiris Apr 3, 2026
437fe7c
Add required-variant test for MissingTimezone
dileepapeiris Apr 3, 2026
58807dd
Add tests for DeleteCallRequestModal
dileepapeiris Apr 3, 2026
cc4d5ce
Update RequestCallModal tests for button behavior
dileepapeiris Apr 3, 2026
60dfc3d
Use formatMinutesAsHrMin for total time display
dileepapeiris Apr 3, 2026
8dc8586
Format service hours using hr/min formatter
dileepapeiris Apr 3, 2026
f5f9cac
Update tests to expect 'hrs' unit formatting
dileepapeiris Apr 3, 2026
b5aafff
Wait for preferred time input in test
dileepapeiris Apr 3, 2026
5cb2a64
Hide customer actions for notes/rejected states
dileepapeiris Apr 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/customer-portal/webapp/src/api/usePatchCallRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ export function usePatchCallRequest(
if (rest.utcTimes != null && rest.utcTimes.length > 0) {
body.utcTimes = rest.utcTimes;
}
if (rest.durationInMinutes != null) {
body.durationInMinutes = rest.durationInMinutes;
}
logger.debug("[usePatchCallRequest] Request payload:", body);

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ import { Clock } from "@wso2/oxygen-ui-icons-react";
import type { JSX } from "react";

import type { ProjectDetails } from "@models/responses";
import { formatProjectDate } from "@utils/projectDetails";
import {
formatProjectDate,
formatServiceHoursDecimalAsHrMin,
} from "@utils/projectDetails";
import ErrorIndicator from "@components/common/error-indicator/ErrorIndicator";

export interface ServiceHoursAllocationsCardProps {
Expand All @@ -44,12 +47,11 @@ function formatHoursDisplay(
const c = Number(consumed ?? 0);
const t = Number(total ?? 0);
const pct = t === 0 ? 0 : Math.round((c / t) * 100);
return `${c}/${t}h (${pct}%)`;
return `${formatServiceHoursDecimalAsHrMin(c)}/${formatServiceHoursDecimalAsHrMin(t)} (${pct}%)`;
}

function formatRemaining(value: number | undefined): string {
if (value == null || typeof value !== "number") return NOT_AVAILABLE;
return `${value}h`;
return formatServiceHoursDecimalAsHrMin(value);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,14 @@ describe("ServiceHoursAllocationsCard", () => {

expect(screen.getByText("Query Hours")).toBeInTheDocument();
expect(screen.getByText("Onboarding Hours")).toBeInTheDocument();
expect(screen.getByText("45/100h (45%)")).toBeInTheDocument();
expect(screen.getByText("180/200h (90%)")).toBeInTheDocument();
expect(screen.getByText("55h")).toBeInTheDocument();
expect(screen.getByText("20h")).toBeInTheDocument();
expect(
screen.getByText("45 hrs/100 hrs (45%)"),
).toBeInTheDocument();
expect(
screen.getByText("180 hrs/200 hrs (90%)"),
).toBeInTheDocument();
expect(screen.getByText("55 hrs")).toBeInTheDocument();
expect(screen.getByText("20 hrs")).toBeInTheDocument();
expect(screen.getByText("Apr 30, 2026")).toBeInTheDocument();
Comment thread
dileepapeiris marked this conversation as resolved.
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ import { Clock } from "@wso2/oxygen-ui-icons-react";
import type { JSX } from "react";

import type { ProjectDetails } from "@models/responses";
import { formatProjectDate } from "@utils/projectDetails";
import {
formatProjectDate,
formatServiceHoursDecimalAsHrMin,
} from "@utils/projectDetails";
import ErrorIndicator from "@components/common/error-indicator/ErrorIndicator";

export interface ServiceHoursStatCardsProps {
Expand All @@ -38,12 +41,11 @@ function formatHoursDisplay(
const c = Number(consumed ?? 0);
const t = Number(total ?? 0);
const pct = t === 0 ? 0 : Math.round((c / t) * 100);
return `${c}/${t}h (${pct}%)`;
return `${formatServiceHoursDecimalAsHrMin(c)}/${formatServiceHoursDecimalAsHrMin(t)} (${pct}%)`;
}

function formatRemaining(value: number | undefined): string {
if (value == null || typeof value !== "number") return NOT_AVAILABLE;
return `${value}h`;
return formatServiceHoursDecimalAsHrMin(value);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
import { Card, Box, Typography, Chip, useTheme } from "@wso2/oxygen-ui";
import { type JSX } from "react";
import type { TimeCard } from "@models/responses";
import { getTimeCardStateColorPath } from "@utils/projectDetails";
import {
formatMinutesAsHrMin,
getTimeCardStateColorPath,
} from "@utils/projectDetails";
import { getSupportOverviewChipSx, getPlainChipSx } from "@utils/support";

interface TimeTrackingCardProps {
Expand All @@ -40,11 +43,7 @@ export default function TimeTrackingCard({
const caseNumber = caseData?.number?.trim() || "--";
const approvedByName = approvedBy?.label?.trim() || "--";

// Convert totalTime from minutes to hours
const totalTimeInHours =
totalTime !== undefined && totalTime !== null
? Math.round((totalTime / 60) * 100) / 100
: null;
const totalTimeDisplay = formatMinutesAsHrMin(totalTime);

const stateColorPath = getTimeCardStateColorPath(state);

Expand Down Expand Up @@ -119,7 +118,7 @@ export default function TimeTrackingCard({
color: "text.primary",
}}
>
{totalTimeInHours !== null ? `${totalTimeInHours} hrs` : "--"}
{totalTimeDisplay === "Not Available" ? "--" : totalTimeDisplay}
</Typography>
</Box>
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,14 @@ describe("ServiceHoursStatCards", () => {

expect(screen.getByText("Query Hours")).toBeInTheDocument();
expect(screen.getByText("Onboarding Hours")).toBeInTheDocument();
expect(screen.getByText("45/100h (45%)")).toBeInTheDocument();
expect(screen.getByText("180/200h (90%)")).toBeInTheDocument();
expect(screen.getByText("55h")).toBeInTheDocument();
expect(screen.getByText("20h")).toBeInTheDocument();
expect(
screen.getByText("45 hrs/100 hrs (45%)"),
).toBeInTheDocument();
expect(
screen.getByText("180 hrs/200 hrs (90%)"),
).toBeInTheDocument();
expect(screen.getByText("55 hrs")).toBeInTheDocument();
expect(screen.getByText("20 hrs")).toBeInTheDocument();
expect(screen.getByText("Apr 30, 2026")).toBeInTheDocument();
Comment thread
dileepapeiris marked this conversation as resolved.
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe("TimeTrackingCard", () => {
expect(screen.getByText("Approved")).toBeInTheDocument();
expect(screen.getByText("Billable")).toBeInTheDocument();
expect(screen.getByText("CS0437343")).toBeInTheDocument();
expect(screen.getByText("1 hrs")).toBeInTheDocument(); // 60 minutes = 1 hour
expect(screen.getByText("1 hr")).toBeInTheDocument(); // 60 minutes = 1 hour
expect(
screen.getByText(/Approved by: Dileepa Peiris \(Intern\)/),
).toBeInTheDocument();
Expand All @@ -60,7 +60,7 @@ describe("TimeTrackingCard", () => {
render(<TimeTrackingCard card={incompleteCard} />);

expect(screen.getByText(/Approved by: --/)).toBeInTheDocument();
expect(screen.getByText(/State:\s*--/)).toBeInTheDocument();
expect(screen.getAllByText("--").length).toBeGreaterThan(0);
});

it("should not render Billable chip when hasBillable is false", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ import {
getCallRequestStatusColor,
resolveColorFromTheme,
} from "@utils/support";
import { CallRequestStatus } from "@constants/supportConstants";
import {
CALL_REQUEST_STATE_CUSTOMER_REJECTED,
CALL_REQUEST_STATE_NOTES_PENDING_ID,
CallRequestStatus,
} from "@constants/supportConstants";

export interface CallRequestCardProps {
call: CallRequest;
Expand Down Expand Up @@ -81,6 +85,15 @@ export default function CallRequestCard({
isCancelled || statusLabel === CallRequestStatus.COMPLETED;
const isPendingOnCustomer =
statusLabel === CallRequestStatus.PENDING_ON_CUSTOMER;
const isNotesPending =
call.state?.id === CALL_REQUEST_STATE_NOTES_PENDING_ID ||
statusLabel === CallRequestStatus.NOTES_PENDING ||
statusLower.includes("notes pending");
const isCustomerRejected =
call.state?.id === String(CALL_REQUEST_STATE_CUSTOMER_REJECTED) ||
statusLabel === CallRequestStatus.REJECTED ||
statusLower.includes("customer rejected");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const hideCustomerActions = isNotesPending || isCustomerRejected;
const colorPath = getCallRequestStatusColor(statusLabel);
const resolvedColor = isCancelled
? theme.palette.error.main
Expand Down Expand Up @@ -197,7 +210,7 @@ export default function CallRequestCard({
Reject
</Button>
</>
) : (
) : hideCustomerActions ? null : (
<>
<Button
variant="contained"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,23 @@ export default function CallsPanel({
const [approveCall, setApproveCall] = useState<CallRequest | null>(null);
const [rejectCall, setRejectCall] = useState<CallRequest | null>(null);
const [isMissingTzDialogOpen, setIsMissingTzDialogOpen] = useState(false);
const [missingTzVariant, setMissingTzVariant] = useState<
"informational" | "required"
>("informational");
const [pendingCallAfterTz, setPendingCallAfterTz] = useState<
| { type: "create" }
| { type: "edit"; call: CallRequest }
| null
>(null);
const [isProfileModalOpen, setIsProfileModalOpen] = useState(false);
const [hasShownTzPrompt, setHasShownTzPrompt] = useState(false);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [callRequestsPage, setCallRequestsPage] = useState(1);

const { data: projectFilters } = useGetProjectFilters(projectId);
const { data: userDetails } = useGetUserDetails();
const { data: userDetails, refetch: refetchUserDetails } =
useGetUserDetails();
const userTimeZone = userDetails?.timeZone || undefined;
Comment thread
dileepapeiris marked this conversation as resolved.

// Derive filtered state keys from project filters (all non-Canceled states for search body)
Expand Down Expand Up @@ -213,16 +222,28 @@ export default function CallsPanel({
};

const handleOpenModal = () => {
setEditCall(null);
setSuccessMessage(null);
setErrorMessage(null);
if (!userDetails?.timeZone?.trim()) {
setPendingCallAfterTz({ type: "create" });
setMissingTzVariant("required");
setIsMissingTzDialogOpen(true);
return;
}
setEditCall(null);
setIsModalOpen(true);
};
const handleCloseModal = () => {
setIsModalOpen(false);
setEditCall(null);
};
const handleEditClick = (call: CallRequest) => {
if (!userDetails?.timeZone?.trim()) {
setPendingCallAfterTz({ type: "edit", call });
setMissingTzVariant("required");
setIsMissingTzDialogOpen(true);
return;
}
setEditCall(call);
setIsModalOpen(true);
};
Expand Down Expand Up @@ -320,7 +341,8 @@ export default function CallsPanel({
// Prompt once per mount when the user has no timezone set and data is loaded
useEffect(() => {
if (hasShownTzPrompt) return;
if (userDetails && !userDetails.timeZone) {
if (userDetails && !userDetails.timeZone?.trim()) {
setMissingTzVariant("required");
setIsMissingTzDialogOpen(true);
setHasShownTzPrompt(true);
}
Expand Down Expand Up @@ -391,6 +413,7 @@ export default function CallsPanel({
<DeleteCallRequestModal
open={!!deleteCall}
call={deleteCall}
userTimeZone={userTimeZone}
onClose={handleCloseDeleteModal}
onConfirm={handleConfirmDelete}
isDeleting={patchCallRequest.isPending}
Expand Down Expand Up @@ -434,6 +457,7 @@ export default function CallsPanel({

<MissingTimezoneDialog
open={isMissingTzDialogOpen}
variant={missingTzVariant}
onClose={() => setIsMissingTzDialogOpen(false)}
onSetTimeZone={() => {
setIsMissingTzDialogOpen(false);
Expand All @@ -443,7 +467,25 @@ export default function CallsPanel({

<UserProfileModal
open={isProfileModalOpen}
onClose={() => setIsProfileModalOpen(false)}
onClose={() => {
setIsProfileModalOpen(false);
void refetchUserDetails().then((result) => {
const tz = result.data?.timeZone?.trim();
if (pendingCallAfterTz && tz) {
if (pendingCallAfterTz.type === "create") {
setEditCall(null);
setIsModalOpen(true);
} else {
setEditCall(pendingCallAfterTz.call);
setIsModalOpen(true);
}
setPendingCallAfterTz(null);
} else if (pendingCallAfterTz && !tz) {
setMissingTzVariant("required");
setIsMissingTzDialogOpen(true);
}
});
}}
Comment thread
dileepapeiris marked this conversation as resolved.
/>
</Stack>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,12 @@ import {
type JSX,
} from "react";
import type { CallRequest } from "@models/responses";
import { formatCallRequestPromptScheduledTime } from "@utils/support";
import { formatUtcToLocal } from "@utils/support";

export interface DeleteCallRequestModalProps {
open: boolean;
call: CallRequest | null;
userTimeZone?: string;
onClose: () => void;
onConfirm: (reason: string) => void;
isDeleting?: boolean;
Expand All @@ -55,6 +56,7 @@ export interface DeleteCallRequestModalProps {
export default function DeleteCallRequestModal({
open,
call,
userTimeZone,
onClose,
onConfirm,
isDeleting = false,
Expand Down Expand Up @@ -93,12 +95,10 @@ export default function DeleteCallRequestModal({

const canConfirm = reason.trim() !== "";

const firstPreferredTime = call?.preferredTimes?.find((t) => t?.trim())?.trim();
const promptWhen =
call != null
? formatCallRequestPromptScheduledTime(
call.preferredTimes,
call.scheduleTime,
)
userTimeZone && firstPreferredTime
? formatUtcToLocal(firstPreferredTime, "short", false, userTimeZone)
: "--";
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const cancelDescription =
call == null
Expand Down Expand Up @@ -144,7 +144,7 @@ export default function DeleteCallRequestModal({
</Typography>
<TextField
id="cancel-call-reason"
label="Reason *"
label="Reason"
placeholder="Enter reason for cancellation..."
value={reason}
onChange={handleReasonChange}
Expand Down
Loading