Refactor currency formatting across multiple components to utilize the new useFormatCurrency hook. This change enhances consistency in currency display and improves maintainability by centralizing currency formatting logic. Updated relevant components to ensure they correctly format amounts with the appropriate currency symbol.

This commit is contained in:
barsa 2025-10-20 14:01:29 +09:00
parent 0233ff2dce
commit 0a2cafed76
10 changed files with 37 additions and 49 deletions

View File

@ -1,9 +1,7 @@
"use client"; "use client";
import React from "react"; import React from "react";
import { Formatting } from "@customer-portal/domain/toolkit"; import { useFormatCurrency } from "@/lib/hooks/useFormatCurrency";
const { formatCurrency } = Formatting;
interface InvoiceTotalsProps { interface InvoiceTotalsProps {
subtotal: number; subtotal: number;
@ -12,7 +10,7 @@ interface InvoiceTotalsProps {
} }
export function InvoiceTotals({ subtotal, tax, total }: InvoiceTotalsProps) { export function InvoiceTotals({ subtotal, tax, total }: InvoiceTotalsProps) {
const fmt = (amount: number) => formatCurrency(amount); const { formatCurrency } = useFormatCurrency();
return ( return (
<div className="bg-gradient-to-br from-slate-50 to-slate-100 rounded-2xl border border-slate-200 shadow-sm overflow-hidden"> <div className="bg-gradient-to-br from-slate-50 to-slate-100 rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
@ -22,13 +20,13 @@ export function InvoiceTotals({ subtotal, tax, total }: InvoiceTotalsProps) {
<div className="space-y-4"> <div className="space-y-4">
<div className="flex justify-between items-center text-slate-600"> <div className="flex justify-between items-center text-slate-600">
<span className="font-medium">Subtotal</span> <span className="font-medium">Subtotal</span>
<span className="font-semibold text-slate-900">{fmt(subtotal)}</span> <span className="font-semibold text-slate-900">{formatCurrency(subtotal)}</span>
</div> </div>
{tax > 0 && ( {tax > 0 && (
<div className="flex justify-between items-center text-slate-600"> <div className="flex justify-between items-center text-slate-600">
<span className="font-medium">Tax</span> <span className="font-medium">Tax</span>
<span className="font-semibold text-slate-900">{fmt(tax)}</span> <span className="font-semibold text-slate-900">{formatCurrency(tax)}</span>
</div> </div>
)} )}
@ -36,7 +34,7 @@ export function InvoiceTotals({ subtotal, tax, total }: InvoiceTotalsProps) {
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-xl font-bold text-slate-900">Total Amount</span> <span className="text-xl font-bold text-slate-900">Total Amount</span>
<div className="flex items-baseline gap-2"> <div className="flex items-baseline gap-2">
<div className="text-3xl font-bold text-slate-900">{fmt(total)}</div> <div className="text-3xl font-bold text-slate-900">{formatCurrency(total)}</div>
<div className="text-lg font-medium text-slate-500">JPY</div> <div className="text-lg font-medium text-slate-500">JPY</div>
</div> </div>
</div> </div>

View File

@ -3,9 +3,8 @@
import Link from "next/link"; import Link from "next/link";
import { CalendarDaysIcon, ChevronRightIcon } from "@heroicons/react/24/outline"; import { CalendarDaysIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
import { format, formatDistanceToNow } from "date-fns"; import { format, formatDistanceToNow } from "date-fns";
import { Formatting } from "@customer-portal/domain/toolkit";
const { formatCurrency, getCurrencyLocale } = Formatting; import { useFormatCurrency } from "@/lib/hooks/useFormatCurrency";
interface UpcomingPaymentBannerProps { interface UpcomingPaymentBannerProps {
invoice: { id: number; amount: number; currency?: string; dueDate: string }; invoice: { id: number; amount: number; currency?: string; dueDate: string };
@ -14,6 +13,8 @@ interface UpcomingPaymentBannerProps {
} }
export function UpcomingPaymentBanner({ invoice, onPay, loading }: UpcomingPaymentBannerProps) { export function UpcomingPaymentBanner({ invoice, onPay, loading }: UpcomingPaymentBannerProps) {
const { formatCurrency } = useFormatCurrency();
return ( return (
<div id="attention" className="bg-white rounded-xl border border-orange-200 shadow-sm p-4"> <div id="attention" className="bg-white rounded-xl border border-orange-200 shadow-sm p-4">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">

View File

@ -27,11 +27,12 @@ import { LoadingStats, LoadingTable } from "@/components/atoms";
import { ErrorState } from "@/components/atoms/error-state"; import { ErrorState } from "@/components/atoms/error-state";
import { Formatting } from "@customer-portal/domain/toolkit"; import { Formatting } from "@customer-portal/domain/toolkit";
const { formatCurrency, getCurrencyLocale } = Formatting; import { useFormatCurrency } from "@/lib/hooks/useFormatCurrency";
import { log } from "@customer-portal/logging"; import { log } from "@customer-portal/logging";
import { useCreateInvoiceSsoLink } from "@/features/billing/hooks/useBilling"; import { useCreateInvoiceSsoLink } from "@/features/billing/hooks/useBilling";
export function DashboardView() { export function DashboardView() {
const { formatCurrency } = useFormatCurrency();
const router = useRouter(); const router = useRouter();
const { user, isAuthenticated, loading: authLoading, clearLoading } = useAuthStore(); const { user, isAuthenticated, loading: authLoading, clearLoading } = useAuthStore();

View File

@ -15,10 +15,9 @@ import {
import { StatusPill } from "@/components/atoms/status-pill"; import { StatusPill } from "@/components/atoms/status-pill";
import { Button } from "@/components/atoms/button"; import { Button } from "@/components/atoms/button";
import { SubCard } from "@/components/molecules/SubCard/SubCard"; import { SubCard } from "@/components/molecules/SubCard/SubCard";
import { Formatting } from "@customer-portal/domain/toolkit";
import type { Subscription } from "@customer-portal/domain/subscriptions"; import type { Subscription } from "@customer-portal/domain/subscriptions";
const { formatCurrency, getCurrencyLocale } = Formatting; import { useFormatCurrency } from "@/lib/hooks/useFormatCurrency";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
interface SubscriptionCardProps { interface SubscriptionCardProps {
@ -78,6 +77,8 @@ const getBillingCycleLabel = (cycle: string) => {
export const SubscriptionCard = forwardRef<HTMLDivElement, SubscriptionCardProps>( export const SubscriptionCard = forwardRef<HTMLDivElement, SubscriptionCardProps>(
({ subscription, variant = "list", showActions = true, onViewClick, className }, ref) => { ({ subscription, variant = "list", showActions = true, onViewClick, className }, ref) => {
const { formatCurrency } = useFormatCurrency();
const handleViewClick = () => { const handleViewClick = () => {
if (onViewClick) { if (onViewClick) {
onViewClick(subscription); onViewClick(subscription);

View File

@ -15,10 +15,9 @@ import {
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { StatusPill } from "@/components/atoms/status-pill"; import { StatusPill } from "@/components/atoms/status-pill";
import { SubCard } from "@/components/molecules/SubCard/SubCard"; import { SubCard } from "@/components/molecules/SubCard/SubCard";
import { Formatting } from "@customer-portal/domain/toolkit";
import type { Subscription } from "@customer-portal/domain/subscriptions"; import type { Subscription } from "@customer-portal/domain/subscriptions";
const { formatCurrency, getCurrencyLocale } = Formatting; import { useFormatCurrency } from "@/lib/hooks/useFormatCurrency";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
interface SubscriptionDetailsProps { interface SubscriptionDetailsProps {
@ -104,6 +103,8 @@ const isVpnService = (productName: string) => {
export const SubscriptionDetails = forwardRef<HTMLDivElement, SubscriptionDetailsProps>( export const SubscriptionDetails = forwardRef<HTMLDivElement, SubscriptionDetailsProps>(
({ subscription, showServiceSpecificSections = true, className }, ref) => { ({ subscription, showServiceSpecificSections = true, className }, ref) => {
const { formatCurrency } = useFormatCurrency();
return ( return (
<div ref={ref} className={cn("space-y-6", className)}> <div ref={ref} className={cn("space-y-6", className)}>
{/* Main Details Card */} {/* Main Details Card */}

View File

@ -26,10 +26,11 @@ import { useSubscriptions, useSubscriptionStats } from "@/features/subscriptions
import { Formatting } from "@customer-portal/domain/toolkit"; import { Formatting } from "@customer-portal/domain/toolkit";
import type { Subscription } from "@customer-portal/domain/subscriptions"; import type { Subscription } from "@customer-portal/domain/subscriptions";
const { formatCurrency, getCurrencyLocale } = Formatting; import { useFormatCurrency } from "@/lib/hooks/useFormatCurrency";
export function SubscriptionsListContainer() { export function SubscriptionsListContainer() {
const router = useRouter(); const router = useRouter();
const { formatCurrency } = useFormatCurrency();
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("all"); const [statusFilter, setStatusFilter] = useState("all");

View File

@ -4,7 +4,7 @@ import { useCurrency } from "@/lib/hooks/useCurrency";
import { formatCurrency as baseFormatCurrency } from "@customer-portal/domain/toolkit"; import { formatCurrency as baseFormatCurrency } from "@customer-portal/domain/toolkit";
export function useFormatCurrency() { export function useFormatCurrency() {
const { currencyCode, loading, error } = useCurrency(); const { currencyCode, currencySymbol, loading, error } = useCurrency();
const formatCurrency = (amount: number) => { const formatCurrency = (amount: number) => {
if (loading) { if (loading) {
@ -14,11 +14,11 @@ export function useFormatCurrency() {
if (error) { if (error) {
// Fallback to JPY if there's an error // Fallback to JPY if there's an error
return baseFormatCurrency(amount, "JPY"); return baseFormatCurrency(amount, "JPY", "¥");
} }
// Use the currency from WHMCS API // Use the currency from WHMCS API
return baseFormatCurrency(amount, currencyCode); return baseFormatCurrency(amount, currencyCode, currencySymbol);
}; };
return { return {

View File

@ -19,7 +19,7 @@ class CurrencyServiceImpl implements CurrencyService {
if (!response.data) { if (!response.data) {
throw new Error("Failed to get default currency"); throw new Error("Failed to get default currency");
} }
return response.data; return response.data as CurrencyInfo;
} }
async getAllCurrencies(): Promise<CurrencyInfo[]> { async getAllCurrencies(): Promise<CurrencyInfo[]> {
@ -27,7 +27,7 @@ class CurrencyServiceImpl implements CurrencyService {
if (!response.data) { if (!response.data) {
throw new Error("Failed to get currencies"); throw new Error("Failed to get currencies");
} }
return response.data; return response.data as CurrencyInfo[];
} }
} }

View File

@ -8,36 +8,33 @@
export type SupportedCurrency = "JPY" | "USD" | "EUR"; export type SupportedCurrency = "JPY" | "USD" | "EUR";
/** /**
* Format a number as currency using WHMCS default currency * Format a number as currency using WHMCS currency data
* *
* @param amount - The numeric amount to format * @param amount - The numeric amount to format
* @param currencyCode - Optional currency code (defaults to WHMCS default) * @param currencyCode - Currency code from WHMCS API (e.g., "JPY", "USD", "EUR")
* @param locale - Optional locale (defaults to currency-specific locale) * @param currencyPrefix - Currency symbol from WHMCS API (e.g., "¥", "$", "€")
* *
* @example * @example
* formatCurrency(1000) // Uses WHMCS default currency * formatCurrency(1000, "JPY", "¥") // ¥1,000
* formatCurrency(1000, "USD") // Uses specific currency * formatCurrency(1000, "USD", "$") // $1,000.00
* formatCurrency(1000, "JPY", "ja-JP") // Uses specific currency and locale * formatCurrency(1000, "EUR", "€") // €1,000.00
*/ */
export function formatCurrency( export function formatCurrency(
amount: number, amount: number,
currencyCode: string = "JPY", currencyCode: string,
locale?: string currencyPrefix: string
): string { ): string {
// Use provided locale or get from currency
const currencyLocale = locale || getCurrencyLocale(currencyCode as SupportedCurrency);
// Determine fraction digits based on currency // Determine fraction digits based on currency
const fractionDigits = currencyCode === "JPY" ? 0 : 2; const fractionDigits = currencyCode === "JPY" ? 0 : 2;
const formatter = new Intl.NumberFormat(currencyLocale, { // Format the number with appropriate decimal places
style: "currency", const formattedAmount = amount.toLocaleString("en-US", {
currency: currencyCode,
minimumFractionDigits: fractionDigits, minimumFractionDigits: fractionDigits,
maximumFractionDigits: fractionDigits, maximumFractionDigits: fractionDigits,
}); });
return formatter.format(amount); // Add currency prefix
return `${currencyPrefix}${formattedAmount}`;
} }
/** /**
@ -50,15 +47,3 @@ export function parseCurrency(value: string): number | null {
return Number.isFinite(parsed) ? parsed : null; return Number.isFinite(parsed) ? parsed : null;
} }
/**
* Get the locale string for a given currency
*/
export function getCurrencyLocale(currency: SupportedCurrency = "JPY"): string {
const localeMap: Record<SupportedCurrency, string> = {
JPY: "ja-JP",
USD: "en-US",
EUR: "de-DE",
};
return localeMap[currency] || "en-US";
}

View File

@ -9,7 +9,7 @@ export * as Validation from "./validation/index";
export * as Typing from "./typing/index"; export * as Typing from "./typing/index";
// Re-export commonly used utilities for convenience // Re-export commonly used utilities for convenience
export { formatCurrency, getCurrencyLocale } from "./formatting/currency"; export { formatCurrency } from "./formatting/currency";
export type { SupportedCurrency } from "./formatting/currency"; export type { SupportedCurrency } from "./formatting/currency";
// Re-export AsyncState types and helpers // Re-export AsyncState types and helpers