Browse Source

Implement currency symbol handling and update related components

Bharat Parsiya 3 months ago
parent
commit
ced51e06fb

+ 1 - 1
frontend/src/__tests__/pages/StatsPage.test.tsx

@@ -46,7 +46,7 @@ const mockArchives = [
 ];
 ];
 
 
 const mockSettings = {
 const mockSettings = {
-  currency: '$',
+  currency: 'USD',
   check_updates: false,
   check_updates: false,
   check_printer_firmware: false,
   check_printer_firmware: false,
 };
 };

+ 43 - 0
frontend/src/__tests__/utils/currency.test.ts

@@ -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);
+  });
+});

+ 2 - 1
frontend/src/pages/ProjectDetailPage.tsx

@@ -43,6 +43,7 @@ import { ConfirmModal } from '../components/ConfirmModal';
 
 
 // Project edit modal (reused from ProjectsPage)
 // Project edit modal (reused from ProjectsPage)
 import { ProjectModal } from './ProjectsPage';
 import { ProjectModal } from './ProjectsPage';
+import { getCurrencySymbol } from '../utils/currency';
 
 
 function formatDuration(hours: number): string {
 function formatDuration(hours: number): string {
   if (hours < 1) {
   if (hours < 1) {
@@ -251,7 +252,7 @@ export function ProjectDetailPage() {
     enabled: projectId > 0,
     enabled: projectId > 0,
   });
   });
 
 
-  const currency = settings?.currency || '$';
+  const currency = getCurrencySymbol(settings?.currency || 'USD');
   const timeFormat: TimeFormat = settings?.time_format || 'system';
   const timeFormat: TimeFormat = settings?.time_format || 'system';
 
 
   const updateMutation = useMutation({
   const updateMutation = useMutation({

+ 42 - 36
frontend/src/pages/SettingsPage.tsx

@@ -5,6 +5,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
 import { api } from '../api/client';
 import { api } from '../api/client';
 import { useAuth } from '../contexts/AuthContext';
 import { useAuth } from '../contexts/AuthContext';
 import { formatDateOnly } from '../utils/date';
 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 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 { Card, CardContent, CardHeader } from '../components/Card';
 import { Button } from '../components/Button';
 import { Button } from '../components/Button';
@@ -1557,21 +1558,6 @@ export function SettingsPage() {
               <h2 className="text-lg font-semibold text-white">{t('settings.costTracking')}</h2>
               <h2 className="text-lg font-semibold text-white">{t('settings.costTracking')}</h2>
             </CardHeader>
             </CardHeader>
             <CardContent className="space-y-4">
             <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>
               <div>
                 <label className="block text-sm text-bambu-gray mb-1">Currency</label>
                 <label className="block text-sm text-bambu-gray mb-1">Currency</label>
                 <select
                 <select
@@ -1579,30 +1565,50 @@ export function SettingsPage() {
                   onChange={(e) => updateSetting('currency', e.target.value)}
                   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"
                   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>
                 </select>
               </div>
               </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>
               <div>
                 <label className="block text-sm text-bambu-gray mb-1">
                 <label className="block text-sm text-bambu-gray mb-1">
                   Electricity cost per kWh
                   Electricity cost per kWh
                 </label>
                 </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>
               <div>
               <div>
                 <label className="block text-sm text-bambu-gray mb-1">
                 <label className="block text-sm text-bambu-gray mb-1">
@@ -2532,7 +2538,7 @@ export function SettingsPage() {
                       </div>
                       </div>
                       {(localSettings?.energy_cost_per_kwh ?? 0) > 0 && (
                       {(localSettings?.energy_cost_per_kwh ?? 0) > 0 && (
                         <div className="text-xs text-bambu-gray mt-1">
                         <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>
                       )}
                       )}
                     </div>
                     </div>
@@ -2549,7 +2555,7 @@ export function SettingsPage() {
                       </div>
                       </div>
                       {(localSettings?.energy_cost_per_kwh ?? 0) > 0 && (
                       {(localSettings?.energy_cost_per_kwh ?? 0) > 0 && (
                         <div className="text-xs text-bambu-gray mt-1">
                         <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>
                       )}
                       )}
                     </div>
                     </div>
@@ -2566,7 +2572,7 @@ export function SettingsPage() {
                       </div>
                       </div>
                       {(localSettings?.energy_cost_per_kwh ?? 0) > 0 && (
                       {(localSettings?.energy_cost_per_kwh ?? 0) > 0 && (
                         <div className="text-xs text-bambu-gray mt-1">
                         <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>
                       )}
                       )}
                     </div>
                     </div>

+ 2 - 1
frontend/src/pages/StatsPage.tsx

@@ -26,6 +26,7 @@ import { api } from '../api/client';
 import { PrintCalendar } from '../components/PrintCalendar';
 import { PrintCalendar } from '../components/PrintCalendar';
 import { FilamentTrends } from '../components/FilamentTrends';
 import { FilamentTrends } from '../components/FilamentTrends';
 import { Dashboard, type DashboardWidget } from '../components/Dashboard';
 import { Dashboard, type DashboardWidget } from '../components/Dashboard';
+import { getCurrencySymbol } from '../utils/currency';
 
 
 // Widget Components
 // Widget Components
 function QuickStatsWidget({
 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 printerMap = new Map(printers?.map((p) => [String(p.id), p.name]) || []);
   const printDates = archives?.map((a) => a.created_at) || [];
   const printDates = archives?.map((a) => a.created_at) || [];
 
 

+ 29 - 0
frontend/src/utils/currency.ts

@@ -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;