tema 1640fae457 Add new payment methods and health check endpoints in Auth and Invoices services
- 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.
2025-08-30 15:10:24 +09:00

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&apos;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&apos;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>
);
}