Skip to content

Commit 5937484

Browse files
committed
Added more consistency
1 parent 79cb15e commit 5937484

9 files changed

Lines changed: 238 additions & 31 deletions

src/components/CollapsibleAllocationTable.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,7 @@ export const CollapsibleAllocationTable: React.FC<CollapsibleAllocationTableProp
482482
<th className="sortable" onClick={() => requestSort(assetClass, 'asset.pricePerShare')}>
483483
Price/Share <span className="sort-indicator">{getSortIndicator(assetClass, 'asset.pricePerShare')}</span>
484484
</th>
485+
<th>Acq. Price</th>
485486
<th className="sortable" onClick={() => requestSort(assetClass, 'delta.currentValue')}>
486487
Current Value <span className="sort-indicator">{getSortIndicator(assetClass, 'delta.currentValue')}</span>
487488
</th>
@@ -617,6 +618,13 @@ export const CollapsibleAllocationTable: React.FC<CollapsibleAllocationTableProp
617618
'-'
618619
)}
619620
</td>
621+
<td className="currency-value">
622+
{isCashAsset || isValueOnlyAsset || !asset.acquisitionPrice ? (
623+
'-'
624+
) : (
625+
<PrivacyBlur isPrivacyMode={isPrivacyMode}>{formatCurrency(asset.acquisitionPrice, currency)}</PrivacyBlur>
626+
)}
627+
</td>
620628
<td className="currency-value">
621629
{isEditing ? (
622630
isCashAsset || isValueOnlyAsset ? (

src/components/NetWorthTrackerPage.tsx

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import { loadSettings, saveSettings, type DateFormat } from '../utils/cookieSett
3434
import { formatDate } from '../utils/dateFormatter';
3535
import { generateDemoNetWorthDataForYear } from '../utils/defaults';
3636
import { syncAssetAllocationToNetWorth, syncNetWorthToAssetAllocation, DEFAULT_ASSET_CLASS_TARGETS } from '../utils/dataSync';
37+
import { fetchMultipleClosingPricesForMonth } from '../utils/priceApi';
38+
import { fetchAssetPrices } from '../utils/dcaCalculator';
3739
import { formatDisplayCurrency, formatDisplayPercent, formatDisplayNumber } from '../utils/numberFormatter';
3840
import { DataManagement } from './DataManagement';
3941
import { HistoricalNetWorthChart, ChartViewMode } from './HistoricalNetWorthChart';
@@ -396,14 +398,64 @@ export function NetWorthTrackerPage() {
396398
const existingCashNames = new Set(monthData.cashEntries.map(c => c.accountName.toLowerCase()));
397399
const existingPensionNames = new Set(monthData.pensions.map(p => p.name.toLowerCase()));
398400

401+
// Collect tickers for price fetching
402+
const tickersToFetch: string[] = [];
403+
399404
// Add assets that don't already exist (by name)
405+
// Carry forward acquisitionPrice, pricePerShare will be updated by month-end fetch
400406
for (const asset of prevMonthData.assets) {
401407
if (!existingAssetNames.has(asset.name.toLowerCase())) {
402408
monthData.assets.push({
403409
...asset,
404410
id: generateNetWorthId(),
411+
acquisitionPrice: asset.acquisitionPrice ?? asset.pricePerShare,
405412
});
406413
existingAssetNames.add(asset.name.toLowerCase());
414+
if (asset.ticker && asset.ticker.trim().length > 0) {
415+
tickersToFetch.push(asset.ticker.trim().toUpperCase());
416+
}
417+
}
418+
}
419+
420+
// Fetch month-end closing prices for inherited ticker-based assets
421+
if (tickersToFetch.length > 0) {
422+
const isCurrentMonth = selectedYear === currentYear && selectedMonth === currentMonth;
423+
if (isCurrentMonth) {
424+
// For current month: fetch latest market prices
425+
fetchAssetPrices([...new Set(tickersToFetch)]).then(prices => {
426+
setData(prevData => {
427+
const updated = deepCloneData(prevData);
428+
const yr = updated.years.find(y => y.year === selectedYear);
429+
const mo = yr?.months.find(m => m.month === selectedMonth);
430+
if (mo) {
431+
for (const asset of mo.assets) {
432+
const ticker = asset.ticker?.trim().toUpperCase();
433+
if (ticker && prices[ticker] != null) {
434+
asset.pricePerShare = prices[ticker]!;
435+
}
436+
}
437+
}
438+
return updated;
439+
});
440+
});
441+
} else {
442+
// For past months: fetch closing price for that month's last trading day
443+
fetchMultipleClosingPricesForMonth([...new Set(tickersToFetch)], selectedYear, selectedMonth).then(prices => {
444+
setData(prevData => {
445+
const updated = deepCloneData(prevData);
446+
const yr = updated.years.find(y => y.year === selectedYear);
447+
const mo = yr?.months.find(m => m.month === selectedMonth);
448+
if (mo) {
449+
for (const asset of mo.assets) {
450+
const ticker = asset.ticker?.trim().toUpperCase();
451+
if (ticker && prices[ticker]) {
452+
asset.pricePerShare = prices[ticker]!.price;
453+
}
454+
}
455+
}
456+
return updated;
457+
});
458+
});
407459
}
408460
}
409461

@@ -1033,7 +1085,8 @@ export function NetWorthTrackerPage() {
10331085
<th>Ticker</th>
10341086
<th>Class</th>
10351087
<th>Shares</th>
1036-
<th className="amount-col">Price</th>
1088+
<th className="amount-col">Mkt Price</th>
1089+
<th className="amount-col">Acq. Price</th>
10371090
<th className="amount-col">Value</th>
10381091
<th className="actions-col">Actions</th>
10391092
</tr>
@@ -1050,6 +1103,7 @@ export function NetWorthTrackerPage() {
10501103
<td>{ASSET_CLASSES.find(c => c.id === asset.assetClass)?.name || asset.assetClass}</td>
10511104
<td>{isValueOnlyAsset ? '-' : <PrivacyBlur isPrivacyMode={isPrivacyMode}>{formatDisplayNumber(asset.shares)}</PrivacyBlur>}</td>
10521105
<td className="amount-col">{isValueOnlyAsset ? '-' : <PrivacyBlur isPrivacyMode={isPrivacyMode}>{formatCurrency(asset.pricePerShare, asset.currency)}</PrivacyBlur>}</td>
1106+
<td className="amount-col">{isValueOnlyAsset || !asset.acquisitionPrice ? '-' : <PrivacyBlur isPrivacyMode={isPrivacyMode}>{formatCurrency(asset.acquisitionPrice, asset.currency)}</PrivacyBlur>}</td>
10531107
<td className="amount-col"><PrivacyBlur isPrivacyMode={isPrivacyMode}>{formatCurrency(asset.shares * asset.pricePerShare, asset.currency)}</PrivacyBlur></td>
10541108
<td className="actions-col">
10551109
<button

src/components/SharedAssetDialog.tsx

Lines changed: 62 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export const SharedAssetDialog: React.FC<SharedAssetDialogProps> = ({
125125
const [isin, setIsin] = useState('');
126126
const [shares, setShares] = useState<string>('1');
127127
const [pricePerShare, setPricePerShare] = useState<string>('');
128+
const [acquisitionPrice, setAcquisitionPrice] = useState<string>('');
128129
const [currency, setCurrency] = useState<SupportedCurrency>(defaultCurrency);
129130
const [targetMode, setTargetMode] = useState<AllocationMode>('PERCENTAGE');
130131
const [targetPercent, setTargetPercent] = useState<string>('0');
@@ -178,6 +179,7 @@ export const SharedAssetDialog: React.FC<SharedAssetDialogProps> = ({
178179
setIsin(asset.isin || '');
179180
setShares(asset.shares?.toString() || '1');
180181
setPricePerShare(asset.pricePerShare?.toString() || '');
182+
setAcquisitionPrice(asset.acquisitionPrice?.toString() || '');
181183
setCurrency(asset.originalCurrency || 'EUR');
182184
setTargetMode(asset.targetMode);
183185
setTargetPercent(asset.targetPercent?.toString() || '0');
@@ -206,6 +208,7 @@ export const SharedAssetDialog: React.FC<SharedAssetDialogProps> = ({
206208
setTicker(holding.ticker);
207209
setShares(holding.shares.toString());
208210
setPricePerShare(holding.pricePerShare.toString());
211+
setAcquisitionPrice(holding.acquisitionPrice?.toString() || '');
209212
setCurrency(holding.currency);
210213
setNote(holding.note || '');
211214
setInstitutionCode('');
@@ -246,6 +249,7 @@ export const SharedAssetDialog: React.FC<SharedAssetDialogProps> = ({
246249
setIsin('');
247250
setShares('1');
248251
setPricePerShare('');
252+
setAcquisitionPrice('');
249253
setCurrency(defaultCurrency);
250254
setTargetMode('PERCENTAGE');
251255
setTargetPercent('0');
@@ -514,6 +518,7 @@ export const SharedAssetDialog: React.FC<SharedAssetDialogProps> = ({
514518

515519
if (mode === 'assetAllocation') {
516520
// Asset Allocation mode
521+
const acqPriceNum = parseFloat(acquisitionPrice) || undefined;
517522
const asset: Omit<Asset, 'id'> & { id?: string } = {
518523
...(initialData && { id: (initialData as Asset).id }),
519524
name: name.trim(),
@@ -524,6 +529,7 @@ export const SharedAssetDialog: React.FC<SharedAssetDialogProps> = ({
524529
currentValue: valueInEUR,
525530
shares: sharesNum,
526531
pricePerShare: priceNum,
532+
acquisitionPrice: acqPriceNum,
527533
marketPrice: priceNum,
528534
originalCurrency: currency !== 'EUR' ? currency : undefined,
529535
originalValue: currency !== 'EUR' ? valueNum : undefined,
@@ -539,12 +545,14 @@ export const SharedAssetDialog: React.FC<SharedAssetDialogProps> = ({
539545
onSubmit(asset);
540546
} else {
541547
// Net Worth Tracker mode
548+
const acqPriceNum = parseFloat(acquisitionPrice) || undefined;
542549
const holding: Omit<AssetHolding, 'id'> & { id?: string } = {
543550
...(initialData && { id: (initialData as AssetHolding).id }),
544551
name: name.trim(),
545552
ticker: generatedTicker,
546553
shares: sharesNum,
547554
pricePerShare: priceNum,
555+
acquisitionPrice: acqPriceNum,
548556
currency,
549557
assetClass: mapToNetWorthAssetClass(assetClass, subAssetType),
550558
note: note.trim() || undefined,
@@ -562,6 +570,7 @@ export const SharedAssetDialog: React.FC<SharedAssetDialogProps> = ({
562570
setIsin('');
563571
setShares('1');
564572
setPricePerShare('');
573+
setAcquisitionPrice('');
565574
setCurrency(defaultCurrency);
566575
setTargetPercent('0');
567576
setNote('');
@@ -838,37 +847,61 @@ export const SharedAssetDialog: React.FC<SharedAssetDialogProps> = ({
838847

839848
{/* Only show shares and price fields for non-cash accounts, non-value-only assets, or for MONEY_ETF */}
840849
{(assetClass !== 'CASH' || subAssetType === 'MONEY_ETF') && !VALUE_ONLY_TYPES.includes(subAssetType) && (
841-
<div className="form-row">
842-
<div className="form-group">
843-
<label>Number of Shares *</label>
844-
<input
845-
type="number"
846-
value={shares}
847-
onChange={(e) => handleSharesChange(e.target.value)}
848-
placeholder="e.g., 100"
849-
className="dialog-input"
850-
min="0"
851-
step="any"
852-
required
853-
/>
854-
</div>
850+
<>
851+
<div className="form-row">
852+
<div className="form-group">
853+
<label>Number of Shares *</label>
854+
<input
855+
type="number"
856+
value={shares}
857+
onChange={(e) => handleSharesChange(e.target.value)}
858+
placeholder="e.g., 100"
859+
className="dialog-input"
860+
min="0"
861+
step="any"
862+
required
863+
/>
864+
</div>
855865

856-
<div className="form-group">
857-
<label>Price per Share (Market)</label>
858-
<input
859-
type="text"
860-
value={pricePerShare ? `${parseFloat(pricePerShare).toFixed(2)}` : '—'}
861-
className="dialog-input dialog-input-calculated"
862-
disabled
863-
title="Fetched automatically from Yahoo Finance"
864-
/>
865-
{!pricePerShare && needsTicker && ticker.trim() && (
866-
<span className="setting-help" style={{ color: 'var(--warning)' }}>
867-
Click the fetch button next to the ticker to get the price
868-
</span>
869-
)}
866+
<div className="form-group">
867+
<label>Price per Share (Market)</label>
868+
<input
869+
type="text"
870+
value={pricePerShare ? `${parseFloat(pricePerShare).toFixed(2)}` : '—'}
871+
className="dialog-input dialog-input-calculated"
872+
disabled
873+
title="Fetched automatically from Yahoo Finance"
874+
/>
875+
{!pricePerShare && needsTicker && ticker.trim() && (
876+
<span className="setting-help" style={{ color: 'var(--warning)' }}>
877+
Click the fetch button next to the ticker to get the price
878+
</span>
879+
)}
880+
</div>
870881
</div>
871-
</div>
882+
883+
{/* Acquisition price — editable, shown for share-based assets */}
884+
{needsTicker && (
885+
<div className="form-row">
886+
<div className="form-group">
887+
<label>Acquisition Price per Share</label>
888+
<input
889+
type="number"
890+
value={acquisitionPrice}
891+
onChange={(e) => setAcquisitionPrice(e.target.value)}
892+
placeholder={pricePerShare ? `Defaults to ${parseFloat(pricePerShare).toFixed(2)}` : 'Price you paid'}
893+
className="dialog-input"
894+
min="0"
895+
step="any"
896+
title="The price at which you originally bought this asset"
897+
/>
898+
<span className="setting-help">
899+
Leave blank to use market price as acquisition price
900+
</span>
901+
</div>
902+
</div>
903+
)}
904+
</>
872905
)}
873906

874907
{/* Value is directly editable for cash accounts (not MONEY_ETF) and PROPERTY assets */}

src/hooks/useAssetPrices.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export function useAssetPrices(): UseAssetPricesReturn {
7070
}
7171

7272
// Update marketPrice and recalculate currentValue if shares are tracked
73+
// Preserve acquisitionPrice — if not set, default to the original pricePerShare
7374
const shares = asset.shares ?? 1;
7475
const newValue = shares * newPrice;
7576

@@ -78,6 +79,7 @@ export function useAssetPrices(): UseAssetPricesReturn {
7879
...asset,
7980
marketPrice: newPrice,
8081
currentValue: newValue,
82+
acquisitionPrice: asset.acquisitionPrice ?? asset.pricePerShare,
8183
};
8284
});
8385

src/types/assetAllocation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export interface Asset {
5555
currentValue: number; // Value in EUR (converted if entered in another currency)
5656
shares?: number; // Number of shares owned (optional, for stocks/ETFs/bonds)
5757
pricePerShare?: number; // Price per share (optional, for stocks/ETFs/bonds)
58+
acquisitionPrice?: number; // Price per share at which the asset was acquired (editable)
5859
originalCurrency?: SupportedCurrency; // The currency the value was originally entered in (defaults to EUR)
5960
originalValue?: number; // The original value before conversion to EUR
6061
targetMode: AllocationMode;

src/types/netWorthTracker.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ export interface AssetHolding {
6262
ticker: string;
6363
name: string;
6464
shares: number; // Number of shares owned
65-
pricePerShare: number; // Price per share at the time of entry
65+
pricePerShare: number; // Current market price per share (closing price for the month)
66+
acquisitionPrice?: number; // Price per share at which the asset was acquired (editable)
6667
currency: SupportedCurrency;
6768
assetClass: 'STOCKS' | 'BONDS' | 'ETF' | 'CRYPTO' | 'REAL_ESTATE' | 'PRIVATE_EQUITY' | 'VEHICLE' | 'COLLECTIBLE' | 'ART' | 'COMMODITIES' | 'OTHER';
6869
note?: string;

src/utils/cookieStorage.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,12 @@ export function loadAssetAllocation(): {
107107
const parsedAssets = JSON.parse(decryptedAssets);
108108
if (Array.isArray(parsedAssets) && parsedAssets.every(isValidAsset)) {
109109
assets = parsedAssets;
110+
// Migration: backfill acquisitionPrice for assets that don't have it
111+
for (const asset of assets) {
112+
if (asset.acquisitionPrice === undefined && asset.pricePerShare !== undefined) {
113+
asset.acquisitionPrice = asset.pricePerShare;
114+
}
115+
}
110116
}
111117
}
112118
}
@@ -422,6 +428,22 @@ export function saveNetWorthTrackerData(data: NetWorthTrackerData): void {
422428
}
423429
}
424430

431+
/**
432+
* Migration: backfill acquisitionPrice for assets that don't have it.
433+
* Sets acquisitionPrice = pricePerShare for existing data where acquisitionPrice is missing.
434+
*/
435+
function migrateAcquisitionPrice(data: NetWorthTrackerData): void {
436+
for (const yearData of data.years) {
437+
for (const monthData of yearData.months) {
438+
for (const asset of monthData.assets) {
439+
if (asset.acquisitionPrice === undefined) {
440+
asset.acquisitionPrice = asset.pricePerShare;
441+
}
442+
}
443+
}
444+
}
445+
}
446+
425447
/**
426448
* Load Net Worth Tracker data from encrypted localStorage
427449
*/
@@ -448,6 +470,8 @@ export function loadNetWorthTrackerData(): NetWorthTrackerData | null {
448470
if (decryptedData) {
449471
const parsed = JSON.parse(decryptedData);
450472
if (isValidNetWorthTrackerData(parsed)) {
473+
// Migration: backfill acquisitionPrice for assets that don't have it
474+
migrateAcquisitionPrice(parsed);
451475
return parsed;
452476
}
453477
}

src/utils/dataSync.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ export function syncAssetAllocationToNetWorth(
203203
name: asset.name,
204204
shares,
205205
pricePerShare,
206+
acquisitionPrice: asset.acquisitionPrice ?? pricePerShare,
206207
currency: (asset.originalCurrency || 'EUR') as SupportedCurrency,
207208
assetClass: mapAssetClassToNetWorth(asset.assetClass),
208209
// Preserve sync metadata (hidden from UI)
@@ -272,6 +273,7 @@ export function syncNetWorthToAssetAllocation(
272273
currentValue: holding.shares * holding.pricePerShare,
273274
shares: holding.shares,
274275
pricePerShare: holding.pricePerShare,
276+
acquisitionPrice: holding.acquisitionPrice,
275277
originalCurrency: holding.currency as SupportedCurrency,
276278
originalValue: holding.shares * holding.pricePerShare,
277279
isin: holding.isin,

0 commit comments

Comments
 (0)