diff --git a/apps/dashboard/src/components/front-desk/GuestDetailsModal.tsx b/apps/dashboard/src/components/front-desk/GuestDetailsModal.tsx
index 33228e60..0076110f 100644
--- a/apps/dashboard/src/components/front-desk/GuestDetailsModal.tsx
+++ b/apps/dashboard/src/components/front-desk/GuestDetailsModal.tsx
@@ -1,4 +1,5 @@
import { Link } from 'react-router-dom';
+import { formatMoney } from '../../lib/money';
import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { ArrowRightLeft, StickyNote, LogIn, LogOut } from 'lucide-react';
@@ -229,7 +230,7 @@ export default function GuestDetailsModal({
{t('frontDesk.accountSummary')}
- {t('frontDesk.balance')}: ${Number(balance).toFixed(2)}
+ {t('frontDesk.balance')}: {formatMoney(balance)}
@@ -247,7 +248,7 @@ export default function GuestDetailsModal({
{c.description || '—'}
- ${Number(c.amount).toFixed(2)}
+ {formatMoney(c.amount)}
))}
@@ -268,7 +269,7 @@ export default function GuestDetailsModal({
{p.method || '—'}
- ${Number(p.amount).toFixed(2)}
+ {formatMoney(p.amount)}
))}
diff --git a/apps/dashboard/src/components/rates/RatePlanCalendar.tsx b/apps/dashboard/src/components/rates/RatePlanCalendar.tsx
index 25664b8c..3e542186 100644
--- a/apps/dashboard/src/components/rates/RatePlanCalendar.tsx
+++ b/apps/dashboard/src/components/rates/RatePlanCalendar.tsx
@@ -1,4 +1,5 @@
import { useMemo, useState } from 'react';
+import { formatMoney } from '../../lib/money';
import { useQuery } from '@tanstack/react-query';
import { addDays, format } from 'date-fns';
import { api } from '../../lib/api';
@@ -165,8 +166,8 @@ export default function RatePlanCalendar({ ratePlanId, propertyId, baseAmount }:
{days.map((day, i) => (
| {day.date} |
- ${day.baseRate.toFixed(2)} |
- ${day.effectiveRate.toFixed(2)} |
+ {formatMoney(day.baseRate)} |
+ {formatMoney(day.effectiveRate)} |
{day.badges.length === 0 ? (
diff --git a/apps/dashboard/src/context/PropertyContext.tsx b/apps/dashboard/src/context/PropertyContext.tsx
index aecfcde8..78fda6f3 100644
--- a/apps/dashboard/src/context/PropertyContext.tsx
+++ b/apps/dashboard/src/context/PropertyContext.tsx
@@ -2,6 +2,7 @@ import { createContext, useContext, useState, useEffect, type ReactNode } from '
import { useSearchParams } from 'react-router-dom';
import { api, setPropertyId as setApiPropertyId } from '../lib/api';
import { joinPropertyRoom, leavePropertyRoom } from '../lib/socket';
+import { DEFAULT_CURRENCY, setActiveCurrency } from '../lib/money';
import {
PORTFOLIO_MODE_ID,
type PropertySummary,
@@ -10,6 +11,8 @@ import {
interface PropertyContextValue {
propertyId: string | null;
+ /** Active property's ISO 4217 code; falls back to USD before properties load. */
+ currencyCode: string;
setPropertyId: (id: string) => void;
isPortfolioMode: boolean;
properties: PropertySummary[];
@@ -20,6 +23,7 @@ interface PropertyContextValue {
const PropertyContext = createContext ({
propertyId: null,
+ currencyCode: DEFAULT_CURRENCY,
setPropertyId: () => {},
isPortfolioMode: false,
properties: [],
@@ -39,6 +43,12 @@ export function PropertyProvider({ children }: { children: ReactNode }) {
const [propertiesError, setPropertiesError] = useState(null);
const isPortfolioMode = propertyId === PORTFOLIO_MODE_ID;
+ // Portfolio mode spans properties that may not share a currency, so it has no
+ // single answer — fall back rather than assert one property's code over others.
+ const currencyCode =
+ (!isPortfolioMode &&
+ properties.find((p) => p.id === propertyId)?.currencyCode) ||
+ DEFAULT_CURRENCY;
function setPropertyId(id: string) {
setPropertyIdState(id);
@@ -75,6 +85,12 @@ export function PropertyProvider({ children }: { children: ReactNode }) {
// Bootstrap once; propertyId auto-select handled inside the effect.
}, []);
+ // Keep the money formatter's default in step with the active property, the
+ // same way setApiPropertyId keeps the API client in step above.
+ useEffect(() => {
+ setActiveCurrency(currencyCode);
+ }, [currencyCode]);
+
useEffect(() => {
if (isPortfolioMode) {
setApiPropertyId(null);
@@ -91,6 +107,7 @@ export function PropertyProvider({ children }: { children: ReactNode }) {
= {
function AccountingHome() {
const { t } = useTranslation();
- const { propertyId } = useProperty();
+ const { propertyId, currencyCode } = useProperty();
const queryClient = useQueryClient();
const today = format(new Date(), 'yyyy-MM-dd');
const [depositOpen, setDepositOpen] = useState(false);
@@ -149,7 +150,7 @@ function AccountingHome() {
return api.post('/v1/deposits', {
propertyId,
amount: moneyString(depositAmount),
- currencyCode: 'USD',
+ currencyCode,
});
},
onSuccess: () => {
@@ -215,7 +216,7 @@ function AccountingHome() {
propertyId,
name: ledgerName,
paymentTermsDays: ledgerTerms || undefined,
- currencyCode: 'USD',
+ currencyCode,
});
},
onSuccess: () => {
@@ -340,12 +341,12 @@ function AccountingHome() {
{(Object.keys(AGING_LABELS) as (keyof AgingBuckets)[]).map((key) => (
{AGING_LABELS[key]}
- ${Number(report.buckets[key] ?? 0).toFixed(2)}
+ {formatMoney(report.buckets[key] ?? 0)}
))}
{t('accounting.total')}
- ${Number(report.total ?? 0).toFixed(2)}
+ {formatMoney(report.total ?? 0)}
) : (
@@ -396,7 +397,7 @@ function AccountingHome() {
{deposits.slice(0, 8).map((d) => (
-
- ${Number(d.amount).toFixed(2)}
+ {formatMoney(d.amount)}
{d.status}
{d.status === 'held' && (
|
))}
{accounts.length === 0 && (
@@ -200,7 +201,7 @@ function HouseAccountList() {
| {p.name} |
{p.category ?? '—'} |
- ${Number(p.price).toFixed(2)} {p.currencyCode} |
+ {formatMoney(p.price)} {p.currencyCode} |
|
{t('houseAccounts.balance')}
- ${Number(account.balance ?? 0).toFixed(2)}
+ {formatMoney(account.balance ?? 0)}
@@ -432,7 +433,7 @@ function HouseAccountDetail() {
setSellQty(e.target.value)} placeholder={t('houseAccounts.quantity')} className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm" />
diff --git a/apps/dashboard/src/pages/NightAudit.tsx b/apps/dashboard/src/pages/NightAudit.tsx
index 20263954..62d79136 100644
--- a/apps/dashboard/src/pages/NightAudit.tsx
+++ b/apps/dashboard/src/pages/NightAudit.tsx
@@ -1,4 +1,5 @@
import { useState } from 'react';
+import { formatMoney } from '../lib/money';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Moon, Play, Brain, AlertTriangle, Info, XCircle } from 'lucide-react';
import { format } from 'date-fns';
@@ -38,9 +39,9 @@ function auditCount(value: string | number | undefined): number {
return Number.isNaN(n) ? 0 : n;
}
-function formatAuditRevenue(a: AuditResult, empty = '$0.00'): string {
+function formatAuditRevenue(a: AuditResult, currencyCode: string, empty = '—'): string {
const rev = auditRevenue(a);
- return rev != null ? `$${rev.toFixed(2)}` : empty;
+ return rev != null ? formatMoney(rev, currencyCode) : empty;
}
const SEVERITY_ICONS = { critical: XCircle, warning: AlertTriangle, info: Info };
@@ -88,7 +89,7 @@ function AnomalySection({ propertyId }: { propertyId: string }) {
export default function NightAudit() {
const { t } = useTranslation();
- const { propertyId } = useProperty();
+ const { propertyId, currencyCode } = useProperty();
const queryClient = useQueryClient();
const [auditDate, setAuditDate] = useState(format(new Date(), 'yyyy-MM-dd'));
const [lastResult, setLastResult] = useState(null);
@@ -153,7 +154,7 @@ export default function NightAudit() {
-
+
)}
@@ -201,7 +202,7 @@ export default function NightAudit() {
{a.completedAt ? format(new Date(a.completedAt), 'HH:mm:ss') : '—'} |
{auditCount(a.roomChargesPosted)} |
{auditCount(a.noShowsProcessed)} |
- {formatAuditRevenue(a, '—')} |
+ {formatAuditRevenue(a, currencyCode)} |
|
))}
{audits.length === 0 && (
diff --git a/apps/dashboard/src/pages/RatePlans.tsx b/apps/dashboard/src/pages/RatePlans.tsx
index efde20c7..6cc7f7c0 100644
--- a/apps/dashboard/src/pages/RatePlans.tsx
+++ b/apps/dashboard/src/pages/RatePlans.tsx
@@ -1,4 +1,5 @@
import { useState } from 'react';
+import { formatMoney } from '../lib/money';
import { Routes, Route, useNavigate, useParams } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { BadgeDollarSign, Plus, ChevronLeft, Pencil, Trash2 } from 'lucide-react';
@@ -89,7 +90,7 @@ function buildRestrictionBody(form: RestrictionForm) {
// ---- Rate Plan List ----
function RatePlanList() {
const { t } = useTranslation();
- const { propertyId } = useProperty();
+ const { propertyId, currencyCode } = useProperty();
const navigate = useNavigate();
const queryClient = useQueryClient();
const [createOpen, setCreateOpen] = useState(false);
@@ -127,7 +128,7 @@ function RatePlanList() {
type,
baseAmount: moneyString(baseAmount || '0'),
roomTypeId,
- currencyCode: 'USD',
+ currencyCode,
};
if (type === 'derived') {
payload.parentRatePlanId = parentRatePlanId;
@@ -184,7 +185,7 @@ function RatePlanList() {
{p.code} |
|
{p.roomTypeName ?? '—'} |
-
{p.baseAmount != null ? `$${Number(p.baseAmount).toFixed(2)}` : '—'} |
+
{p.baseAmount != null ? formatMoney(p.baseAmount, currencyCode) : '—'} |
|
))}
@@ -483,7 +484,7 @@ function RestrictionsPanel({ ratePlanId }: { ratePlanId: string }) {
// ---- Rate Plan Detail ----
function RatePlanDetail() {
const { t } = useTranslation();
- const { propertyId } = useProperty();
+ const { propertyId , currencyCode } = useProperty();
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [testDate, setTestDate] = useState('');
@@ -537,7 +538,7 @@ function RatePlanDetail() {
{t('ratePlans.details')}
{t('ratePlans.code')}
{plan.code}
-
{t('ratePlans.baseAmount')}
{plan.baseAmount != null ? `$${Number(plan.baseAmount).toFixed(2)}` : '—'}
+
{t('ratePlans.baseAmount')}
{plan.baseAmount != null ? formatMoney(plan.baseAmount, currencyCode) : '—'}
{t('ratePlans.roomType')}
{plan.roomTypeName ?? '—'}
{t('ratePlans.currency')}
{plan.currency ?? plan.currencyCode ?? 'USD'}
{plan.type === 'derived' && (
@@ -566,7 +567,7 @@ function RatePlanDetail() {
{effectiveRate != null && (
{t('ratePlans.effectiveRateFor', { date: testDate || '—' })}
-
${Number(effectiveRate).toFixed(2)}
+
{formatMoney(effectiveRate)}
)}
diff --git a/apps/dashboard/src/pages/Reports.tsx b/apps/dashboard/src/pages/Reports.tsx
index 42b23172..eb523722 100644
--- a/apps/dashboard/src/pages/Reports.tsx
+++ b/apps/dashboard/src/pages/Reports.tsx
@@ -1,4 +1,5 @@
import { useEffect, useMemo, useState } from 'react';
+import { formatMoney } from '../lib/money';
import { useSearchParams } from 'react-router-dom';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { BarChart3, Percent, DollarSign, TrendingUp, Building2, Star } from 'lucide-react';
@@ -26,7 +27,7 @@ const DEMO_FAVORITES_KEY = 'haip.reportFavorites';
export default function Reports() {
const { t } = useTranslation();
- const { propertyId, isPortfolioMode, properties } = useProperty();
+ const { propertyId, isPortfolioMode, properties, currencyCode } = useProperty();
const queryClient = useQueryClient();
const [searchParams] = useSearchParams();
// Deep links (e.g. Accounting → live trial balance) may preselect report/date.
@@ -255,8 +256,8 @@ export default function Reports() {
{report === 'financial-summary' && (
-
-
+
+
{isPortfolioMode && Array.isArray(reportData.byProperty) && (
@@ -277,10 +278,10 @@ export default function Reports() {
{(reportData.byProperty as Array<{ propertyId: string; totalRevenue: number; occupancyRate: number; adr: number; revpar: number }>).map((row) => (
| {propertyNameMap.get(row.propertyId) ?? row.propertyId} |
- ${Number(row.totalRevenue).toFixed(2)} |
+ {formatMoney(row.totalRevenue)} |
{formatOccupancyPercent(row.occupancyRate)} |
- ${Number(row.adr).toFixed(2)} |
- ${Number(row.revpar).toFixed(2)} |
+ {formatMoney(row.adr)} |
+ {formatMoney(row.revpar)} |
))}
@@ -295,7 +296,7 @@ export default function Reports() {
{Object.entries(reportData.revenueByType as Record
).map(([k, v]) => (
{k.replace(/_/g, ' ')}
- ${Number(v).toFixed(2)}
+ {formatMoney(v)}
))}
@@ -354,9 +355,9 @@ export default function Reports() {
{report === 'daily-revenue' && (
-
-
-
+
+
+
{payments && Object.keys(payments).length > 0 && (
@@ -415,11 +416,11 @@ export default function Reports() {
return (
| {t(`reports.${labelKey}`)} |
- ${Number(row.opening).toFixed(2)} |
- ${Number(row.netActivity).toFixed(2)} |
- ${Number(row.transfersIn).toFixed(2)} |
- ${Number(row.transfersOut).toFixed(2)} |
- ${Number(row.closing).toFixed(2)} |
+ {formatMoney(row.opening)} |
+ {formatMoney(row.netActivity)} |
+ {formatMoney(row.transfersIn)} |
+ {formatMoney(row.transfersOut)} |
+ {formatMoney(row.closing)} |
);
})}
@@ -430,7 +431,7 @@ export default function Reports() {
{t('reports.trialBalanceInterLedger')}
- ${Number(reportData.interLedgerTransfers).toFixed(2)}
+ {formatMoney(reportData.interLedgerTransfers)}
)}
diff --git a/apps/dashboard/src/pages/Reservations.tsx b/apps/dashboard/src/pages/Reservations.tsx
index a0582106..e8750a92 100644
--- a/apps/dashboard/src/pages/Reservations.tsx
+++ b/apps/dashboard/src/pages/Reservations.tsx
@@ -1,4 +1,5 @@
import { useState, useMemo } from 'react';
+import { formatMoney } from '../lib/money';
import { Routes, Route, useNavigate } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
@@ -104,7 +105,7 @@ function parseImportRows(text: string): Record
[] {
// ---- Reservation List ----
function ReservationList() {
const { t } = useTranslation();
- const { propertyId } = useProperty();
+ const { propertyId, currencyCode } = useProperty();
const navigate = useNavigate();
const queryClient = useQueryClient();
@@ -238,7 +239,7 @@ function ReservationList() {
adults: createAdults,
children: createChildren,
totalAmount,
- currencyCode: 'USD',
+ currencyCode,
source: 'direct',
});
},
@@ -382,7 +383,7 @@ function ReservationList() {
return ;
}, size: 120
},
- { accessorKey: 'totalAmount', header: t('reservations.total'), cell: ({ getValue }) => getValue() != null ? `$${Number(getValue()).toFixed(2)}` : '—', size: 100 },
+ { accessorKey: 'totalAmount', header: t('reservations.total'), cell: ({ getValue }) => getValue() != null ? formatMoney(getValue() as string | number, currencyCode) : '—', size: 100 },
{ accessorKey: 'source', header: t('reservations.source'), cell: ({ getValue }) => (getValue() as string) ?? 'direct', size: 90 },
{
id: 'actions',
@@ -628,7 +629,7 @@ function ReservationList() {
-
+
{detailRes.notes && (
@@ -742,7 +743,7 @@ function ReservationList() {
{(rt.ratePlans ?? []).map((rp) => (
))}