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:
parent
0233ff2dce
commit
0a2cafed76
@ -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>
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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();
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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 */}
|
||||||
|
|||||||
@ -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");
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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[];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user