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";
import React from "react";
import { Formatting } from "@customer-portal/domain/toolkit";
const { formatCurrency } = Formatting;
import { useFormatCurrency } from "@/lib/hooks/useFormatCurrency";
interface InvoiceTotalsProps {
subtotal: number;
@ -12,7 +10,7 @@ interface InvoiceTotalsProps {
}
export function InvoiceTotals({ subtotal, tax, total }: InvoiceTotalsProps) {
const fmt = (amount: number) => formatCurrency(amount);
const { formatCurrency } = useFormatCurrency();
return (
<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="flex justify-between items-center text-slate-600">
<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>
{tax > 0 && (
<div className="flex justify-between items-center text-slate-600">
<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>
)}
@ -36,7 +34,7 @@ export function InvoiceTotals({ subtotal, tax, total }: InvoiceTotalsProps) {
<div className="flex justify-between items-center">
<span className="text-xl font-bold text-slate-900">Total Amount</span>
<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>
</div>

View File

@ -3,9 +3,8 @@
import Link from "next/link";
import { CalendarDaysIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
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 {
invoice: { id: number; amount: number; currency?: string; dueDate: string };
@ -14,6 +13,8 @@ interface UpcomingPaymentBannerProps {
}
export function UpcomingPaymentBanner({ invoice, onPay, loading }: UpcomingPaymentBannerProps) {
const { formatCurrency } = useFormatCurrency();
return (
<div id="attention" className="bg-white rounded-xl border border-orange-200 shadow-sm p-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 { Formatting } from "@customer-portal/domain/toolkit";
const { formatCurrency, getCurrencyLocale } = Formatting;
import { useFormatCurrency } from "@/lib/hooks/useFormatCurrency";
import { log } from "@customer-portal/logging";
import { useCreateInvoiceSsoLink } from "@/features/billing/hooks/useBilling";
export function DashboardView() {
const { formatCurrency } = useFormatCurrency();
const router = useRouter();
const { user, isAuthenticated, loading: authLoading, clearLoading } = useAuthStore();

View File

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

View File

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

View File

@ -26,10 +26,11 @@ import { useSubscriptions, useSubscriptionStats } from "@/features/subscriptions
import { Formatting } from "@customer-portal/domain/toolkit";
import type { Subscription } from "@customer-portal/domain/subscriptions";
const { formatCurrency, getCurrencyLocale } = Formatting;
import { useFormatCurrency } from "@/lib/hooks/useFormatCurrency";
export function SubscriptionsListContainer() {
const router = useRouter();
const { formatCurrency } = useFormatCurrency();
const [searchTerm, setSearchTerm] = useState("");
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";
export function useFormatCurrency() {
const { currencyCode, loading, error } = useCurrency();
const { currencyCode, currencySymbol, loading, error } = useCurrency();
const formatCurrency = (amount: number) => {
if (loading) {
@ -14,11 +14,11 @@ export function useFormatCurrency() {
if (error) {
// Fallback to JPY if there's an error
return baseFormatCurrency(amount, "JPY");
return baseFormatCurrency(amount, "JPY", "¥");
}
// Use the currency from WHMCS API
return baseFormatCurrency(amount, currencyCode);
return baseFormatCurrency(amount, currencyCode, currencySymbol);
};
return {

View File

@ -19,7 +19,7 @@ class CurrencyServiceImpl implements CurrencyService {
if (!response.data) {
throw new Error("Failed to get default currency");
}
return response.data;
return response.data as CurrencyInfo;
}
async getAllCurrencies(): Promise<CurrencyInfo[]> {
@ -27,7 +27,7 @@ class CurrencyServiceImpl implements CurrencyService {
if (!response.data) {
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";
/**
* 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 currencyCode - Optional currency code (defaults to WHMCS default)
* @param locale - Optional locale (defaults to currency-specific locale)
* @param currencyCode - Currency code from WHMCS API (e.g., "JPY", "USD", "EUR")
* @param currencyPrefix - Currency symbol from WHMCS API (e.g., "¥", "$", "€")
*
* @example
* formatCurrency(1000) // Uses WHMCS default currency
* formatCurrency(1000, "USD") // Uses specific currency
* formatCurrency(1000, "JPY", "ja-JP") // Uses specific currency and locale
* formatCurrency(1000, "JPY", "¥") // ¥1,000
* formatCurrency(1000, "USD", "$") // $1,000.00
* formatCurrency(1000, "EUR", "€") // €1,000.00
*/
export function formatCurrency(
amount: number,
currencyCode: string = "JPY",
locale?: string
currencyCode: string,
currencyPrefix: string
): string {
// Use provided locale or get from currency
const currencyLocale = locale || getCurrencyLocale(currencyCode as SupportedCurrency);
// Determine fraction digits based on currency
const fractionDigits = currencyCode === "JPY" ? 0 : 2;
const formatter = new Intl.NumberFormat(currencyLocale, {
style: "currency",
currency: currencyCode,
// Format the number with appropriate decimal places
const formattedAmount = amount.toLocaleString("en-US", {
minimumFractionDigits: 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;
}
/**
* 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";
// Re-export commonly used utilities for convenience
export { formatCurrency, getCurrencyLocale } from "./formatting/currency";
export { formatCurrency } from "./formatting/currency";
export type { SupportedCurrency } from "./formatting/currency";
// Re-export AsyncState types and helpers