Skip to content
Open
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
8 changes: 5 additions & 3 deletions components/status/CashBalancePane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { useMemo, useState } from 'react';
import { Panel } from '../shared';
import { useBalancesStore } from '../../stores/entities/balances';
import { useCompanyStore } from '../../stores/company';
import { primaryCurrencyFor, sortBalances } from '../../lib/currency';
import { useSettingsStore } from '../../stores/settings';
import { resolvePrimaryCurrency, sortBalances } from '../../lib/currency';

function formatAmount(amount: number): string {
return Math.round(amount).toLocaleString('en-US');
Expand Down Expand Up @@ -51,15 +52,16 @@ export function CashBalancePane() {
const fetched = useBalancesStore((s) => s.fetched);
const entities = useBalancesStore((s) => s.entities);
const company = useCompanyStore((s) => s.company);
const preferredCurrency = useSettingsStore((s) => s.preferredCurrency);
const [expanded, setExpanded] = useState(false);

const balances = useMemo(
() =>
sortBalances(
Array.from(entities.values()),
company ? primaryCurrencyFor(company.countryId) : null
resolvePrimaryCurrency(company?.countryId ?? null, preferredCurrency)
),
[entities, company]
[entities, company, preferredCurrency]
);

const title = company
Expand Down
37 changes: 36 additions & 1 deletion components/views/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ const refreshModeOptions: { id: RefreshMode; label: string }[] = [
{ id: 'auto', label: 'Auto' },
];

// PrUn's currency set is fixed (ECD excluded — legacy, not spendable).
// null = derive from the company's faction (the unchanged default).
const primaryCurrencyOptions: { id: string | null; label: string }[] = [
{ id: null, label: 'Auto' },
{ id: 'AIC', label: 'AIC' },
{ id: 'CIS', label: 'CIS' },
{ id: 'ICA', label: 'ICA' },
{ id: 'NCC', label: 'NCC' },
];

/** 24-bit hex number → CSS hex string, for inline preset-preview swatches. */
function toCssHex(value: number): string {
return `#${value.toString(16).padStart(6, '0')}`;
Expand Down Expand Up @@ -150,7 +160,7 @@ export function validateRepairThresholds(
}

export function SettingsView() {
const { fio, setFioConfig, setFioLastFetch, materialTheme, setMaterialTheme, uiTheme, setUiTheme, burnThresholds, setBurnThresholds } = useSettingsStore();
const { fio, setFioConfig, setFioLastFetch, materialTheme, setMaterialTheme, uiTheme, setUiTheme, burnThresholds, setBurnThresholds, preferredCurrency, setPreferredCurrency } = useSettingsStore();

// Burn threshold local state — strings for free-form editing, persist on valid input
const [critical, setCritical] = useState(String(burnThresholds.critical));
Expand Down Expand Up @@ -532,6 +542,31 @@ export function SettingsView() {
</div>
</Panel>

{/* Primary Currency Section */}
<Panel title="Primary Currency">
<div className="space-y-3">
<div className="flex gap-2">
{primaryCurrencyOptions.map((option) => (
<button
key={option.label}
onClick={() => setPreferredCurrency(option.id)}
className={`flex-1 min-h-touch px-2 py-2 font-mono text-[11px] font-semibold uppercase tracking-wider ${
preferredCurrency === option.id
? 'bg-prun-yellow text-apxm-bg'
: 'border border-apxm-accent text-apxm-muted'
}`}
>
{option.label}
</button>
))}
</div>
<p className="text-xs text-apxm-muted">
The headline balance on the Status tab. Auto uses your company&apos;s
faction currency.
</p>
</div>
</Panel>

{/* UI Theme Section */}
<Panel title="Theme">

Expand Down
17 changes: 17 additions & 0 deletions components/views/__tests__/SettingsView.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,23 @@ describe('SettingsView repair threshold validation', () => {
});
});

describe('preferred currency setting (#63)', () => {
beforeEach(() => {
useSettingsStore.getState().reset();
});

it('defaults to null (faction-derived headline currency)', () => {
expect(useSettingsStore.getState().preferredCurrency).toBeNull();
});

it('setPreferredCurrency round-trips a code and back to Auto', () => {
useSettingsStore.getState().setPreferredCurrency('NCC');
expect(useSettingsStore.getState().preferredCurrency).toBe('NCC');
useSettingsStore.getState().setPreferredCurrency(null);
expect(useSettingsStore.getState().preferredCurrency).toBeNull();
});
});

describe('SettingsView burn threshold validation', () => {
describe('validateThresholds', () => {
it('returns null for valid thresholds', () => {
Expand Down
23 changes: 22 additions & 1 deletion lib/__tests__/currency.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { primaryCurrencyFor, sortBalances } from '../currency';
import { primaryCurrencyFor, resolvePrimaryCurrency, sortBalances } from '../currency';
import type { PrunApi } from '../../types/prun-api';

function balance(currency: string, amount = 1000): PrunApi.CurrencyAmount {
Expand All @@ -24,6 +24,27 @@ describe('primaryCurrencyFor', () => {
});
});

describe('resolvePrimaryCurrency', () => {
const AI_GUID = '18419e6fd11b5af8bf0d0a996ad1a622';

it('override wins over the faction-derived currency (#63)', () => {
expect(resolvePrimaryCurrency(AI_GUID, 'NCC')).toBe('NCC');
});

it('falls back to the faction currency when no override is set', () => {
expect(resolvePrimaryCurrency(AI_GUID, null)).toBe('AIC');
});

it('override works even without company data', () => {
expect(resolvePrimaryCurrency(null, 'CIS')).toBe('CIS');
});

it('returns null with no override and no/unknown countryId', () => {
expect(resolvePrimaryCurrency(null, null)).toBeNull();
expect(resolvePrimaryCurrency('unknown-guid', null)).toBeNull();
});
});

describe('sortBalances', () => {
it('filters out ECD', () => {
const result = sortBalances([balance('ECD'), balance('AIC')], null);
Expand Down
13 changes: 13 additions & 0 deletions lib/currency.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ export function primaryCurrencyFor(countryId: string): string | null {
return COUNTRY_CURRENCY[countryId] ?? null;
}

/**
* The currency to headline in the liquidity pane. A user override wins —
* players often trade in a non-faction currency based on where they operate
* (issue #63) — otherwise fall back to the faction-derived primary.
*/
export function resolvePrimaryCurrency(
countryId: string | null,
override: string | null
): string | null {
if (override) return override;
return countryId ? primaryCurrencyFor(countryId) : null;
}

/**
* Display order for the liquidity list: ECD filtered out (legacy currency,
* not spendable), the company's primary currency first, remaining
Expand Down
7 changes: 7 additions & 0 deletions stores/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ interface SettingsState {
* Opt-in per the action-authorisation rule. No UI yet — the Settings
* toggle lands with the ACT views (#25 Phase D). */
autoConfirm: boolean;
/** Headline currency for the liquidity pane. null (default) derives it from
* the company's faction; a currency code overrides — players often trade
* in a non-faction currency based on location (#63). */
preferredCurrency: string | null;
}

interface SettingsActions {
Expand All @@ -69,6 +73,7 @@ interface SettingsActions {
setNoBuy: (tickers: string[]) => void;
setRefreshMode: (mode: RefreshMode) => void;
setAutoConfirm: (enabled: boolean) => void;
setPreferredCurrency: (currency: string | null) => void;
reset: () => void;
}

Expand Down Expand Up @@ -96,6 +101,7 @@ const initialState: SettingsState = {
noBuy: [],
refreshMode: 'manual',
autoConfirm: false,
preferredCurrency: null,
};

// Check if browser storage API is available
Expand Down Expand Up @@ -195,6 +201,7 @@ export const useSettingsStore = create<SettingsStore>()(
setNoBuy: (tickers) => set({ noBuy: tickers }),
setRefreshMode: (mode) => set({ refreshMode: mode }),
setAutoConfirm: (enabled) => set({ autoConfirm: enabled }),
setPreferredCurrency: (currency) => set({ preferredCurrency: currency }),

reset: () => set(initialState),
}),
Expand Down