Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 24 additions & 15 deletions apps/csm-portal/backend/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3384,21 +3384,30 @@ components:
type: string
description: Root-cause category for a closed or solution-proposed case.
enum:
- USER_MISUNDERSTANDING_CONCEPTS
- USER_MISUNDERSTANDING_DOCUMENTATION
- USER_NOT_FOLLOWING_DOCUMENTATION
- USER_MISTAKE
- SOLUTION_PROBLEMATIC_SOLUTION_ARCHITECTURE
- SOLUTION_PROBLEMATIC_CODE
- APPLICATION_BUG
- APPLICATION_MISLEADING_UX_UI
- APPLICATION_LIMITATION
- APPLICATION_MISSING_FEATURE
- APPLICATION_DOCUMENTATION_GAP
- APPLICATION_DOCUMENTATION_ERROR
- INFRASTRUCTURE_CUSTOMERS_SIDE
- INFRASTRUCTURE_SAAS_SIDE_NOT_ENOUGH
- INFRASTRUCTURE_SAAS_SIDE_OTHER
- SOLUTION_ARCHITECTURE
- DEPLOYMENT_ARCHITECTURE
- USER_ERROR_CONFIGURATION
- USER_ERROR_PRODUCT_CONCEPT
- USER_ERROR_RUNTIME
- USER_ERROR_RECOMMENDATION_BEST_PRACTICES
- CUSTOMIZATION_LIMITATION
- CUSTOMIZATION_BUG
- DOCUMENTATION_GAP
- DOCUMENTATION_ERROR
- PRODUCT_LIMITATION
- PRODUCT_BUG
- PRODUCT_REGRESSION
- PRODUCT_MIGRATION
- INFRASTRUCTURE_DATABASE
- INFRASTRUCTURE_OS
- INFRASTRUCTURE_NETWORK
- INFRASTRUCTURE_JDK
- INFRASTRUCTURE_LDAP
- INFRASTRUCTURE_LOAD_BALANCER
- INFRASTRUCTURE_IAAS
- INFRASTRUCTURE_EXTERNAL_PRODUCT
- INFRASTRUCTURE_PROXY
- INFRASTRUCTURE_OTHER
- UNKNOWN

CaseLabelRef:
Expand Down
47 changes: 32 additions & 15 deletions apps/csm-portal/webapp/src/api/backend/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,21 +114,30 @@ export type BeCaseResolutionCode =

/** Root-cause category for a closed or solution-proposed case. Same gating as {@link BeCaseResolutionCode}. */
export type BeCaseCause =
| "USER_MISUNDERSTANDING_CONCEPTS"
| "USER_MISUNDERSTANDING_DOCUMENTATION"
| "USER_NOT_FOLLOWING_DOCUMENTATION"
| "USER_MISTAKE"
| "SOLUTION_PROBLEMATIC_SOLUTION_ARCHITECTURE"
| "SOLUTION_PROBLEMATIC_CODE"
| "APPLICATION_BUG"
| "APPLICATION_MISLEADING_UX_UI"
| "APPLICATION_LIMITATION"
| "APPLICATION_MISSING_FEATURE"
| "APPLICATION_DOCUMENTATION_GAP"
| "APPLICATION_DOCUMENTATION_ERROR"
| "INFRASTRUCTURE_CUSTOMERS_SIDE"
| "INFRASTRUCTURE_SAAS_SIDE_NOT_ENOUGH"
| "INFRASTRUCTURE_SAAS_SIDE_OTHER"
| "SOLUTION_ARCHITECTURE"
| "DEPLOYMENT_ARCHITECTURE"
| "USER_ERROR_CONFIGURATION"
| "USER_ERROR_PRODUCT_CONCEPT"
| "USER_ERROR_RUNTIME"
| "USER_ERROR_RECOMMENDATION_BEST_PRACTICES"
| "CUSTOMIZATION_LIMITATION"
| "CUSTOMIZATION_BUG"
| "DOCUMENTATION_GAP"
| "DOCUMENTATION_ERROR"
| "PRODUCT_LIMITATION"
| "PRODUCT_BUG"
| "PRODUCT_REGRESSION"
| "PRODUCT_MIGRATION"
| "INFRASTRUCTURE_DATABASE"
| "INFRASTRUCTURE_OS"
| "INFRASTRUCTURE_NETWORK"
| "INFRASTRUCTURE_JDK"
| "INFRASTRUCTURE_LDAP"
| "INFRASTRUCTURE_LOAD_BALANCER"
| "INFRASTRUCTURE_IAAS"
| "INFRASTRUCTURE_EXTERNAL_PRODUCT"
| "INFRASTRUCTURE_PROXY"
| "INFRASTRUCTURE_OTHER"
| "UNKNOWN";

export type BeCaseSortField = "createdOn" | "updatedOn" | "severity" | "state";
Expand Down Expand Up @@ -255,6 +264,14 @@ export interface BeCaseView {
createdOn?: string;
updatedOn?: string;
closedOn?: string | null;
/** Timestamp when the case was resolved. Populated for resolved/closed cases; null otherwise. */
resolvedOn?: string | null;
/** Resolution code from a prior close/propose-solution. Populated for resolved/closed cases; null otherwise. */
resolutionCode?: BeCaseResolutionCode | null;
/** Root-cause category from a prior close/propose-solution. Populated for resolved/closed cases; null otherwise. */
cause?: BeCaseCause | null;
/** Free-text resolution/close notes from a prior close/propose-solution. Populated for resolved/closed cases; null otherwise. */
resolutionNotes?: string | null;
}

export interface BeCaseCreatePayload {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@ function detailFromBeCase(
audit: [],
attachments: [],
isWatching: false,
resolution:
c.resolutionCode || c.cause || c.resolutionNotes
? {
resolutionCode: c.resolutionCode ?? undefined,
cause: c.cause ?? undefined,
notes: c.resolutionNotes ?? undefined,
}
: undefined,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ describe("ResolutionDialog", () => {
);
expect(screen.getByRole("button", { name: /close case/i })).toBeDisabled();

chooseOption("resolution-code-label", /solved fixed by support guidance provided/i);
chooseOption("resolution-code-label", /solved.*fixed by support\/guidance provided/i);
expect(screen.getByRole("button", { name: /close case/i })).toBeDisabled();

chooseOption("case-cause-label", /application bug/i);
chooseOption("case-cause-label", /product\/bug/i);
expect(screen.getByRole("button", { name: /close case/i })).not.toBeDisabled();
});

Expand All @@ -68,6 +68,36 @@ describe("ResolutionDialog", () => {
});
});

it("prefills fields from a prior resolution and submits unchanged", () => {
const onSubmit = vi.fn();
render(
<ResolutionDialog
kind="propose_solution"
isSubmitting={false}
initial={{
resolutionCode: "SOLVED_BY_CUSTOMER",
cause: "UNKNOWN",
closeNotes: "Root-caused and verified with the customer.",
}}
onClose={() => {}}
onSubmit={onSubmit}
/>,
);
expect(
screen.getByRole("button", { name: /propose solution/i }),
).not.toBeDisabled();
expect(screen.getByLabelText(/close notes/i)).toHaveValue(
"Root-caused and verified with the customer.",
);

fireEvent.click(screen.getByRole("button", { name: /propose solution/i }));
expect(onSubmit).toHaveBeenCalledWith({
resolutionCode: "SOLVED_BY_CUSTOMER",
cause: "UNKNOWN",
closeNotes: "Root-caused and verified with the customer.",
});
});

it("calls onClose when cancelled", () => {
const onClose = vi.fn();
render(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,25 @@ import { useState, type JSX } from "react";
import type { BeCaseCause, BeCaseResolutionCode } from "@api/backend/types";
import {
CASE_CAUSES,
CASE_CAUSE_LABELS,
RESOLUTION_CODES,
humanizeResolutionEnum,
RESOLUTION_CODE_LABELS,
} from "@features/csm-cases/utils/caseResolution";

interface ResolutionDialogProps {
/** Which lifecycle transition this dialog is confirming. */
kind: "close" | "propose_solution";
isSubmitting: boolean;
/**
* Values from a prior close/propose-solution on this case, when the
* backend has them (e.g. a case reopened after a previous resolution).
* Prefills the fields instead of opening blank.
*/
initial?: {
resolutionCode?: BeCaseResolutionCode;
cause?: BeCaseCause;
closeNotes?: string;
};
onClose: () => void;
onSubmit: (fields: {
resolutionCode: BeCaseResolutionCode;
Expand Down Expand Up @@ -75,12 +86,15 @@ const COPY: Record<
export default function ResolutionDialog({
kind,
isSubmitting,
initial,
onClose,
onSubmit,
}: ResolutionDialogProps): JSX.Element {
const [resolutionCode, setResolutionCode] = useState<BeCaseResolutionCode | "">("");
const [cause, setCause] = useState<BeCaseCause | "">("");
const [closeNotes, setCloseNotes] = useState("");
const [resolutionCode, setResolutionCode] = useState<BeCaseResolutionCode | "">(
initial?.resolutionCode ?? "",
);
const [cause, setCause] = useState<BeCaseCause | "">(initial?.cause ?? "");
const [closeNotes, setCloseNotes] = useState(initial?.closeNotes ?? "");
const copy = COPY[kind];
const canSubmit = !!resolutionCode && !!cause && !isSubmitting;

Expand All @@ -107,7 +121,7 @@ export default function ResolutionDialog({
>
{RESOLUTION_CODES.map((code) => (
<MenuItem key={code} value={code}>
{humanizeResolutionEnum(code)}
{RESOLUTION_CODE_LABELS[code]}
</MenuItem>
))}
</Select>
Expand All @@ -123,7 +137,7 @@ export default function ResolutionDialog({
>
{CASE_CAUSES.map((c) => (
<MenuItem key={c} value={c}>
{humanizeResolutionEnum(c)}
{CASE_CAUSE_LABELS[c]}
</MenuItem>
))}
</Select>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1563,6 +1563,15 @@ export default function CsmCaseDetailPage(): JSX.Element {
<ResolutionDialog
kind={resolutionDialog.kind}
isSubmitting={patchCase.isPending}
initial={
data?.resolution
? {
resolutionCode: data.resolution.resolutionCode,
cause: data.resolution.cause,
closeNotes: data.resolution.notes,
}
: undefined
}
onClose={() => setResolutionDialog(null)}
onSubmit={onResolutionSubmit}
/>
Expand Down
16 changes: 15 additions & 1 deletion apps/csm-portal/webapp/src/features/csm-cases/types/csmCases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ import type {
Severity,
SlaClockType,
} from "@features/csm-dashboard/types/abtDashboard";
import type { BeCaseIssueType, BeCaseType } from "@api/backend/types";
import type {
BeCaseCause,
BeCaseIssueType,
BeCaseResolutionCode,
BeCaseType,
} from "@api/backend/types";

export interface CsmCaseRow {
/**
Expand Down Expand Up @@ -413,4 +418,13 @@ export interface CsmCaseDetail extends CsmCaseRow {
attachments: CaseAttachment[];
/** Whether the current user is watching this case (controls Watch toggle). */
isWatching: boolean;
/**
* Post Resolution Activity from a prior close/propose-solution, when set —
* used to prefill {@link ResolutionDialog} instead of reopening it blank.
*/
resolution?: {
resolutionCode?: BeCaseResolutionCode;
cause?: BeCaseCause;
notes?: string;
};
}
Loading