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() {
    - setDepositActionOpen(false)} title={t('accounting.depositActions', { amount: Number(selectedDeposit?.amount ?? 0).toFixed(2) })}> + setDepositActionOpen(false)} title={t('accounting.depositActions', { amount: formatMoney(selectedDeposit?.amount ?? 0) })}>
    setApplyFolioId(e.target.value)} placeholder={t('accounting.folioIdPlaceholder')} className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm font-mono text-xs" /> @@ -596,7 +597,7 @@ function AccountingHome() { {reversible.map((tx) => ( ))} diff --git a/apps/dashboard/src/pages/Cashier.tsx b/apps/dashboard/src/pages/Cashier.tsx index 2ee7372f..9961c34a 100644 --- a/apps/dashboard/src/pages/Cashier.tsx +++ b/apps/dashboard/src/pages/Cashier.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } 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 { Banknote, Plus, ChevronLeft, FileText } from 'lucide-react'; @@ -85,7 +86,7 @@ function CashierHome() { {drawers.map((d, i) => ( {d.name} - ${Number(d.startingFloat ?? 0).toFixed(2)} + {formatMoney(d.startingFloat ?? 0)} @@ -222,7 +223,7 @@ function CashierSession() {
    {drawer?.startingFloat != null && (

    - {t('cashier.startingFloat')}: ${Number(drawer.startingFloat).toFixed(2)} + {t('cashier.startingFloat')}: {formatMoney(drawer.startingFloat)}

    )}
    @@ -291,10 +292,10 @@ function SessionReport() { ) : (
    -

    {t('cashier.openingFloat')}

    ${session?.openingFloat ?? '0.00'}

    -

    {t('cashier.expected')}

    ${report.expectedBalance ?? '0.00'}

    -

    {t('cashier.counted')}

    ${session?.countedBalance ?? '—'}

    -

    {t('cashier.variance')}

    ${session?.variance ?? '0.00'}

    +

    {t('cashier.openingFloat')}

    {formatMoney(session?.openingFloat ?? 0)}

    +

    {t('cashier.expected')}

    {formatMoney(report.expectedBalance ?? 0)}

    +

    {t('cashier.counted')}

    {formatMoney(session?.countedBalance)}

    +

    {t('cashier.variance')}

    {formatMoney(session?.variance ?? 0)}

    @@ -314,7 +315,7 @@ function SessionReport() { {type.replace('_', ' ')} {row.count} - ${Number(row.total).toFixed(2)} + {formatMoney(row.total)} ))} diff --git a/apps/dashboard/src/pages/Channels.tsx b/apps/dashboard/src/pages/Channels.tsx index 5360f242..66807b94 100644 --- a/apps/dashboard/src/pages/Channels.tsx +++ b/apps/dashboard/src/pages/Channels.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useMemo } 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 { Radio, Plus, ChevronLeft, RefreshCw, Zap, Image as ImageIcon, Trash2 } from 'lucide-react'; @@ -95,10 +96,6 @@ function contentPushErrorMessages(results: unknown[]): string[] { return messages; } -function formatMoney(amount: number): string { - return `$${Number(amount).toFixed(2)}`; -} - // ---- Connection List ---- function ConnectionList() { const { t } = useTranslation(); @@ -540,7 +537,7 @@ function ConnectionDetail() { // ---- Rate Parity ---- function RateParity() { const { t } = useTranslation(); - const { propertyId } = useProperty(); + const { propertyId, currencyCode } = useProperty(); const navigate = useNavigate(); const queryClient = useQueryClient(); const { toast } = useToast(); @@ -663,7 +660,7 @@ function RateParity() { {parity.map((row) => ( {row.ratePlanName} - {formatMoney(row.baseAmount)} + {formatMoney(row.baseAmount, currencyCode)} {channelColumns.map((col) => { const ch = row.channels.find((c) => c.channelConnectionId === col.channelConnectionId); if (!ch) { @@ -671,7 +668,7 @@ function RateParity() { } return ( -
    {formatMoney(ch.effectiveRate)}
    +
    {formatMoney(ch.effectiveRate, currencyCode)}
    {!ch.isParity && (
    - {t('channels.variance')}: {formatMoney(ch.variance)} + {t('channels.variance')}: {formatMoney(ch.variance, currencyCode)}
    )} diff --git a/apps/dashboard/src/pages/Commercial.tsx b/apps/dashboard/src/pages/Commercial.tsx index fe87b95e..3832f1a8 100644 --- a/apps/dashboard/src/pages/Commercial.tsx +++ b/apps/dashboard/src/pages/Commercial.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 { Briefcase, Plus, ChevronLeft } from 'lucide-react'; @@ -25,7 +26,7 @@ interface CommercialProfile { function CommercialList() { const { t } = useTranslation(); - const { propertyId } = useProperty(); + const { propertyId, currencyCode } = useProperty(); const navigate = useNavigate(); const queryClient = useQueryClient(); const [createOpen, setCreateOpen] = useState(false); @@ -197,7 +198,7 @@ function CommercialList() { function CommercialDetail() { const { t } = useTranslation(); const { id } = useParams<{ id: string }>(); - const { propertyId } = useProperty(); + const { propertyId , currencyCode } = useProperty(); const navigate = useNavigate(); const queryClient = useQueryClient(); const [arOpen, setArOpen] = useState(false); @@ -223,7 +224,7 @@ function CommercialDetail() { propertyId, name: profile!.name, paymentTermsDays: profile?.paymentTermsDays, - currencyCode: 'USD', + currencyCode, groupProfileId: id, description: `Direct bill — ${profile!.name}`, }); @@ -281,7 +282,7 @@ function CommercialDetail() { {arLedgers.map((l) => (
  • {l.name} - ${Number(l.balance ?? 0).toFixed(2)} + {formatMoney(l.balance ?? 0)}
  • ))} {arLedgers.length === 0 && ( diff --git a/apps/dashboard/src/pages/Dashboard.tsx b/apps/dashboard/src/pages/Dashboard.tsx index 82fc8225..96031c14 100644 --- a/apps/dashboard/src/pages/Dashboard.tsx +++ b/apps/dashboard/src/pages/Dashboard.tsx @@ -1,4 +1,5 @@ import { useEffect, useState, useCallback } from 'react'; +import { formatMoney } from '../lib/money'; import { useQuery } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; import { @@ -44,7 +45,7 @@ interface ActivityEvent { export default function Dashboard() { const { t, i18n } = useTranslation(); - const { propertyId, setPropertyId, isPortfolioMode, properties } = useProperty(); + const { propertyId, setPropertyId, isPortfolioMode, properties, currencyCode } = useProperty(); const navigate = useNavigate(); const now = new Date(); const dateLocale = getDateLocale(i18n.resolvedLanguage); @@ -163,19 +164,19 @@ export default function Dashboard() { /> @@ -189,7 +190,7 @@ export default function Dashboard() { - [`$${v.toFixed(2)}`, t('dashboard.portfolio.revenue')]} /> + [formatMoney(v, currencyCode), t('dashboard.portfolio.revenue')]} /> @@ -221,9 +222,9 @@ export default function Dashboard() { {formatOccupancyPercent(row.occupancyRate)} - ${Number(row.adr).toFixed(2)} - ${Number(row.revpar).toFixed(2)} - ${Number(row.totalRevenue).toFixed(2)} + {formatMoney(row.adr)} + {formatMoney(row.revpar)} + {formatMoney(row.totalRevenue)} ))} @@ -276,7 +277,7 @@ export default function Dashboard() { /> {f.guestName ?? '—'} - ${Number(f.balance ?? 0).toFixed(2)} + {formatMoney(f.balance ?? 0)} ))} {folios.length === 0 && ( @@ -398,7 +399,7 @@ function SplitFolioPanel({ {movableCharges.map((c) => ( ))} @@ -648,7 +649,7 @@ function FolioDetail() {

    {t('folios.balance')}

    -

    ${Number(folio.balance ?? 0).toFixed(2)}

    +

    {formatMoney(folio.balance ?? 0)}

    @@ -679,7 +680,7 @@ function FolioDetail() { {c.serviceDate} {c.description} {c.isLocked && } {t(`folios.chargeTypes.${c.type}`, { defaultValue: c.type })} - ${Number(c.amount).toFixed(2)} + {formatMoney(c.amount)} {!c.isReversal && !reversedIds.has(c.id) && !c.isLocked && folio.status === 'open' && (