- Added new fields for internet eligibility in the environment validation schema to support Salesforce integration. - Updated CatalogCdcSubscriber to extract and handle additional eligibility details from Salesforce events. - Refactored InternetEligibilityController to return detailed eligibility information, improving user experience. - Enhanced CatalogCacheService with a new method for setting eligibility details, optimizing cache management. - Updated InternetCatalogService to retrieve and process comprehensive eligibility data, ensuring accurate service availability checks. - Improved public-facing components to reflect the new eligibility status and provide clearer user guidance during the checkout process.
177 lines
6.4 KiB
TypeScript
177 lines
6.4 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useRef, useState } from "react";
|
||
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||
|
||
import { useAuthStore } from "@/features/auth/services/auth.store";
|
||
import { useDashboardSummary, useDashboardTasks } from "@/features/dashboard/hooks";
|
||
import { TaskList, QuickStats, ActivityFeed } from "@/features/dashboard/components";
|
||
import { ErrorState } from "@/components/atoms/error-state";
|
||
import { PageLayout } from "@/components/templates";
|
||
import { cn } from "@/lib/utils";
|
||
import { InlineToast } from "@/components/atoms/inline-toast";
|
||
import { useInternetEligibility } from "@/features/catalog/hooks";
|
||
|
||
export function DashboardView() {
|
||
const { user, isAuthenticated, loading: authLoading, clearLoading } = useAuthStore();
|
||
const hideToastTimeout = useRef<number | null>(null);
|
||
const [eligibilityToast, setEligibilityToast] = useState<{
|
||
visible: boolean;
|
||
text: string;
|
||
tone: "info" | "success" | "warning" | "error";
|
||
}>({ visible: false, text: "", tone: "info" });
|
||
|
||
// Clear auth loading state when dashboard loads (after successful login)
|
||
useEffect(() => {
|
||
clearLoading();
|
||
}, [clearLoading]);
|
||
|
||
const { data: summary, isLoading: summaryLoading, error } = useDashboardSummary();
|
||
const { tasks, isLoading: tasksLoading, taskCount } = useDashboardTasks();
|
||
const { data: eligibility } = useInternetEligibility({ enabled: isAuthenticated });
|
||
|
||
// Combined loading state
|
||
const isLoading = authLoading || summaryLoading || !isAuthenticated;
|
||
|
||
useEffect(() => {
|
||
if (!isAuthenticated || !user?.id) return;
|
||
const status = eligibility?.status;
|
||
if (!status) return; // query not ready yet
|
||
|
||
const key = `cp:internet-eligibility:last:${user.id}`;
|
||
const last = localStorage.getItem(key);
|
||
|
||
if (status === "pending") {
|
||
localStorage.setItem(key, "PENDING");
|
||
return;
|
||
}
|
||
|
||
if (status === "eligible" && typeof eligibility?.eligibility === "string") {
|
||
const current = eligibility.eligibility.trim();
|
||
if (last === "PENDING") {
|
||
setEligibilityToast({
|
||
visible: true,
|
||
text: "We’ve finished reviewing your address — you can now choose personalized internet plans.",
|
||
tone: "success",
|
||
});
|
||
if (hideToastTimeout.current) window.clearTimeout(hideToastTimeout.current);
|
||
hideToastTimeout.current = window.setTimeout(() => {
|
||
setEligibilityToast(t => ({ ...t, visible: false }));
|
||
hideToastTimeout.current = null;
|
||
}, 3500);
|
||
}
|
||
localStorage.setItem(key, current);
|
||
}
|
||
}, [eligibility?.eligibility, eligibility?.status, isAuthenticated, user?.id]);
|
||
|
||
useEffect(() => {
|
||
return () => {
|
||
if (hideToastTimeout.current) window.clearTimeout(hideToastTimeout.current);
|
||
};
|
||
}, []);
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<PageLayout title="Dashboard" description="Overview of your account" loading>
|
||
<div className="space-y-8">
|
||
{/* Greeting skeleton */}
|
||
<div className="animate-pulse">
|
||
<div className="h-4 bg-muted rounded w-24 mb-3" />
|
||
<div className="h-10 bg-muted rounded w-56 mb-2" />
|
||
<div className="h-4 bg-muted rounded w-40" />
|
||
</div>
|
||
{/* Tasks skeleton */}
|
||
<div className="space-y-4">
|
||
<div className="h-24 bg-muted rounded-2xl" />
|
||
<div className="h-24 bg-muted rounded-2xl" />
|
||
</div>
|
||
{/* Bottom section skeleton */}
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||
<div className="h-44 bg-muted rounded-2xl" />
|
||
<div className="h-44 bg-muted rounded-2xl" />
|
||
</div>
|
||
</div>
|
||
</PageLayout>
|
||
);
|
||
}
|
||
|
||
// Handle error state
|
||
if (error) {
|
||
const errorMessage =
|
||
typeof error === "string"
|
||
? error
|
||
: error instanceof Error
|
||
? error.message
|
||
: typeof error === "object" && error && "message" in error
|
||
? String((error as { message?: unknown }).message)
|
||
: "An unexpected error occurred";
|
||
|
||
return (
|
||
<PageLayout title="Dashboard" description="Overview of your account">
|
||
<ErrorState title="Error loading dashboard" message={errorMessage} variant="page" />
|
||
</PageLayout>
|
||
);
|
||
}
|
||
|
||
// Get user's display name
|
||
const displayName = user?.firstname || user?.email?.split("@")[0] || "there";
|
||
|
||
// Determine urgency level for task badge
|
||
const hasUrgentTask = tasks.some(t => t.tone === "critical");
|
||
|
||
return (
|
||
<PageLayout title="Dashboard" description="Overview of your account">
|
||
<InlineToast
|
||
visible={eligibilityToast.visible}
|
||
text={eligibilityToast.text}
|
||
tone={eligibilityToast.tone}
|
||
/>
|
||
{/* Greeting Section */}
|
||
<div className="mb-8">
|
||
<p className="text-sm font-medium text-muted-foreground">Welcome back</p>
|
||
<h2 className="text-3xl sm:text-4xl font-bold text-foreground mt-1">{displayName}</h2>
|
||
|
||
{/* Task status badge */}
|
||
{taskCount > 0 ? (
|
||
<div className="flex items-center gap-2 mt-3">
|
||
<span
|
||
className={cn(
|
||
"inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-sm font-medium",
|
||
hasUrgentTask ? "bg-danger/10 text-danger" : "bg-warning/10 text-warning"
|
||
)}
|
||
>
|
||
{hasUrgentTask && <ExclamationTriangleIcon className="h-4 w-4" />}
|
||
{taskCount === 1 ? "1 task needs attention" : `${taskCount} tasks need attention`}
|
||
</span>
|
||
</div>
|
||
) : (
|
||
<p className="text-sm text-muted-foreground mt-2">Everything is up to date</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* Tasks Section - Main focus area */}
|
||
<section className="mb-10" aria-labelledby="tasks-heading">
|
||
<h3 id="tasks-heading" className="sr-only">
|
||
Your Tasks
|
||
</h3>
|
||
<TaskList tasks={tasks} isLoading={tasksLoading} maxTasks={4} />
|
||
</section>
|
||
|
||
{/* Bottom Section: Quick Stats + Recent Activity */}
|
||
<section className="grid grid-cols-1 lg:grid-cols-2 gap-6" aria-label="Account overview">
|
||
<QuickStats
|
||
activeSubscriptions={summary?.stats?.activeSubscriptions ?? 0}
|
||
openCases={summary?.stats?.openCases ?? 0}
|
||
recentOrders={summary?.stats?.recentOrders}
|
||
isLoading={summaryLoading}
|
||
/>
|
||
<ActivityFeed
|
||
activities={summary?.recentActivity || []}
|
||
maxItems={5}
|
||
isLoading={summaryLoading}
|
||
/>
|
||
</section>
|
||
</PageLayout>
|
||
);
|
||
}
|