Refactor Salesforce Opportunity Integration for Enhanced Cancellation Handling
- Updated opportunity field mappings to align with new requirements for SIM and Internet cancellations, improving data accuracy. - Introduced distinct data structures for SIM and Internet cancellations, enhancing type safety and validation processes. - Refactored SalesforceOpportunityService to manage updates for both cancellation types, ensuring precise data handling. - Enhanced cancellation query fields to accommodate new SIM cancellation specifications, streamlining the cancellation workflow. - Cleaned up portal integration to reflect updated opportunity source fields, promoting better data integrity and tracking.
This commit is contained in:
parent
6096c15659
commit
922fd3dab0
@ -0,0 +1,478 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { PageLayout, type BreadcrumbItem } from "@/components/templates/PageLayout";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
||||
import { Button } from "@/components/atoms";
|
||||
import { ArrowLeftIcon, CheckIcon, ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export type Step = 1 | 2 | 3;
|
||||
|
||||
export interface CancellationMonth {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface CancellationFlowProps {
|
||||
/** Page icon */
|
||||
icon: ReactNode;
|
||||
/** Page title */
|
||||
title: string;
|
||||
/** Page description / subtitle */
|
||||
description: string;
|
||||
/** Breadcrumb items */
|
||||
breadcrumbs: BreadcrumbItem[];
|
||||
/** Back link URL */
|
||||
backHref: string;
|
||||
/** Back link label */
|
||||
backLabel: string;
|
||||
/** Available cancellation months */
|
||||
availableMonths: CancellationMonth[];
|
||||
/** Customer email for confirmation */
|
||||
customerEmail: string;
|
||||
/** Loading state */
|
||||
loading?: boolean;
|
||||
/** Page-level blocking error */
|
||||
error?: string | null;
|
||||
/** Success message after submission */
|
||||
successMessage?: string | null;
|
||||
/** Non-blocking error message */
|
||||
formError?: string | null;
|
||||
/** Submitting state */
|
||||
submitting?: boolean;
|
||||
/** Service info to display in step 1 */
|
||||
serviceInfo: ReactNode;
|
||||
/** Notice/terms content for step 2 */
|
||||
termsContent: ReactNode;
|
||||
/** Summary content for step 3 (before email and comments) */
|
||||
summaryContent: ReactNode;
|
||||
/** Optional extra content for step 3 (after summary, before email) */
|
||||
step3ExtraContent?: ReactNode;
|
||||
/** Optional warning banner (e.g., minimum contract term) */
|
||||
warningBanner?: ReactNode;
|
||||
/** Confirmation message shown in the browser confirm dialog */
|
||||
confirmMessage: string;
|
||||
/** Called when user submits the cancellation */
|
||||
onSubmit: (data: {
|
||||
cancellationMonth: string;
|
||||
confirmRead: boolean;
|
||||
confirmCancel: boolean;
|
||||
comments?: string;
|
||||
}) => void | Promise<void>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Shared Components
|
||||
// ============================================================================
|
||||
|
||||
const STEP_LABELS = ["Select Date", "Review Terms", "Confirm"] as const;
|
||||
|
||||
export function Notice({ title, children }: { title: string; children: ReactNode }) {
|
||||
return (
|
||||
<div className="bg-warning-soft/50 border-l-4 border-warning rounded-r-lg p-4">
|
||||
<div className="text-sm font-medium text-foreground mb-1">{title}</div>
|
||||
<div className="text-sm text-muted-foreground leading-relaxed">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function InfoNotice({ title, children }: { title: string; children: ReactNode }) {
|
||||
return (
|
||||
<div className="p-4 bg-info-soft/50 border-l-4 border-info rounded-r-lg">
|
||||
<div className="text-sm font-medium text-foreground mb-1">{title}</div>
|
||||
<div className="text-sm text-muted-foreground">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StepIndicator({ currentStep }: { currentStep: Step }) {
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-2 mb-8">
|
||||
{STEP_LABELS.map((label, idx) => {
|
||||
const stepNum = (idx + 1) as Step;
|
||||
const isCompleted = stepNum < currentStep;
|
||||
const isCurrent = stepNum === currentStep;
|
||||
|
||||
return (
|
||||
<div key={stepNum} className="flex items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`
|
||||
w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium
|
||||
${isCompleted ? "bg-primary text-primary-foreground" : ""}
|
||||
${isCurrent ? "bg-primary text-primary-foreground ring-4 ring-primary/20" : ""}
|
||||
${!isCompleted && !isCurrent ? "bg-muted text-muted-foreground" : ""}
|
||||
`}
|
||||
>
|
||||
{isCompleted ? <CheckIcon className="w-4 h-4" /> : stepNum}
|
||||
</div>
|
||||
<span
|
||||
className={`text-sm hidden sm:inline ${isCurrent ? "font-medium text-foreground" : "text-muted-foreground"}`}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
{stepNum < 3 && (
|
||||
<div
|
||||
className={`w-8 sm:w-12 h-0.5 mx-2 ${stepNum < currentStep ? "bg-primary" : "bg-border"}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Main Component
|
||||
// ============================================================================
|
||||
|
||||
export function CancellationFlow({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
breadcrumbs,
|
||||
backHref,
|
||||
backLabel,
|
||||
availableMonths,
|
||||
customerEmail,
|
||||
loading = false,
|
||||
error = null,
|
||||
successMessage = null,
|
||||
formError = null,
|
||||
submitting = false,
|
||||
serviceInfo,
|
||||
termsContent,
|
||||
summaryContent,
|
||||
step3ExtraContent,
|
||||
warningBanner,
|
||||
confirmMessage,
|
||||
onSubmit,
|
||||
}: CancellationFlowProps) {
|
||||
const [step, setStep] = useState<Step>(1);
|
||||
const [selectedMonth, setSelectedMonth] = useState<string>("");
|
||||
const [acceptTerms, setAcceptTerms] = useState(false);
|
||||
const [confirmMonthEnd, setConfirmMonthEnd] = useState(false);
|
||||
const [comments, setComments] = useState("");
|
||||
|
||||
const selectedMonthInfo = availableMonths.find(m => m.value === selectedMonth);
|
||||
const canProceedStep2 = !!selectedMonth;
|
||||
const canProceedStep3 = acceptTerms && confirmMonthEnd;
|
||||
|
||||
const handleSubmit = () => {
|
||||
const message = confirmMessage.replace("{month}", selectedMonthInfo?.label || selectedMonth);
|
||||
if (window.confirm(message)) {
|
||||
void onSubmit({
|
||||
cancellationMonth: selectedMonth,
|
||||
confirmRead: acceptTerms,
|
||||
confirmCancel: confirmMonthEnd,
|
||||
comments: comments.trim() || undefined,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const headerActions = step === 3 && canProceedStep3 && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleSubmit}
|
||||
disabled={submitting}
|
||||
loading={submitting}
|
||||
loadingText="Processing…"
|
||||
>
|
||||
Request Cancellation
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<PageLayout
|
||||
icon={icon}
|
||||
title={title}
|
||||
description={description}
|
||||
actions={headerActions}
|
||||
breadcrumbs={breadcrumbs}
|
||||
loading={loading}
|
||||
error={error}
|
||||
>
|
||||
<div className="max-w-2xl mx-auto">
|
||||
{/* Back link */}
|
||||
<Link
|
||||
href={backHref}
|
||||
className="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground mb-6"
|
||||
>
|
||||
<ArrowLeftIcon className="w-4 h-4" />
|
||||
{backLabel}
|
||||
</Link>
|
||||
|
||||
{/* Warning banner (e.g., minimum contract) */}
|
||||
{warningBanner}
|
||||
|
||||
{/* Alerts */}
|
||||
{formError && (
|
||||
<div className="mb-6">
|
||||
<AlertBanner variant="error" title="Unable to proceed" elevated>
|
||||
{formError}
|
||||
</AlertBanner>
|
||||
</div>
|
||||
)}
|
||||
{successMessage && (
|
||||
<div className="mb-6">
|
||||
<AlertBanner variant="success" title="Request submitted" elevated>
|
||||
{successMessage}
|
||||
</AlertBanner>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step indicator */}
|
||||
<StepIndicator currentStep={step} />
|
||||
|
||||
{/* Step content */}
|
||||
<div className="bg-card border border-border rounded-xl p-6 sm:p-8">
|
||||
{step === 1 && (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-foreground mb-1">
|
||||
Select Cancellation Date
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Choose when you would like your service to end.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Service info (passed from parent) */}
|
||||
{serviceInfo}
|
||||
|
||||
{/* Month selection */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="cancelMonth"
|
||||
className="block text-sm font-medium text-foreground mb-2"
|
||||
>
|
||||
Cancellation Month
|
||||
</label>
|
||||
<select
|
||||
id="cancelMonth"
|
||||
value={selectedMonth}
|
||||
onChange={e => {
|
||||
setSelectedMonth(e.target.value);
|
||||
setConfirmMonthEnd(false);
|
||||
}}
|
||||
className="w-full border border-input rounded-lg px-4 py-3 text-sm bg-background text-foreground focus:ring-2 focus:ring-primary focus:border-primary"
|
||||
>
|
||||
<option value="">Select a month...</option>
|
||||
{availableMonths.map(month => (
|
||||
<option key={month.value} value={month.value}>
|
||||
{month.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
Service will end at the end of the selected month.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pt-4">
|
||||
<Button disabled={!canProceedStep2} onClick={() => setStep(2)}>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 2 && (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-foreground mb-1">
|
||||
Review Cancellation Terms
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Please read the following information carefully.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Terms content (passed from parent) */}
|
||||
{termsContent}
|
||||
|
||||
<div className="space-y-4 p-4 bg-muted/50 rounded-lg">
|
||||
<label className="flex items-start gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={acceptTerms}
|
||||
onChange={e => setAcceptTerms(e.target.checked)}
|
||||
className="h-5 w-5 text-primary border-input rounded mt-0.5 focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
<span className="text-sm text-foreground">
|
||||
I have read and understood the cancellation terms above.
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-start gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={confirmMonthEnd}
|
||||
onChange={e => setConfirmMonthEnd(e.target.checked)}
|
||||
disabled={!selectedMonth}
|
||||
className="h-5 w-5 text-primary border-input rounded mt-0.5 focus:ring-2 focus:ring-primary disabled:opacity-50"
|
||||
/>
|
||||
<span className="text-sm text-foreground">
|
||||
I confirm cancellation at the end of{" "}
|
||||
<strong>{selectedMonthInfo?.label || "the selected month"}</strong>.
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between pt-4">
|
||||
<Button variant="ghost" onClick={() => setStep(1)}>
|
||||
Back
|
||||
</Button>
|
||||
<Button disabled={!canProceedStep3} onClick={() => setStep(3)}>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 3 && (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-foreground mb-1">Confirm Your Request</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Review your cancellation details and submit.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Summary content (passed from parent) */}
|
||||
{summaryContent}
|
||||
|
||||
{/* Extra content (e.g., Voice SIM notice) */}
|
||||
{step3ExtraContent}
|
||||
|
||||
{/* Email confirmation */}
|
||||
<div className="p-4 bg-muted/50 rounded-lg">
|
||||
<div className="text-sm text-muted-foreground">Confirmation will be sent to:</div>
|
||||
<div className="text-sm font-medium text-foreground">{customerEmail || "—"}</div>
|
||||
</div>
|
||||
|
||||
{/* Comments */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="comments"
|
||||
className="block text-sm font-medium text-foreground mb-2"
|
||||
>
|
||||
Additional Comments{" "}
|
||||
<span className="font-normal text-muted-foreground">(optional)</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="comments"
|
||||
className="w-full border border-input rounded-lg px-4 py-3 text-sm bg-background text-foreground focus:ring-2 focus:ring-primary focus:border-primary resize-none"
|
||||
rows={3}
|
||||
value={comments}
|
||||
onChange={e => setComments(e.target.value)}
|
||||
placeholder="Any questions or special requests..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between pt-4 border-t border-border">
|
||||
<Button variant="ghost" onClick={() => setStep(2)}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleSubmit}
|
||||
disabled={submitting || !canProceedStep3}
|
||||
loading={submitting}
|
||||
loadingText="Processing…"
|
||||
>
|
||||
Request Cancellation
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper Components for Service Info
|
||||
// ============================================================================
|
||||
|
||||
export function ServiceInfoGrid({ children }: { children: ReactNode }) {
|
||||
return <div className="grid grid-cols-3 gap-4 p-4 bg-muted/50 rounded-lg">{children}</div>;
|
||||
}
|
||||
|
||||
export function ServiceInfoItem({
|
||||
label,
|
||||
value,
|
||||
mono = false,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
mono?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground uppercase tracking-wide">{label}</div>
|
||||
<div
|
||||
className={`text-sm font-medium text-foreground mt-0.5 ${mono ? "font-mono text-xs" : ""}`}
|
||||
>
|
||||
{value || "—"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper Components for Summary
|
||||
// ============================================================================
|
||||
|
||||
export function CancellationSummary({
|
||||
items,
|
||||
selectedMonth,
|
||||
}: {
|
||||
items: Array<{ label: string; value: string }>;
|
||||
selectedMonth: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="p-4 bg-primary/5 border border-primary/20 rounded-lg">
|
||||
<div className="text-sm font-medium text-foreground mb-3">Cancellation Summary</div>
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
{items.map(item => (
|
||||
<div key={item.label}>
|
||||
<span className="text-muted-foreground">{item.label}:</span>
|
||||
<div className="font-medium text-foreground">{item.value}</div>
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
<span className="text-muted-foreground">Effective Date:</span>
|
||||
<div className="font-medium text-foreground">End of {selectedMonth}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Warning Banner Component
|
||||
// ============================================================================
|
||||
|
||||
export function MinimumContractWarning({ endDate }: { endDate: string }) {
|
||||
return (
|
||||
<div className="mb-6 flex items-start gap-3 p-4 bg-danger-soft border border-danger/20 rounded-lg">
|
||||
<ExclamationTriangleIcon className="w-5 h-5 text-danger flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<div className="text-sm font-medium text-foreground">Minimum Contract Term Warning</div>
|
||||
<div className="text-sm text-muted-foreground mt-1">
|
||||
Your subscription is within the minimum contract period (ends {endDate}). Early
|
||||
cancellation may incur additional charges.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
export {
|
||||
CancellationFlow,
|
||||
Notice,
|
||||
InfoNotice,
|
||||
ServiceInfoGrid,
|
||||
ServiceInfoItem,
|
||||
CancellationSummary,
|
||||
MinimumContractWarning,
|
||||
type CancellationFlowProps,
|
||||
type CancellationMonth,
|
||||
type Step,
|
||||
} from "./CancellationFlow";
|
||||
Loading…
x
Reference in New Issue
Block a user