Parcourir la source

Merge pull request #331 from bnap00/feature/currency-symbol-prefix

Implement currency symbol handling and update related components
MartinNYHC il y a 3 mois
Parent
commit
4e75703018

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

@@ -46,7 +46,7 @@ const mockArchives = [
 ];
 
 const mockSettings = {
-  currency: '$',
+  currency: 'USD',
   check_updates: 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 24 entries', () => {
+    expect(SUPPORTED_CURRENCIES).toHaveLength(24);
+  });
+});

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

@@ -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({

+ 44 - 51
frontend/src/pages/SettingsPage.tsx

@@ -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,45 +1565,52 @@ 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>
-                  <option value="HKD">HKD ($)</option>
-                  <option value="INR">INR (₹)</option>
-                  <option value="KRW">KRW (₩)</option>
-                  <option value="SEK">SEK (kr)</option>
-                  <option value="NOK">NOK (kr)</option>
-                  <option value="DKK">DKK (kr)</option>
-                  <option value="PLN">PLN (zł)</option>
-                  <option value="BRL">BRL (R$)</option>
-                  <option value="TWD">TWD ($)</option>
-                  <option value="SGD">SGD ($)</option>
-                  <option value="NZD">NZD ($)</option>
-                  <option value="MXN">MXN ($)</option>
-                  <option value="CZK">CZK (Kč)</option>
-                  <option value="THB">THB (฿)</option>
-                  <option value="ZAR">ZAR (R)</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)
+                    }
+                    style={{ paddingLeft: `${Math.max(2, getCurrencySymbol(localSettings.currency).length * 0.6 + 1)}rem` }}
+                    className="w-full 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)
+                    }
+                    style={{ paddingLeft: `${Math.max(2, getCurrencySymbol(localSettings.currency).length * 0.6 + 1)}rem` }}
+                    className="w-full 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">
@@ -2547,7 +2540,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>
@@ -2564,7 +2557,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>
@@ -2581,7 +2574,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>

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

@@ -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) || [];
 

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

@@ -0,0 +1,57 @@
+const CURRENCY_SYMBOLS: Record<string, string> = {
+  USD: '$',
+  EUR: '€',
+  GBP: '£',
+  CHF: 'Fr.',
+  JPY: '¥',
+  CNY: '¥',
+  CAD: '$',
+  AUD: '$',
+  INR: '₹',
+  HKD: 'HK$',
+  KRW: '₩',
+  SEK: 'kr',
+  NOK: 'kr',
+  DKK: 'kr',
+  PLN: 'zł',
+  BRL: 'R$',
+  TWD: 'NT$',
+  SGD: 'S$',
+  NZD: 'NZ$',
+  MXN: 'MX$',
+  CZK: 'Kč',
+  THB: '฿',
+  ZAR: 'R',
+  TRY: '₺',
+};
+
+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$)' },
+  { code: 'KRW', label: 'KRW (₩)' },
+  { code: 'SEK', label: 'SEK (kr)' },
+  { code: 'NOK', label: 'NOK (kr)' },
+  { code: 'DKK', label: 'DKK (kr)' },
+  { code: 'PLN', label: 'PLN (zł)' },
+  { code: 'BRL', label: 'BRL (R$)' },
+  { code: 'TWD', label: 'TWD (NT$)' },
+  { code: 'SGD', label: 'SGD (S$)' },
+  { code: 'NZD', label: 'NZD (NZ$)' },
+  { code: 'MXN', label: 'MXN (MX$)' },
+  { code: 'CZK', label: 'CZK (Kč)' },
+  { code: 'THB', label: 'THB (฿)' },
+  { code: 'ZAR', label: 'ZAR (R)' },
+  { code: 'TRY', label: 'TRY (₺)' },
+] as const;