mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-06 14:11:35 +02:00
Implement currency symbol handling and update related components
This commit is contained in:
@@ -46,7 +46,7 @@ const mockArchives = [
|
||||
];
|
||||
|
||||
const mockSettings = {
|
||||
currency: '$',
|
||||
currency: 'USD',
|
||||
check_updates: false,
|
||||
check_printer_firmware: false,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { getCurrencySymbol, SUPPORTED_CURRENCIES } from '../../utils/currency';
|
||||
|
||||
describe('getCurrencySymbol', () => {
|
||||
it('returns $ for USD', () => {
|
||||
expect(getCurrencySymbol('USD')).toBe('$');
|
||||
});
|
||||
|
||||
it('returns € for EUR', () => {
|
||||
expect(getCurrencySymbol('EUR')).toBe('€');
|
||||
});
|
||||
|
||||
it('returns £ for GBP', () => {
|
||||
expect(getCurrencySymbol('GBP')).toBe('£');
|
||||
});
|
||||
|
||||
it('returns ₹ for INR', () => {
|
||||
expect(getCurrencySymbol('INR')).toBe('₹');
|
||||
});
|
||||
|
||||
it('returns HK$ for HKD', () => {
|
||||
expect(getCurrencySymbol('HKD')).toBe('HK$');
|
||||
});
|
||||
|
||||
it('returns the code itself for unknown currencies', () => {
|
||||
expect(getCurrencySymbol('XYZ')).toBe('XYZ');
|
||||
});
|
||||
|
||||
it('is case-insensitive', () => {
|
||||
expect(getCurrencySymbol('usd')).toBe('$');
|
||||
expect(getCurrencySymbol('eur')).toBe('€');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SUPPORTED_CURRENCIES', () => {
|
||||
it('contains INR', () => {
|
||||
expect(SUPPORTED_CURRENCIES.find((c) => c.code === 'INR')).toBeDefined();
|
||||
});
|
||||
|
||||
it('has 10 entries', () => {
|
||||
expect(SUPPORTED_CURRENCIES).toHaveLength(10);
|
||||
});
|
||||
});
|
||||
@@ -43,6 +43,7 @@ import { ConfirmModal } from '../components/ConfirmModal';
|
||||
|
||||
// Project edit modal (reused from ProjectsPage)
|
||||
import { ProjectModal } from './ProjectsPage';
|
||||
import { getCurrencySymbol } from '../utils/currency';
|
||||
|
||||
function formatDuration(hours: number): string {
|
||||
if (hours < 1) {
|
||||
@@ -251,7 +252,7 @@ export function ProjectDetailPage() {
|
||||
enabled: projectId > 0,
|
||||
});
|
||||
|
||||
const currency = settings?.currency || '$';
|
||||
const currency = getCurrencySymbol(settings?.currency || 'USD');
|
||||
const timeFormat: TimeFormat = settings?.time_format || 'system';
|
||||
|
||||
const updateMutation = useMutation({
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { api } from '../api/client';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { formatDateOnly } from '../utils/date';
|
||||
import { getCurrencySymbol, SUPPORTED_CURRENCIES } from '../utils/currency';
|
||||
import type { AppSettings, AppSettingsUpdate, SmartPlug, SmartPlugStatus, NotificationProvider, NotificationTemplate, UpdateStatus, GitHubBackupStatus, CloudAuthStatus, UserCreate, UserUpdate, UserResponse, Group, GroupCreate, GroupUpdate, Permission, PermissionCategory } from '../api/client';
|
||||
import { Card, CardContent, CardHeader } from '../components/Card';
|
||||
import { Button } from '../components/Button';
|
||||
@@ -1557,21 +1558,6 @@ export function SettingsPage() {
|
||||
<h2 className="text-lg font-semibold text-white">{t('settings.costTracking')}</h2>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1">
|
||||
Default filament cost (per kg)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
value={localSettings.default_filament_cost}
|
||||
onChange={(e) =>
|
||||
updateSetting('default_filament_cost', parseFloat(e.target.value) || 0)
|
||||
}
|
||||
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1">Currency</label>
|
||||
<select
|
||||
@@ -1579,30 +1565,50 @@ export function SettingsPage() {
|
||||
onChange={(e) => updateSetting('currency', e.target.value)}
|
||||
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
|
||||
>
|
||||
<option value="USD">USD ($)</option>
|
||||
<option value="EUR">EUR (€)</option>
|
||||
<option value="GBP">GBP (£)</option>
|
||||
<option value="CHF">CHF (Fr.)</option>
|
||||
<option value="JPY">JPY (¥)</option>
|
||||
<option value="CNY">CNY (¥)</option>
|
||||
<option value="CAD">CAD ($)</option>
|
||||
<option value="AUD">AUD ($)</option>
|
||||
{SUPPORTED_CURRENCIES.map((c) => (
|
||||
<option key={c.code} value={c.code}>{c.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1">
|
||||
Default filament cost (per kg)
|
||||
</label>
|
||||
<div className="relative">
|
||||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-bambu-gray text-sm pointer-events-none">
|
||||
{getCurrencySymbol(localSettings.currency)}
|
||||
</span>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
value={localSettings.default_filament_cost}
|
||||
onChange={(e) =>
|
||||
updateSetting('default_filament_cost', parseFloat(e.target.value) || 0)
|
||||
}
|
||||
className="w-full pl-8 pr-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1">
|
||||
Electricity cost per kWh
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
value={localSettings.energy_cost_per_kwh}
|
||||
onChange={(e) =>
|
||||
updateSetting('energy_cost_per_kwh', parseFloat(e.target.value) || 0)
|
||||
}
|
||||
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
|
||||
/>
|
||||
<div className="relative">
|
||||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-bambu-gray text-sm pointer-events-none">
|
||||
{getCurrencySymbol(localSettings.currency)}
|
||||
</span>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
value={localSettings.energy_cost_per_kwh}
|
||||
onChange={(e) =>
|
||||
updateSetting('energy_cost_per_kwh', parseFloat(e.target.value) || 0)
|
||||
}
|
||||
className="w-full pl-8 pr-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1">
|
||||
@@ -2532,7 +2538,7 @@ export function SettingsPage() {
|
||||
</div>
|
||||
{(localSettings?.energy_cost_per_kwh ?? 0) > 0 && (
|
||||
<div className="text-xs text-bambu-gray mt-1">
|
||||
~{(plugEnergySummary.totalToday * (localSettings?.energy_cost_per_kwh ?? 0)).toFixed(2)} {localSettings?.currency}
|
||||
~{(plugEnergySummary.totalToday * (localSettings?.energy_cost_per_kwh ?? 0)).toFixed(2)} {getCurrencySymbol(localSettings?.currency || 'USD')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -2549,7 +2555,7 @@ export function SettingsPage() {
|
||||
</div>
|
||||
{(localSettings?.energy_cost_per_kwh ?? 0) > 0 && (
|
||||
<div className="text-xs text-bambu-gray mt-1">
|
||||
~{(plugEnergySummary.totalYesterday * (localSettings?.energy_cost_per_kwh ?? 0)).toFixed(2)} {localSettings?.currency}
|
||||
~{(plugEnergySummary.totalYesterday * (localSettings?.energy_cost_per_kwh ?? 0)).toFixed(2)} {getCurrencySymbol(localSettings?.currency || 'USD')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -2566,7 +2572,7 @@ export function SettingsPage() {
|
||||
</div>
|
||||
{(localSettings?.energy_cost_per_kwh ?? 0) > 0 && (
|
||||
<div className="text-xs text-bambu-gray mt-1">
|
||||
~{(plugEnergySummary.totalLifetime * (localSettings?.energy_cost_per_kwh ?? 0)).toFixed(2)} {localSettings?.currency}
|
||||
~{(plugEnergySummary.totalLifetime * (localSettings?.energy_cost_per_kwh ?? 0)).toFixed(2)} {getCurrencySymbol(localSettings?.currency || 'USD')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -26,6 +26,7 @@ import { api } from '../api/client';
|
||||
import { PrintCalendar } from '../components/PrintCalendar';
|
||||
import { FilamentTrends } from '../components/FilamentTrends';
|
||||
import { Dashboard, type DashboardWidget } from '../components/Dashboard';
|
||||
import { getCurrencySymbol } from '../utils/currency';
|
||||
|
||||
// Widget Components
|
||||
function QuickStatsWidget({
|
||||
@@ -603,7 +604,7 @@ export function StatsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const currency = settings?.currency || '$';
|
||||
const currency = getCurrencySymbol(settings?.currency || 'USD');
|
||||
const printerMap = new Map(printers?.map((p) => [String(p.id), p.name]) || []);
|
||||
const printDates = archives?.map((a) => a.created_at) || [];
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
const CURRENCY_SYMBOLS: Record<string, string> = {
|
||||
USD: '$',
|
||||
EUR: '€',
|
||||
GBP: '£',
|
||||
CHF: 'Fr.',
|
||||
JPY: '¥',
|
||||
CNY: '¥',
|
||||
CAD: '$',
|
||||
AUD: '$',
|
||||
INR: '₹',
|
||||
HKD: 'HK$',
|
||||
};
|
||||
|
||||
export function getCurrencySymbol(currencyCode: string): string {
|
||||
return CURRENCY_SYMBOLS[currencyCode.toUpperCase()] || currencyCode;
|
||||
}
|
||||
|
||||
export const SUPPORTED_CURRENCIES = [
|
||||
{ code: 'USD', label: 'USD ($)' },
|
||||
{ code: 'EUR', label: 'EUR (€)' },
|
||||
{ code: 'GBP', label: 'GBP (£)' },
|
||||
{ code: 'CHF', label: 'CHF (Fr.)' },
|
||||
{ code: 'JPY', label: 'JPY (¥)' },
|
||||
{ code: 'CNY', label: 'CNY (¥)' },
|
||||
{ code: 'CAD', label: 'CAD ($)' },
|
||||
{ code: 'AUD', label: 'AUD ($)' },
|
||||
{ code: 'INR', label: 'INR (₹)' },
|
||||
{ code: 'HKD', label: 'HKD (HK$)' },
|
||||
] as const;
|
||||
Reference in New Issue
Block a user