- Changed worktree setup command from npm to pnpm for improved package management. - Added SupportModule to app.module.ts and router.config.ts for better support case handling. - Refactored OrderEventsService to utilize OrderUpdateEventPayload for improved type safety. - Updated InvoicesList component to use INVOICE_STATUS for status filtering and improved type definitions. - Enhanced SimActions and SimDetailsCard components to utilize SimStatus for better state management. - Refactored Subscription components to leverage new utility functions for status handling and billing cycle labels. - Improved SupportCasesView with better state management and error handling. - Updated API query keys to include support cases for better data retrieval.
212 lines
7.9 KiB
TypeScript
212 lines
7.9 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
import { formatPlanShort } from "@/lib/utils";
|
|
import {
|
|
DevicePhoneMobileIcon,
|
|
CheckCircleIcon,
|
|
ExclamationTriangleIcon,
|
|
XCircleIcon,
|
|
ClockIcon,
|
|
} from "@heroicons/react/24/outline";
|
|
import type { SimDetails, SimStatus } from "@customer-portal/domain/sim";
|
|
|
|
interface SimDetailsCardProps {
|
|
simDetails: SimDetails;
|
|
isLoading?: boolean;
|
|
error?: string | null;
|
|
embedded?: boolean;
|
|
showFeaturesSummary?: boolean;
|
|
}
|
|
|
|
const STATUS_ICON_MAP: Record<SimStatus, React.ReactNode> = {
|
|
active: <CheckCircleIcon className="h-5 w-5 text-green-500" />,
|
|
suspended: <ExclamationTriangleIcon className="h-5 w-5 text-yellow-500" />,
|
|
cancelled: <XCircleIcon className="h-5 w-5 text-red-500" />,
|
|
pending: <ClockIcon className="h-5 w-5 text-blue-500" />,
|
|
};
|
|
|
|
const STATUS_BADGE_CLASS_MAP: Record<SimStatus, string> = {
|
|
active: "bg-green-100 text-green-800",
|
|
suspended: "bg-yellow-100 text-yellow-800",
|
|
cancelled: "bg-red-100 text-red-800",
|
|
pending: "bg-blue-100 text-blue-800",
|
|
};
|
|
|
|
const formatDate = (value?: string | null) => {
|
|
if (!value) return "-";
|
|
const date = new Date(value);
|
|
return Number.isNaN(date.getTime())
|
|
? value
|
|
: date.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
|
|
};
|
|
|
|
const formatQuota = (remainingMb: number) => {
|
|
if (remainingMb >= 1000) {
|
|
return `${(remainingMb / 1000).toFixed(1)} GB`;
|
|
}
|
|
return `${remainingMb.toFixed(0)} MB`;
|
|
};
|
|
|
|
const FeatureToggleRow = ({ label, enabled }: { label: string; enabled: boolean }) => (
|
|
<div className="flex items-center justify-between py-2 px-3 bg-gray-50 rounded-lg">
|
|
<span className="text-sm text-gray-700">{label}</span>
|
|
<span
|
|
className={`text-xs font-semibold px-2 py-1 rounded-full ${
|
|
enabled ? "bg-green-100 text-green-700" : "bg-gray-200 text-gray-600"
|
|
}`}
|
|
>
|
|
{enabled ? "Enabled" : "Disabled"}
|
|
</span>
|
|
</div>
|
|
);
|
|
|
|
const LoadingCard = ({ embedded }: { embedded: boolean }) => (
|
|
<div
|
|
className={`${embedded ? "" : "bg-white shadow rounded-xl border border-gray-100"} p-6 lg:p-8`}
|
|
>
|
|
<div className="animate-pulse space-y-4">
|
|
<div className="flex items-center gap-4">
|
|
<div className="h-12 w-12 rounded-full bg-gray-200" />
|
|
<div className="flex-1 space-y-2">
|
|
<div className="h-4 bg-gray-200 rounded w-1/2" />
|
|
<div className="h-4 bg-gray-200 rounded w-1/3" />
|
|
</div>
|
|
</div>
|
|
<div className="h-4 bg-gray-200 rounded" />
|
|
<div className="h-4 bg-gray-200 rounded w-5/6" />
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="h-24 bg-gray-200 rounded" />
|
|
<div className="h-24 bg-gray-200 rounded" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const ErrorCard = ({ embedded, message }: { embedded: boolean; message: string }) => (
|
|
<div
|
|
className={`${embedded ? "" : "bg-white shadow rounded-xl border border-red-100"} p-6 lg:p-8`}
|
|
>
|
|
<div className="text-center text-red-600 text-sm">{message}</div>
|
|
</div>
|
|
);
|
|
|
|
export function SimDetailsCard({
|
|
simDetails,
|
|
isLoading = false,
|
|
error = null,
|
|
embedded = false,
|
|
showFeaturesSummary = true,
|
|
}: SimDetailsCardProps) {
|
|
if (isLoading) {
|
|
return <LoadingCard embedded={embedded} />;
|
|
}
|
|
|
|
if (error) {
|
|
return <ErrorCard embedded={embedded} message={error} />;
|
|
}
|
|
|
|
const planName = simDetails.planName || formatPlanShort(simDetails.planCode) || "SIM Plan";
|
|
const statusIcon = STATUS_ICON_MAP[simDetails.status] ?? (
|
|
<DevicePhoneMobileIcon className="h-5 w-5 text-gray-500" />
|
|
);
|
|
const statusClass = STATUS_BADGE_CLASS_MAP[simDetails.status] ?? "bg-gray-100 text-gray-800";
|
|
const containerClasses = embedded ? "" : "bg-white shadow-lg rounded-xl border border-gray-100";
|
|
|
|
return (
|
|
<div className={`${containerClasses} ${embedded ? "" : "p-6 lg:p-8"}`}>
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="rounded-xl bg-blue-50 p-3">
|
|
<DevicePhoneMobileIcon className="h-7 w-7 text-blue-600" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-xl font-semibold text-gray-900">{planName}</h3>
|
|
<p className="text-sm text-gray-600">Account #{simDetails.account}</p>
|
|
</div>
|
|
</div>
|
|
<span className={`inline-flex items-center gap-2 px-3 py-1 rounded-full ${statusClass}`}>
|
|
{statusIcon}
|
|
<span className="text-sm font-medium capitalize">{simDetails.status}</span>
|
|
</span>
|
|
</div>
|
|
|
|
<div className="mt-6 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<div className="space-y-4">
|
|
<section className="bg-gray-50 rounded-lg p-4">
|
|
<h4 className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-3">
|
|
SIM Information
|
|
</h4>
|
|
<dl className="space-y-2 text-sm text-gray-700">
|
|
<div className="flex justify-between">
|
|
<dt className="font-medium text-gray-600">Phone Number</dt>
|
|
<dd className="font-semibold text-gray-900">{simDetails.msisdn}</dd>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<dt className="font-medium text-gray-600">SIM Type</dt>
|
|
<dd className="font-semibold text-gray-900">{simDetails.simType}</dd>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<dt className="font-medium text-gray-600">ICCID</dt>
|
|
<dd className="font-mono text-gray-900 break-all">{simDetails.iccid}</dd>
|
|
</div>
|
|
{simDetails.eid && (
|
|
<div className="flex justify-between">
|
|
<dt className="font-medium text-gray-600">EID</dt>
|
|
<dd className="font-mono text-gray-900 break-all">{simDetails.eid}</dd>
|
|
</div>
|
|
)}
|
|
<div className="flex justify-between">
|
|
<dt className="font-medium text-gray-600">Network Type</dt>
|
|
<dd className="font-semibold text-gray-900">{simDetails.networkType}</dd>
|
|
</div>
|
|
</dl>
|
|
</section>
|
|
|
|
<section className="bg-gray-50 rounded-lg p-4">
|
|
<h4 className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-3">
|
|
Data Remaining
|
|
</h4>
|
|
<p className="text-2xl font-bold text-green-600">
|
|
{formatQuota(simDetails.remainingQuotaMb)}
|
|
</p>
|
|
<p className="text-xs text-gray-500 mt-1">Remaining allowance in current cycle</p>
|
|
</section>
|
|
|
|
<section className="bg-gray-50 rounded-lg p-4">
|
|
<h4 className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-3">
|
|
Activation Timeline
|
|
</h4>
|
|
<dl className="space-y-2 text-sm text-gray-700">
|
|
<div className="flex justify-between">
|
|
<dt className="font-medium text-gray-600">Activated</dt>
|
|
<dd>{formatDate(simDetails.activatedAt)}</dd>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<dt className="font-medium text-gray-600">Expires</dt>
|
|
<dd>{formatDate(simDetails.expiresAt)}</dd>
|
|
</div>
|
|
</dl>
|
|
</section>
|
|
</div>
|
|
|
|
{showFeaturesSummary && (
|
|
<section className="space-y-3">
|
|
<h4 className="text-sm font-semibold text-gray-700 uppercase tracking-wide">
|
|
Service Features
|
|
</h4>
|
|
<FeatureToggleRow label="Voice Mail" enabled={simDetails.voiceMailEnabled} />
|
|
<FeatureToggleRow label="Call Waiting" enabled={simDetails.callWaitingEnabled} />
|
|
<FeatureToggleRow
|
|
label="International Roaming"
|
|
enabled={simDetails.internationalRoamingEnabled}
|
|
/>
|
|
</section>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export type { SimDetails };
|