barsa 7ab5e12051 Add Residence Card Submission and Verification Features
- Introduced ResidenceCardSubmission model to handle user submissions of residence cards, including status tracking and file management.
- Updated User model to include a relation to ResidenceCardSubmission for better user data management.
- Enhanced the checkout process to require residence card submission for SIM orders, improving compliance and verification.
- Integrated VerificationModule into the application, updating relevant modules and routes to support new verification features.
- Refactored various components and services to utilize the new residence card functionality, ensuring a seamless user experience.
- Updated public-facing views to guide users through the residence card submission process, enhancing clarity and accessibility.
2025-12-18 18:12:20 +09:00

176 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 current = eligibility?.eligibility;
if (current === undefined) return; // query not ready yet
const key = `cp:internet-eligibility:last:${user.id}`;
const last = localStorage.getItem(key);
if (current === null) {
localStorage.setItem(key, "PENDING");
return;
}
if (typeof current === "string" && current.trim().length > 0) {
if (last === "PENDING") {
setEligibilityToast({
visible: true,
text: "Weve 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, 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>
);
}