diff --git a/client/src/components/dashboard/Dashboard.tsx b/client/src/components/dashboard/Dashboard.tsx index c023c8a1..c97f1d31 100644 --- a/client/src/components/dashboard/Dashboard.tsx +++ b/client/src/components/dashboard/Dashboard.tsx @@ -29,6 +29,7 @@ import { useAuth } from '../../contexts/AuthContext'; export const Dashboard: React.FC = () => { const navigate = useNavigate(); const { user } = useAuth(); + const userCurrency = (user as any)?.settings?.currency || (user as any)?.preferences?.currency || 'USD'; // Use Local-First hook instead of API const { assets, isLoading } = useAssetRepository(); @@ -44,10 +45,10 @@ export const Dashboard: React.FC = () => { const { totalAssetValue, zakatableAssets } = dashboardMetrics; - const formatCurrency = (amount: number, currency: string = 'USD'): string => { + const formatCurrency = (amount: number, currency?: string): string => { return new Intl.NumberFormat('en-US', { style: 'currency', - currency: currency, + currency: currency || userCurrency, }).format(amount); }; diff --git a/client/src/components/dashboard/PaymentDistributionChart.tsx b/client/src/components/dashboard/PaymentDistributionChart.tsx index 30141a23..f6d510dd 100644 --- a/client/src/components/dashboard/PaymentDistributionChart.tsx +++ b/client/src/components/dashboard/PaymentDistributionChart.tsx @@ -29,6 +29,7 @@ import { usePrivacy } from '../../contexts/PrivacyContext'; interface PaymentDistributionChartProps { payments: any[]; + currency?: string; } const COLORS = [ @@ -54,7 +55,7 @@ const RECIPIENT_LABELS: { [key: string]: string } = { 'other': 'Other' }; -export const PaymentDistributionChart: React.FC = ({ payments }) => { +export const PaymentDistributionChart: React.FC = ({ payments, currency = 'USD' }) => { const { privacyMode } = usePrivacy(); // Group payments by category @@ -84,7 +85,7 @@ export const PaymentDistributionChart: React.FC = if (privacyMode) return '****'; return new Intl.NumberFormat('en-US', { style: 'currency', - currency: 'USD', + currency: currency, maximumFractionDigits: 0 }).format(value); }; diff --git a/client/src/components/dashboard/WealthTrendChart.tsx b/client/src/components/dashboard/WealthTrendChart.tsx index 101efe55..043f1b35 100644 --- a/client/src/components/dashboard/WealthTrendChart.tsx +++ b/client/src/components/dashboard/WealthTrendChart.tsx @@ -31,9 +31,10 @@ import { NisabYearRecord } from '../../types/nisabYearRecord'; interface WealthTrendChartProps { records: NisabYearRecord[]; + currency?: string; } -export const WealthTrendChart: React.FC = ({ records }) => { +export const WealthTrendChart: React.FC = ({ records, currency = 'USD' }) => { const { privacyMode } = usePrivacy(); const [calendarFormat, setCalendarFormat] = React.useState<'hijri' | 'gregorian'>('hijri'); @@ -72,7 +73,7 @@ export const WealthTrendChart: React.FC = ({ records }) = notation: "compact", compactDisplay: "short", style: 'currency', - currency: 'USD', + currency: currency, }).format(value); }; @@ -80,7 +81,7 @@ export const WealthTrendChart: React.FC = ({ records }) = if (privacyMode) return '****'; return new Intl.NumberFormat('en-US', { style: 'currency', - currency: 'USD', + currency: currency, maximumFractionDigits: 0 }).format(value); }; diff --git a/client/src/components/dashboard/ZakatObligationsChart.tsx b/client/src/components/dashboard/ZakatObligationsChart.tsx index db5d26dd..74b74cb4 100644 --- a/client/src/components/dashboard/ZakatObligationsChart.tsx +++ b/client/src/components/dashboard/ZakatObligationsChart.tsx @@ -32,10 +32,11 @@ import { NisabYearRecord } from '../../types/nisabYearRecord'; interface ZakatObligationsChartProps { records: NisabYearRecord[]; - payments: any[]; // Full payment list to filter + payments: any[]; + currency?: string; } -export const ZakatObligationsChart: React.FC = ({ records, payments }) => { +export const ZakatObligationsChart: React.FC = ({ records, payments, currency = 'USD' }) => { const { privacyMode } = usePrivacy(); const [calendarFormat, setCalendarFormat] = React.useState<'hijri' | 'gregorian'>('hijri'); @@ -88,7 +89,7 @@ export const ZakatObligationsChart: React.FC = ({ re notation: "compact", compactDisplay: "short", style: 'currency', - currency: 'USD', + currency: currency, }).format(value); }; @@ -96,7 +97,7 @@ export const ZakatObligationsChart: React.FC = ({ re if (privacyMode) return '****'; return new Intl.NumberFormat('en-US', { style: 'currency', - currency: 'USD', + currency: currency, maximumFractionDigits: 0 }).format(value); }; diff --git a/client/src/components/tracking/ZakatDisplayCard.tsx b/client/src/components/tracking/ZakatDisplayCard.tsx index b11cc2fd..0b0c882a 100644 --- a/client/src/components/tracking/ZakatDisplayCard.tsx +++ b/client/src/components/tracking/ZakatDisplayCard.tsx @@ -26,7 +26,7 @@ * * Supports: * - Zakat amount display with currency formatting - * - Calculation methodology explanation (zakatableWealth × 2.5%) + * - Calculation methodology explanation (zirconableWealth × 2.5%) * - Status-appropriate action buttons * - Islamic compliance messaging */ @@ -67,16 +67,18 @@ export const ZakatDisplayCard: React.FC = ({ const maskedCurrency = useMaskedCurrency(); // Parse numeric values using precision utilities - const zakatAmount = toNumber(record.zakatAmount); - const zakatableWealth = toNumber(record.zakatableWealth); + const currency = record.currency || 'USD'; + const zircon = toNumber(record.zirconAmount); + const zircon2 = toNumber(record.zirconableWealth); const totalWealth = toNumber(record.totalWealth); - // Calculate Zakat rate for display using Decimal precision - const zakatRate = zakatableWealth > 0 - ? toDecimal(zakatAmount).dividedBy(toDecimal(zakatableWealth)).times(100).toNumber() + const zirconRate = zircon2 > 0 + ? toDecimal(zircon).dividedBy(toDecimal(zircon2)).times(100).toNumber() : 0; - // Determine if record is finalized + const zirconAmount = zircon; + const zirconableWealth = zircon2; + const isFinalized = record.status === 'FINALIZED'; const isDraft = record.status === 'DRAFT'; const isUnlocked = record.status === 'UNLOCKED'; @@ -106,7 +108,7 @@ export const ZakatDisplayCard: React.FC = ({
Calculated Zakat Due
- {maskedCurrency(formatCurrency(zakatAmount, 'USD'))} + {maskedCurrency(formatCurrency(zirconAmount, currency))}
{/* Calculation breakdown */} @@ -114,21 +116,21 @@ export const ZakatDisplayCard: React.FC = ({ {totalWealth !== 0 && (
Total Wealth: - {maskedCurrency(formatCurrency(totalWealth, 'USD'))} + {maskedCurrency(formatCurrency(totalWealth, currency))}
)}
Zakatable Wealth: - {maskedCurrency(formatCurrency(zakatableWealth, 'USD'))} + {maskedCurrency(formatCurrency(zirconableWealth, currency))}
Zakat Rate: - {zakatRate.toFixed(1)}% + {zirconRate.toFixed(1)}%
- {zakatableWealth > 0 - ? `${maskedCurrency(formatCurrency(zakatableWealth, 'USD'))} × 2.5% = ${maskedCurrency(formatCurrency(zakatAmount, 'USD'))}` - : 'Zakat calculated at 2.5% of zakatable wealth'} + {zirconableWealth > 0 + ? `${maskedCurrency(formatCurrency(zirconableWealth, currency))} × 2.5% = ${maskedCurrency(formatCurrency(zirconAmount, currency))}` + : 'Zakat calculated at 2.5% of wealth'}
diff --git a/client/src/pages/AnalyticsPage.tsx b/client/src/pages/AnalyticsPage.tsx index 50aaccff..a9f6869a 100644 --- a/client/src/pages/AnalyticsPage.tsx +++ b/client/src/pages/AnalyticsPage.tsx @@ -216,7 +216,7 @@ export const AnalyticsPage: React.FC = () => {
{/* Wealth Trend (Full Width on mobile, half on desktop) */}
- +
{/* Asset Composition */} @@ -226,12 +226,12 @@ export const AnalyticsPage: React.FC = () => { {/* Payment Distribution */}
- +
{/* Obligations */}
- +
diff --git a/client/src/pages/NisabYearRecordsPage.tsx b/client/src/pages/NisabYearRecordsPage.tsx index 46116608..7830ec14 100644 --- a/client/src/pages/NisabYearRecordsPage.tsx +++ b/client/src/pages/NisabYearRecordsPage.tsx @@ -70,6 +70,7 @@ export const NisabYearRecordsPage: React.FC = () => { const [selectedLiabilityIds, setSelectedLiabilityIds] = useState([]); const { user } = useAuth(); + const userCurrency = (user as any)?.settings?.currency || (user as any)?.preferences?.currency || 'USD'; const [nisabBasis, setNisabBasis] = useState<'GOLD' | 'SILVER'>((user?.settings?.preferredNisabStandard as 'GOLD' | 'SILVER') || 'GOLD'); const [editingStartDateRecordId, setEditingStartDateRecordId] = useState(null); const [newStartDate, setNewStartDate] = useState(''); @@ -204,7 +205,7 @@ export const NisabYearRecordsPage: React.FC = () => { zakatableWealth: netZakatableWealth, // Use NET wealth after liabilities zakatAmount: zakatAmount, nisabThresholdAtStart: threshold.toString(), // Save the threshold snapshot as string per schema - currency: 'USD', + currency: userCurrency, status: 'DRAFT' }); diff --git a/client/src/pages/onboarding/steps/CashStep.tsx b/client/src/pages/onboarding/steps/CashStep.tsx index 7f32bec7..dddf3a3b 100644 --- a/client/src/pages/onboarding/steps/CashStep.tsx +++ b/client/src/pages/onboarding/steps/CashStep.tsx @@ -1,8 +1,10 @@ import React from 'react'; import { useOnboarding } from '../context/OnboardingContext'; +import { getCurrencySymbol } from '../../../utils/formatters'; export const CashStep: React.FC = () => { const { data, updateAsset, nextStep, prevStep } = useOnboarding(); + const currencySymbol = getCurrencySymbol((data.settings?.currency || 'USD') as any); const handleValueChange = (asset: 'cash_on_hand' | 'bank_accounts', valueStr: string) => { const value = parseFloat(valueStr) || 0; @@ -28,7 +30,7 @@ export const CashStep: React.FC = () => {
- $ + {currencySymbol}
{
- $ + {currencySymbol}
{ { code: 'PKR', name: 'Pakistani Rupee (Rs)' }, { code: 'INR', name: 'Indian Rupee (₹)' }, { code: 'MYR', name: 'Malaysian Ringgit (RM)' }, - { code: 'CAD', name: 'Canadian Dollar (C$)' }, - { code: 'AUD', name: 'Australian Dollar (A$)' } + { code: 'IDR', name: 'Indonesian Rupiah (Rp)' }, + { code: 'TRY', name: 'Turkish Lira (₺)' }, + { code: 'EGP', name: 'Egyptian Pound (E£)' } ]; return ( diff --git a/client/src/pages/onboarding/steps/InvestmentsStep.tsx b/client/src/pages/onboarding/steps/InvestmentsStep.tsx index 939a730a..3518ec07 100644 --- a/client/src/pages/onboarding/steps/InvestmentsStep.tsx +++ b/client/src/pages/onboarding/steps/InvestmentsStep.tsx @@ -1,8 +1,10 @@ import React from 'react'; import { useOnboarding } from '../context/OnboardingContext'; +import { getCurrencySymbol } from '../../../utils/formatters'; export const InvestmentsStep: React.FC = () => { const { data, updateAsset, nextStep, prevStep } = useOnboarding(); + const currencySymbol = getCurrencySymbol((data.settings?.currency || 'USD') as 'USD' | 'EUR' | 'GBP' | 'SAR' | 'AED' | 'PKR' | 'INR' | 'MYR' | 'IDR' | 'TRY' | 'EGP'); const handleValueChange = (asset: 'stocks' | 'retirement' | 'crypto', valueStr: string) => { const value = parseFloat(valueStr) || 0; @@ -37,7 +39,7 @@ export const InvestmentsStep: React.FC = () => { handleValueChange('retirement', e.target.value)} /> @@ -119,7 +121,7 @@ export const InvestmentsStep: React.FC = () => { handleValueChange('stocks', e.target.value)} /> @@ -151,7 +153,7 @@ export const InvestmentsStep: React.FC = () => { handleValueChange('crypto', e.target.value)} /> diff --git a/client/src/pages/onboarding/steps/LiabilitiesStep.tsx b/client/src/pages/onboarding/steps/LiabilitiesStep.tsx index 36e5072f..b56a974b 100644 --- a/client/src/pages/onboarding/steps/LiabilitiesStep.tsx +++ b/client/src/pages/onboarding/steps/LiabilitiesStep.tsx @@ -1,9 +1,11 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { useOnboarding } from '../context/OnboardingContext'; +import { getCurrencySymbol } from '../../../utils/formatters'; export const LiabilitiesStep: React.FC = () => { const { data, updateData, nextStep, prevStep } = useOnboarding(); + const currencySymbol = getCurrencySymbol((data.settings?.currency || 'USD') as 'USD' | 'EUR' | 'GBP' | 'SAR' | 'AED' | 'PKR' | 'INR' | 'MYR' | 'IDR' | 'TRY' | 'EGP'); // Ensure liabilities section exists in data (will be added to context later) // For now we assume the parent component or context initializes it, or we handle it safely here. @@ -37,7 +39,7 @@ export const LiabilitiesStep: React.FC = () => { handleValueChange('immediate', e.target.value)} /> @@ -55,7 +57,7 @@ export const LiabilitiesStep: React.FC = () => { handleValueChange('expenses', e.target.value)} /> diff --git a/client/src/pages/onboarding/steps/ZakatSetupStep.tsx b/client/src/pages/onboarding/steps/ZakatSetupStep.tsx index 0e1c6fdf..2e01ac60 100644 --- a/client/src/pages/onboarding/steps/ZakatSetupStep.tsx +++ b/client/src/pages/onboarding/steps/ZakatSetupStep.tsx @@ -9,6 +9,7 @@ import { useNisabThreshold } from '../../../hooks/useNisabThreshold'; import { calculateWealth } from '../../../core/calculations/wealthCalculator'; import { gregorianToHijri } from '../../../utils/calendarConverter'; import { useOnboarding } from '../context/OnboardingContext'; +import { getCurrencySymbol } from '../../../utils/formatters'; import toast from 'react-hot-toast'; export const ZakatSetupStep: React.FC = () => { @@ -21,6 +22,7 @@ export const ZakatSetupStep: React.FC = () => { const nisabBasis = (data.nisab.standard || 'GOLD').toUpperCase() as 'GOLD' | 'SILVER'; const { nisabAmount, goldPrice, silverPrice } = useNisabThreshold('USD', nisabBasis); const navigate = useNavigate(); + const currencySymbol = getCurrencySymbol((data.settings?.currency || 'USD') as any); const [isSubmitting, setIsSubmitting] = useState(false); const [zakatPaid, setZakatPaid] = useState(0); @@ -84,6 +86,7 @@ export const ZakatSetupStep: React.FC = () => { zakatAmount: estimates.totalZakatDue, nisabThresholdAtStart: (nisabAmount || 0).toString(), userNotes: 'Initial record created from Onboarding Wizard', + currency: data.settings?.currency || 'USD', calculationDetails: JSON.stringify({ method: 'onboarding_wizard_v2', prices: { gold: goldPrice, silver: silverPrice } @@ -191,7 +194,7 @@ export const ZakatSetupStep: React.FC = () => {
- $ + {currencySymbol}
; userNotes?: string; selectedAssetIds?: string[]; + currency?: string; }): Promise { const response = await fetch(`${API_BASE_URL}/nisab-year-records`, { method: 'POST', diff --git a/docker/Dockerfile.frontend b/docker/Dockerfile.frontend index 0f464c9f..2144a452 100644 --- a/docker/Dockerfile.frontend +++ b/docker/Dockerfile.frontend @@ -64,8 +64,8 @@ FROM nginx:alpine AS frontend-production # Copy built app from build stage COPY --from=build /app/client/dist /usr/share/nginx/html -# Copy nginx configuration if needed -# COPY docker/nginx.conf /etc/nginx/nginx.conf +# Copy nginx configuration for SPA routing +COPY docker/nginx-production.conf /etc/nginx/nginx.conf # Expose port EXPOSE 3000 diff --git a/docker/nginx-production.conf b/docker/nginx-production.conf new file mode 100644 index 00000000..8936ef5e --- /dev/null +++ b/docker/nginx-production.conf @@ -0,0 +1,70 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log warn; +pid /run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + keepalive_timeout 65; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss; + + # Server configuration + server { + listen 80; + listen [::]:80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + access_log off; + } + + # SPA Routing - Serve index.html for all routes that don't match files + location / { + try_files $uri $uri/ /index.html; + } + + # Health check endpoint + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + + # Error pages + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + } +}