Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 112 additions & 112 deletions dashboard/package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,6 @@
"eslint-plugin-react-refresh": "^0.4.20",
"eslint-plugin-simple-import-sort": "^12.1.1",
"globals": "^16.4.0",
"vite": "^7.1.7"
"vite": "^7.3.0"
}
}
214 changes: 180 additions & 34 deletions dashboard/src/components/MenuPage/MultiCurrencyDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { DevTool } from "@hookform/devtools";
import { useCurrencyExchange, useMultiCurrencyPayment } from "@/hooks";
import { toast, Toaster } from "sonner";
import { useCartStore } from "@/stores/useCartStore";
import { db, call } from "@/lib/frappeClient";

export default function MultiCurrencyDialog({
open,
Expand All @@ -31,12 +32,14 @@ export default function MultiCurrencyDialog({
setPaymentDialogOpenState,
}) {
const BASE_TOTAL = total || 0;
const { exchangeRates } = useCurrencyExchange();
const { exchangeRates: defaultExchangeRates } = useCurrencyExchange();

const [ratesAtOpen, setRatesAtOpen] = useState(null);
const [paymentMethods, setPaymentMethods] = useState([]); // Store payment methods with mode info
const [activeCurrency, setActiveCurrency] = useState(null);
const { submitPayment, loading, error, success } = useMultiCurrencyPayment();
const clearCart = useCartStore((state) => state.clearCart);
const [loadingRates, setLoadingRates] = useState(true);

const { register, watch, setValue, reset, setFocus, control, handleSubmit } =
useForm({
Expand All @@ -50,48 +53,172 @@ export default function MultiCurrencyDialog({
if (success) toast.success("Payment successful");
}, [error, success]);

// Fetch exchange rates from selected payment methods
useEffect(() => {
if (open && exchangeRates) {
setRatesAtOpen(exchangeRates);
const fetchPaymentMethodRates = async () => {
if (!open) return;

setLoadingRates(true);
try {
// Get default currency from Global Defaults
const { message: defaultCurrency } = await db.getSingleValue("Global Defaults", "default_currency");
const systemCurrency = defaultCurrency || "USD";

reset({
payments: Object.keys(exchangeRates).reduce((acc, currency) => {
acc[currency] = "";
return acc;
}, {}),
});
}
}, [open, exchangeRates, reset]);
// Get HA POS Settings document (Single doctype)
const settingsResponse = await call.get("havano_restaurant_pos.api.get_ha_pos_settings");
const doc = settingsResponse?.message?.data;

// Get all selected payment methods (show all regardless of currency)
const selectedMethods = doc?.selected_payment_methods || [];

// Build payment methods list with mode, currency, and exchange rate
const methodsList = [];
const rates = {};
const addedKeys = new Set(); // Track added keys to avoid duplicates

// Always add Cash with default currency first
const cashKey = `Cash_${systemCurrency}`;
methodsList.push({
key: cashKey,
mode: "Cash",
currency: systemCurrency,
exchangeRate: 1.0,
currencySymbol: systemCurrency
});
rates[cashKey] = 1.0;
addedKeys.add(cashKey);

// Process each selected payment method
selectedMethods.forEach((method) => {
if (method.mode_of_payment && method.currency && method.exchange_rate) {
const currency = method.currency;
const exchangeRate = parseFloat(method.exchange_rate) || 1.0;

// Use mode_of_payment + currency as unique key
const key = `${method.mode_of_payment}_${currency}`;

// Only add if not already added (avoid duplicates)
if (!addedKeys.has(key)) {
methodsList.push({
key: key,
mode: method.mode_of_payment,
currency: currency,
exchangeRate: exchangeRate,
currencySymbol: method.currency_symbol || currency
});

rates[key] = exchangeRate;
addedKeys.add(key);
}
}
});

// If no other payment methods found, add default exchange rates
if (methodsList.length === 1 && defaultExchangeRates && Object.keys(defaultExchangeRates).length > 0) {
Object.keys(defaultExchangeRates).forEach(currency => {
// Skip if it's the default currency (already added as Cash)
if (currency === systemCurrency) return;

const key = `Cash_${currency}`;
if (!addedKeys.has(key)) {
methodsList.push({
key: key,
mode: "Cash",
currency: currency,
exchangeRate: defaultExchangeRates[currency],
currencySymbol: currency
});
rates[key] = defaultExchangeRates[currency];
addedKeys.add(key);
}
});
}

setPaymentMethods(methodsList);
setRatesAtOpen(rates);

reset({
payments: methodsList.reduce((acc, method) => {
acc[method.key] = "";
return acc;
}, {}),
});

// Set first payment method as active
const firstMethod = methodsList[0];
if (firstMethod) {
setActiveCurrency(firstMethod.key);
}
} catch (error) {
console.error("Error fetching payment method rates:", error);
// Fall back to default exchange rates
if (defaultExchangeRates && Object.keys(defaultExchangeRates).length > 0) {
const fallbackMethods = Object.keys(defaultExchangeRates).map(currency => ({
key: `Cash_${currency}`,
mode: "Cash",
currency: currency,
exchangeRate: defaultExchangeRates[currency],
currencySymbol: currency
}));
const fallbackRates = {};
fallbackMethods.forEach(method => {
fallbackRates[method.key] = method.exchangeRate;
});

setPaymentMethods(fallbackMethods);
setRatesAtOpen(fallbackRates);
reset({
payments: fallbackMethods.reduce((acc, method) => {
acc[method.key] = "";
return acc;
}, {}),
});

if (fallbackMethods[0]) {
setActiveCurrency(fallbackMethods[0].key);
}
}
} finally {
setLoadingRates(false);
}
};

fetchPaymentMethodRates();
}, [open, defaultExchangeRates, reset]);

const payments =
useWatch({
control,
name: "payments",
}) || {};

const currencies = ratesAtOpen ? Object.keys(ratesAtOpen) : [];

const getBaseValue = (paid, currency) => {
if (!ratesAtOpen) return 0;
const getBaseValue = (paid, key) => {
if (!ratesAtOpen || !ratesAtOpen[key]) return 0;

const amount = Number(paid);
if (isNaN(amount) || amount === 0) return 0;

return amount / ratesAtOpen[currency];
// Exchange rate is FROM payment currency TO company currency
// So to convert payment amount to base currency: multiply by rate
// Example: 100 EUR * 1.1 (EUR to USD rate) = 110 USD
return amount * ratesAtOpen[key];
};

const totalPaidInBase = currencies.reduce((sum, currency) => {
const paid = Number(payments[currency] || 0);
return sum + getBaseValue(paid, currency);
const totalPaidInBase = paymentMethods.reduce((sum, method) => {
const paid = Number(payments[method.key] || 0);
return sum + getBaseValue(paid, method.key);
}, 0);

const remainingBase = Math.max(BASE_TOTAL - totalPaidInBase, 0);
const changeBase = Math.max(totalPaidInBase - BASE_TOTAL, 0);
const isPaid = totalPaidInBase >= BASE_TOTAL;

const getAmountDue = (currency) => {
if (!ratesAtOpen) return 0;
return remainingBase * ratesAtOpen[currency];
const getAmountDue = (key) => {
if (!ratesAtOpen || !ratesAtOpen[key]) return 0;
// Exchange rate is FROM payment currency TO company currency
// So to convert base currency to payment currency: divide by rate
// Example: 100 USD / 1.1 (EUR to USD rate) = 90.91 EUR
return remainingBase / ratesAtOpen[key];
};
return (
<>
Expand Down Expand Up @@ -207,6 +334,7 @@ export default function MultiCurrencyDialog({
<Table>
<TableHeader>
<TableRow>
<TableHead>MODE</TableHead>
<TableHead>CURRENCY</TableHead>
<TableHead>RATE</TableHead>
<TableHead>AMOUNT DUE</TableHead>
Expand All @@ -215,28 +343,30 @@ export default function MultiCurrencyDialog({
</TableRow>
</TableHeader>
<TableBody>
{currencies.map((currency) => {
const paid = Number(payments[currency] || 0);
const rate = ratesAtOpen[currency];
{paymentMethods.map((method) => {
const paid = Number(payments[method.key] || 0);
const rate = ratesAtOpen[method.key];

return (
<TableRow key={currency}>
<TableCell>{currency}</TableCell>
<TableRow key={method.key}>
<TableCell className="font-medium">{method.mode}</TableCell>
<TableCell>{method.currency}</TableCell>
<TableCell>{rate.toFixed(4)}</TableCell>

<TableCell>
{getAmountDue(currency).toFixed(4)}
{getAmountDue(method.key).toFixed(4)}
</TableCell>

<TableCell>
<Input
{...register(`payments.${currency}`)}
onFocus={() => setActiveCurrency(currency)}
{...register(`payments.${method.key}`)}
onFocus={() => setActiveCurrency(method.key)}
disabled={loadingRates}
/>
</TableCell>

<TableCell>
{getBaseValue(paid, currency).toFixed(4)}
{getBaseValue(paid, method.key).toFixed(4)}
</TableCell>
</TableRow>
);
Expand All @@ -251,7 +381,7 @@ export default function MultiCurrencyDialog({
<div className="payment-keyboard-container flex-shrink-0">
<div className="payment-keyboard-box">
<Keyboard
value={payments[activeCurrency]}
value={payments[activeCurrency] || ""}
setValue={(val) => {
setValue(`payments.${activeCurrency}`, val, {
shouldDirty: true,
Expand All @@ -272,11 +402,27 @@ export default function MultiCurrencyDialog({
<Button
className="flex-1"
type="submit"
disabled={loading || !isPaid}
disabled={loading || !isPaid || loadingRates}
onClick={handleSubmit(async (data) => {
try {
// Convert payment keys to include mode and currency info
const paymentData = {};
Object.entries(data.payments || {}).forEach(([key, amount]) => {
if (Number(amount) > 0) {
const method = paymentMethods.find(m => m.key === key);
if (method) {
// Send as mode_currency: amount format
paymentData[key] = {
mode: method.mode,
currency: method.currency,
amount: Number(amount)
};
}
}
});

await submitPayment({
payments: data.payments,
payments: paymentData,
});

onOpenChange(false);
Expand Down
Loading
Loading