- Introduced `validateSignup` endpoint in AuthController for customer number validation during signup. - Added `healthCheck` method in AuthService to verify service integrations and database connectivity. - Implemented `getPaymentMethods`, `getPaymentGateways`, and `refreshPaymentMethods` endpoints in InvoicesController for managing user payment options. - Enhanced InvoicesService with methods to invalidate payment methods cache and improved error handling. - Updated currency handling across various services and components to reflect JPY as the default currency. - Added new dependencies in package.json for ESLint configuration.
372 lines
16 KiB
TypeScript
372 lines
16 KiB
TypeScript
"use client";
|
|
import { logger } from "@/lib/logger";
|
|
|
|
import { useState } from "react";
|
|
import Link from "next/link";
|
|
import { DashboardLayout } from "@/components/layout/dashboard-layout";
|
|
import { useAuthStore } from "@/lib/auth/store";
|
|
import { useDashboardSummary } from "@/features/dashboard/hooks";
|
|
|
|
import type { Activity } from "@customer-portal/shared";
|
|
import {
|
|
CreditCardIcon,
|
|
ServerIcon,
|
|
ChatBubbleLeftRightIcon,
|
|
ExclamationTriangleIcon,
|
|
ChevronRightIcon,
|
|
PlusIcon,
|
|
DocumentTextIcon,
|
|
ArrowTrendingUpIcon,
|
|
CalendarDaysIcon,
|
|
BellIcon,
|
|
ClipboardDocumentListIcon,
|
|
} from "@heroicons/react/24/outline";
|
|
import {
|
|
CreditCardIcon as CreditCardIconSolid,
|
|
ServerIcon as ServerIconSolid,
|
|
ChatBubbleLeftRightIcon as ChatBubbleLeftRightIconSolid,
|
|
ClipboardDocumentListIcon as ClipboardDocumentListIconSolid,
|
|
} from "@heroicons/react/24/solid";
|
|
import { format } from "date-fns";
|
|
import { StatCard, QuickAction, DashboardActivityItem } from "@/features/dashboard/components";
|
|
import { formatCurrency, getCurrencyLocale } from "@/utils/currency";
|
|
|
|
export default function DashboardPage() {
|
|
const { user, isAuthenticated, isLoading: authLoading } = useAuthStore();
|
|
const { data: summary, isLoading: summaryLoading, error } = useDashboardSummary();
|
|
|
|
const [paymentLoading, setPaymentLoading] = useState(false);
|
|
const [paymentError, setPaymentError] = useState<string | null>(null);
|
|
|
|
// Handle Pay Now functionality
|
|
const handlePayNow = (invoiceId: number) => {
|
|
setPaymentLoading(true);
|
|
setPaymentError(null);
|
|
|
|
void (async () => {
|
|
try {
|
|
const { createInvoiceSsoLink } = await import("@/hooks/useInvoices");
|
|
const ssoLink = await createInvoiceSsoLink(invoiceId, "pay");
|
|
window.open(ssoLink.url, "_blank", "noopener,noreferrer");
|
|
} catch (error) {
|
|
logger.error("Failed to create payment link:", error);
|
|
setPaymentError(error instanceof Error ? error.message : "Failed to open payment page");
|
|
} finally {
|
|
setPaymentLoading(false);
|
|
}
|
|
})();
|
|
};
|
|
|
|
// Handle activity item clicks
|
|
const handleActivityClick = (activity: Activity) => {
|
|
if (activity.type === "invoice_created" || activity.type === "invoice_paid") {
|
|
// Use the related invoice ID for navigation
|
|
if (activity.relatedId) {
|
|
window.location.href = `/billing/invoices/${activity.relatedId}`;
|
|
}
|
|
}
|
|
};
|
|
|
|
if (authLoading || summaryLoading || !isAuthenticated) {
|
|
return (
|
|
<DashboardLayout>
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
|
<p className="mt-4 text-gray-600">Loading dashboard...</p>
|
|
</div>
|
|
</div>
|
|
</DashboardLayout>
|
|
);
|
|
}
|
|
|
|
// Handle error state
|
|
if (error) {
|
|
return (
|
|
<DashboardLayout>
|
|
<div className="space-y-6">
|
|
<div className="bg-red-50 border border-red-200 rounded-md p-4">
|
|
<div className="flex">
|
|
<div className="flex-shrink-0">
|
|
<ExclamationTriangleIcon className="h-5 w-5 text-red-400" />
|
|
</div>
|
|
<div className="ml-3">
|
|
<h3 className="text-sm font-medium text-red-800">Error loading dashboard</h3>
|
|
<div className="mt-2 text-sm text-red-700">
|
|
{error instanceof Error ? error.message : "An unexpected error occurred"}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</DashboardLayout>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<DashboardLayout>
|
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
{/* Modern Header */}
|
|
<div className="mb-10">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
|
Welcome back, {user?.firstName || user?.email?.split("@")[0] || "User"}!
|
|
</h1>
|
|
<p className="text-lg text-gray-600">Here's your account overview for today</p>
|
|
</div>
|
|
<div className="hidden md:flex items-center space-x-4">
|
|
<button className="inline-flex items-center px-4 py-2 border border-gray-300 rounded-lg shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors">
|
|
<BellIcon className="h-4 w-4 mr-2" />
|
|
Notifications
|
|
</button>
|
|
<Link
|
|
href="/catalog"
|
|
className="inline-flex items-center px-4 py-2 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
|
|
>
|
|
<PlusIcon className="h-4 w-4 mr-2" />
|
|
Order Services
|
|
</Link>
|
|
<Link
|
|
href="/support/new"
|
|
className="inline-flex items-center px-4 py-2 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
|
|
>
|
|
<PlusIcon className="h-4 w-4 mr-2" />
|
|
Get Support
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Modern Stats Grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
|
<StatCard
|
|
title="Active Services"
|
|
value={summary?.stats?.activeSubscriptions || 0}
|
|
icon={ServerIconSolid}
|
|
gradient="from-blue-500 to-cyan-500"
|
|
href="/subscriptions"
|
|
/>
|
|
<StatCard
|
|
title="Recent Orders"
|
|
value={((summary?.stats as Record<string, unknown>)?.recentOrders as number) || 0}
|
|
icon={ClipboardDocumentListIconSolid}
|
|
gradient="from-gray-500 to-gray-600"
|
|
href="/orders"
|
|
/>
|
|
<StatCard
|
|
title="Pending Invoices"
|
|
value={summary?.stats?.unpaidInvoices || 0}
|
|
icon={CreditCardIconSolid}
|
|
gradient={
|
|
(summary?.stats?.unpaidInvoices ?? 0) > 0
|
|
? "from-amber-500 to-orange-500"
|
|
: "from-gray-500 to-gray-600"
|
|
}
|
|
href="/billing/invoices"
|
|
/>
|
|
<StatCard
|
|
title="Support Cases"
|
|
value={summary?.stats?.openCases || 0}
|
|
icon={ChatBubbleLeftRightIconSolid}
|
|
gradient={
|
|
(summary?.stats?.openCases ?? 0) > 0
|
|
? "from-blue-500 to-cyan-500"
|
|
: "from-gray-500 to-gray-600"
|
|
}
|
|
href="/support/cases"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
{/* Main Content Area */}
|
|
<div className="lg:col-span-2 space-y-8">
|
|
{/* Next Invoice Due - Enhanced */}
|
|
{summary?.nextInvoice && (
|
|
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden">
|
|
<div className="bg-gradient-to-r from-amber-500 to-orange-500 px-6 py-4">
|
|
<div className="flex items-center">
|
|
<div className="flex-shrink-0">
|
|
<CalendarDaysIcon className="h-8 w-8 text-white" />
|
|
</div>
|
|
<div className="ml-4">
|
|
<h3 className="text-lg font-semibold text-white">Upcoming Payment</h3>
|
|
<p className="text-amber-100 text-sm">
|
|
Don't forget your next payment
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<Link
|
|
href={`/billing/invoices/${summary.nextInvoice.id}`}
|
|
className="flex-1 mr-4 group cursor-pointer"
|
|
>
|
|
<div className="text-3xl font-bold text-gray-900 mb-1 group-hover:text-blue-600 transition-colors">
|
|
{formatCurrency(summary.nextInvoice.amount, {
|
|
currency: "JPY",
|
|
locale: getCurrencyLocale("JPY"),
|
|
})}
|
|
</div>
|
|
<div className="text-sm text-gray-600 group-hover:text-blue-500 transition-colors">
|
|
Due on {format(new Date(summary.nextInvoice.dueDate), "MMMM d, yyyy")}
|
|
<span className="ml-2 text-blue-600 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
Click to view details →
|
|
</span>
|
|
</div>
|
|
</Link>
|
|
<button
|
|
onClick={() => handlePayNow(summary.nextInvoice!.id)}
|
|
disabled={paymentLoading}
|
|
className="inline-flex items-center px-6 py-3 border border-transparent text-sm font-medium rounded-lg text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{paymentLoading ? (
|
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
|
) : null}
|
|
{paymentLoading ? "Opening Payment..." : "Pay Now"}
|
|
{!paymentLoading && <ChevronRightIcon className="ml-2 h-4 w-4" />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Payment Error Display */}
|
|
{paymentError && (
|
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
|
<div className="flex">
|
|
<div className="flex-shrink-0">
|
|
<ExclamationTriangleIcon className="h-5 w-5 text-red-400" />
|
|
</div>
|
|
<div className="ml-3">
|
|
<h3 className="text-sm font-medium text-red-800">Payment Error</h3>
|
|
<div className="mt-2 text-sm text-red-700">{paymentError}</div>
|
|
<div className="mt-3">
|
|
<button
|
|
onClick={() => setPaymentError(null)}
|
|
className="text-sm text-red-600 hover:text-red-500 font-medium"
|
|
>
|
|
Dismiss
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Recent Activity - Modern Timeline */}
|
|
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden">
|
|
<div className="px-6 py-4 border-b border-gray-100">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-lg font-semibold text-gray-900">Recent Activity</h3>
|
|
<div className="flex items-center space-x-4">
|
|
<span className="text-sm text-gray-500">Last 30 days</span>
|
|
<Link
|
|
href="/activity"
|
|
className="text-sm text-blue-600 hover:text-blue-700 font-medium transition-colors"
|
|
>
|
|
View All
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="p-6">
|
|
{summary?.recentActivity && summary.recentActivity.length > 0 ? (
|
|
<div className="space-y-6">
|
|
{summary.recentActivity.slice(0, 5).map(activity => {
|
|
const isClickable =
|
|
activity.type === "invoice_created" || activity.type === "invoice_paid";
|
|
return (
|
|
<DashboardActivityItem
|
|
key={activity.id}
|
|
id={activity.id}
|
|
type={activity.type}
|
|
title={activity.title ?? ""}
|
|
description={activity.description ?? ""}
|
|
date={format(new Date(activity.date), "MMM d, yyyy · h:mm a")}
|
|
onClick={isClickable ? () => handleActivityClick(activity) : undefined}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-12">
|
|
<ArrowTrendingUpIcon className="mx-auto h-12 w-12 text-gray-400" />
|
|
<h3 className="mt-2 text-sm font-medium text-gray-900">No recent activity</h3>
|
|
<p className="mt-1 text-sm text-gray-500">
|
|
Your account activity will appear here.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Sidebar */}
|
|
<div className="space-y-6">
|
|
{/* Quick Actions */}
|
|
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden">
|
|
<div className="px-6 py-4 border-b border-gray-100">
|
|
<h3 className="text-lg font-semibold text-gray-900">Quick Actions</h3>
|
|
</div>
|
|
<div className="p-6 space-y-4">
|
|
<QuickAction
|
|
href="/orders"
|
|
title="Track Orders"
|
|
description="View and track your orders"
|
|
icon={ClipboardDocumentListIcon}
|
|
iconColor="text-blue-600"
|
|
bgColor="bg-blue-50"
|
|
/>
|
|
<QuickAction
|
|
href="/subscriptions"
|
|
title="Manage Services"
|
|
description="View and control your subscriptions"
|
|
icon={ServerIcon}
|
|
iconColor="text-blue-600"
|
|
bgColor="bg-blue-50"
|
|
/>
|
|
<QuickAction
|
|
href="/catalog"
|
|
title="Order Services"
|
|
description="Browse catalog and add services"
|
|
icon={PlusIcon}
|
|
iconColor="text-blue-600"
|
|
bgColor="bg-blue-50"
|
|
/>
|
|
<QuickAction
|
|
href="/billing/invoices"
|
|
title="View Invoices"
|
|
description="Check your billing history"
|
|
icon={DocumentTextIcon}
|
|
iconColor="text-blue-600"
|
|
bgColor="bg-blue-50"
|
|
/>
|
|
<QuickAction
|
|
href="/support/new"
|
|
title="Get Support"
|
|
description="Create a new support ticket"
|
|
icon={ChatBubbleLeftRightIcon}
|
|
iconColor="text-blue-600"
|
|
bgColor="bg-blue-50"
|
|
/>
|
|
<QuickAction
|
|
href="/billing/payments"
|
|
title="Payment Methods"
|
|
description="Manage your payment options"
|
|
icon={CreditCardIcon}
|
|
iconColor="text-blue-600"
|
|
bgColor="bg-blue-50"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</DashboardLayout>
|
|
);
|
|
}
|