Restructured
This commit is contained in:
@ -1,7 +1,7 @@
|
||||
# Stage 1: Build static content (Node.js not needed, just copy HTML)
|
||||
FROM alpine:3.18 AS static-builder
|
||||
WORKDIR /build
|
||||
COPY index.html .
|
||||
COPY static/index.html .
|
||||
RUN mkdir -p /build/static && cp index.html /build/static/
|
||||
|
||||
# Stage 2: Build Go backend
|
||||
@ -9,11 +9,9 @@ FROM golang:1.21-alpine AS go-builder
|
||||
WORKDIR /app
|
||||
|
||||
# Copy go.mod and go.sum if they exist, otherwise they'll be created
|
||||
COPY go.mod go.sum* ./
|
||||
RUN go mod download || true
|
||||
|
||||
# Copy the Go source code
|
||||
COPY main.go .
|
||||
COPY backend/. .
|
||||
RUN go mod download || true
|
||||
|
||||
# Build the Go binary
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o stock-analyzer .
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
stock-analyzer:
|
||||
build:
|
||||
|
||||
518
index.html
518
index.html
@ -1,518 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Stock Tax Analyzer</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
<script type="text/babel">
|
||||
const { useState } = React;
|
||||
|
||||
// Lucide icons as inline SVG components
|
||||
const Upload = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="17 8 12 3 7 8"/>
|
||||
<line x1="12" y1="3" x2="12" y2="15"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const Loader2 = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="animate-spin">
|
||||
<path d="M21 12a9 9 0 1 1-6.219-8.56"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
function StockTaxAnalyzer() {
|
||||
const [results, setResults] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const knownSplits = {
|
||||
'NVDA': [
|
||||
{ date: '2024-06-10', ratio: 10 },
|
||||
{ date: '2021-07-20', ratio: 4 },
|
||||
{ date: '2007-09-11', ratio: 1.5 },
|
||||
{ date: '2006-04-07', ratio: 2 },
|
||||
{ date: '2001-09-17', ratio: 2 },
|
||||
{ date: '2000-06-27', ratio: 2 }
|
||||
],
|
||||
'TSLA': [
|
||||
{ date: '2022-08-25', ratio: 3 },
|
||||
{ date: '2020-08-31', ratio: 5 }
|
||||
],
|
||||
'AAPL': [
|
||||
{ date: '2020-08-31', ratio: 4 },
|
||||
{ date: '2014-06-09', ratio: 7 },
|
||||
{ date: '2005-02-28', ratio: 2 },
|
||||
{ date: '2000-06-21', ratio: 2 },
|
||||
{ date: '1987-06-16', ratio: 2 }
|
||||
],
|
||||
'GOOGL': [
|
||||
{ date: '2022-07-18', ratio: 20 }
|
||||
],
|
||||
'AMZN': [
|
||||
{ date: '2022-06-06', ratio: 20 },
|
||||
{ date: '1999-09-02', ratio: 2 },
|
||||
{ date: '1999-01-05', ratio: 3 },
|
||||
{ date: '1998-06-02', ratio: 2 }
|
||||
],
|
||||
'MSFT': [],
|
||||
'NFLX': [],
|
||||
'AMD': [],
|
||||
'COIN': []
|
||||
};
|
||||
|
||||
const fetchStockPrices = async (tickers) => {
|
||||
try {
|
||||
const response = await fetch('/api/stock-prices', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ tickers }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch stock prices');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return { prices: data.prices, exchangeRate: data.exchangeRate };
|
||||
} catch (err) {
|
||||
console.error('Error fetching prices:', err);
|
||||
return { prices: {}, exchangeRate: 0.92, error: err.message };
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileUpload = async (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const text = await file.text();
|
||||
const lines = text.split('\n');
|
||||
const headers = lines[0].split(',').map(h => h.trim().replace(/"/g, ''));
|
||||
|
||||
const data = lines.slice(1).map(line => {
|
||||
const values = line.split(',').map(v => v.trim().replace(/"/g, ''));
|
||||
const row = {};
|
||||
headers.forEach((header, i) => {
|
||||
row[header] = values[i];
|
||||
});
|
||||
return row;
|
||||
}).filter(row => row.Date);
|
||||
|
||||
data.sort((a, b) => new Date(a.Date) - new Date(b.Date));
|
||||
|
||||
const today = new Date();
|
||||
const twoYearsAgo = new Date(today);
|
||||
twoYearsAgo.setFullYear(today.getFullYear() - 2);
|
||||
|
||||
const allBuys = data.filter(row => row.Type === 'BUY - MARKET' && row.Ticker);
|
||||
const oldBuys = allBuys.filter(row => new Date(row.Date) < twoYearsAgo);
|
||||
|
||||
const processHoldings = (buys) => {
|
||||
const holdings = {};
|
||||
buys.forEach(buy => {
|
||||
const ticker = buy.Ticker;
|
||||
if (!holdings[ticker]) {
|
||||
holdings[ticker] = {
|
||||
quantity: 0,
|
||||
totalCost: 0,
|
||||
transactions: [],
|
||||
dates: [],
|
||||
appliedSplits: []
|
||||
};
|
||||
}
|
||||
|
||||
let quantity = parseFloat(buy.Quantity) || 0;
|
||||
const buyDate = new Date(buy.Date);
|
||||
const priceStr = buy['Price per share'] || '';
|
||||
let price = parseFloat(priceStr.replace(/[$,]/g, ''));
|
||||
|
||||
let splitMultiplier = 1;
|
||||
if (knownSplits[ticker]) {
|
||||
const relevantSplits = knownSplits[ticker].filter(split =>
|
||||
new Date(split.date) > buyDate
|
||||
);
|
||||
|
||||
relevantSplits.forEach(split => {
|
||||
splitMultiplier *= split.ratio;
|
||||
const splitExists = holdings[ticker].appliedSplits.some(
|
||||
s => s.date === split.date
|
||||
);
|
||||
if (!splitExists) {
|
||||
holdings[ticker].appliedSplits.push(split);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const adjustedQuantity = quantity * splitMultiplier;
|
||||
|
||||
holdings[ticker].quantity += adjustedQuantity;
|
||||
if (!isNaN(price)) {
|
||||
holdings[ticker].totalCost += price * quantity;
|
||||
}
|
||||
holdings[ticker].transactions.push(buy);
|
||||
holdings[ticker].dates.push(buyDate);
|
||||
});
|
||||
return holdings;
|
||||
};
|
||||
|
||||
const allHoldings = processHoldings(allBuys);
|
||||
const oldHoldings = processHoldings(oldBuys);
|
||||
|
||||
const allTickers = [...new Set([...Object.keys(allHoldings), ...Object.keys(oldHoldings)])];
|
||||
|
||||
const { prices, exchangeRate } = await fetchStockPrices(allTickers);
|
||||
|
||||
const formattedHoldings = allTickers.map(ticker => {
|
||||
const allInfo = allHoldings[ticker] || { quantity: 0, totalCost: 0, transactions: [], dates: [], appliedSplits: [] };
|
||||
const oldInfo = oldHoldings[ticker] || { quantity: 0, totalCost: 0, transactions: [], dates: [], appliedSplits: [] };
|
||||
|
||||
const currentPrice = prices[ticker];
|
||||
|
||||
const totalQuantity = allInfo.quantity;
|
||||
const totalCostEur = allInfo.totalCost * exchangeRate;
|
||||
const totalCurrentValueEur = currentPrice?.success ? totalQuantity * currentPrice.eur : null;
|
||||
const totalGainLossEur = totalCurrentValueEur ? totalCurrentValueEur - totalCostEur : null;
|
||||
const totalGainLossPercent = totalGainLossEur && totalCostEur ? (totalGainLossEur / totalCostEur) * 100 : null;
|
||||
|
||||
const oldQuantity = oldInfo.quantity;
|
||||
const oldCostEur = oldInfo.totalCost * exchangeRate;
|
||||
const oldCurrentValueEur = currentPrice?.success ? oldQuantity * currentPrice.eur : null;
|
||||
const oldGainLossEur = oldCurrentValueEur ? oldCurrentValueEur - oldCostEur : null;
|
||||
const oldGainLossPercent = oldGainLossEur && oldCostEur ? (oldGainLossEur / oldCostEur) * 100 : null;
|
||||
|
||||
return {
|
||||
ticker,
|
||||
totalQuantity,
|
||||
totalCostEur,
|
||||
totalCurrentValueEur,
|
||||
totalGainLossEur,
|
||||
totalGainLossPercent,
|
||||
totalTransactions: allInfo.transactions.length,
|
||||
oldQuantity,
|
||||
oldCostEur,
|
||||
oldCurrentValueEur,
|
||||
oldGainLossEur,
|
||||
oldGainLossPercent,
|
||||
oldTransactions: oldInfo.transactions.length,
|
||||
oldestDate: oldInfo.dates.length > 0 ? new Date(Math.min(...oldInfo.dates)) : null,
|
||||
newestOldDate: oldInfo.dates.length > 0 ? new Date(Math.max(...oldInfo.dates)) : null,
|
||||
currentPriceEur: currentPrice?.eur,
|
||||
avgPriceEur: totalQuantity > 0 ? (allInfo.totalCost / totalQuantity) * exchangeRate : 0,
|
||||
oldAvgPriceEur: oldQuantity > 0 ? (oldInfo.totalCost / oldQuantity) * exchangeRate : 0,
|
||||
priceAvailable: currentPrice?.success || false,
|
||||
splits: oldInfo.appliedSplits.sort((a, b) => new Date(a.date) - new Date(b.date)),
|
||||
hasSplitData: knownSplits[ticker] !== undefined
|
||||
};
|
||||
}).sort((a, b) => a.ticker.localeCompare(b.ticker));
|
||||
|
||||
const totalAllInvestedEur = formattedHoldings.reduce((sum, h) => sum + h.totalCostEur, 0);
|
||||
const totalAllCurrentValueEur = formattedHoldings.reduce((sum, h) => sum + (h.totalCurrentValueEur || 0), 0);
|
||||
const totalAllGainLossEur = totalAllCurrentValueEur - totalAllInvestedEur;
|
||||
const totalAllGainLossPercent = (totalAllGainLossEur / totalAllInvestedEur) * 100;
|
||||
|
||||
const totalOldInvestedEur = formattedHoldings.reduce((sum, h) => sum + h.oldCostEur, 0);
|
||||
const totalOldCurrentValueEur = formattedHoldings.reduce((sum, h) => sum + (h.oldCurrentValueEur || 0), 0);
|
||||
const totalOldGainLossEur = totalOldCurrentValueEur - totalOldInvestedEur;
|
||||
const totalOldGainLossPercent = totalOldInvestedEur > 0 ? (totalOldGainLossEur / totalOldInvestedEur) * 100 : 0;
|
||||
|
||||
setResults({
|
||||
holdings: formattedHoldings,
|
||||
totalAllInvestedEur,
|
||||
totalAllCurrentValueEur,
|
||||
totalAllGainLossEur,
|
||||
totalAllGainLossPercent,
|
||||
totalOldInvestedEur,
|
||||
totalOldCurrentValueEur,
|
||||
totalOldGainLossEur,
|
||||
totalOldGainLossPercent,
|
||||
exchangeRate,
|
||||
cutoffDate: twoYearsAgo
|
||||
});
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
setError('Error parsing file: ' + err.message);
|
||||
setResults(null);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 p-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="bg-white rounded-lg shadow-lg p-8">
|
||||
<h1 className="text-3xl font-bold text-gray-800 mb-2">
|
||||
Stock Tax Analyzer
|
||||
</h1>
|
||||
<p className="text-gray-600 mb-2">
|
||||
Compare total holdings vs. stocks bought more than 2 years ago
|
||||
</p>
|
||||
<p className="text-sm text-indigo-600 mb-6">
|
||||
✓ Real-time prices in EUR • Split adjustments • Tax planning
|
||||
</p>
|
||||
|
||||
<div className="mb-8">
|
||||
<label className="flex items-center justify-center w-full h-32 px-4 transition bg-white border-2 border-gray-300 border-dashed rounded-lg appearance-none cursor-pointer hover:border-indigo-400 focus:outline-none">
|
||||
<span className="flex items-center space-x-2">
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 />
|
||||
<span className="font-medium text-indigo-600">Loading prices...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Upload />
|
||||
<span className="font-medium text-gray-600">
|
||||
Click to upload CSV file
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
<input
|
||||
type="file"
|
||||
accept=".csv"
|
||||
onChange={handleFileUpload}
|
||||
className="hidden"
|
||||
disabled={loading}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{results && (
|
||||
<div>
|
||||
<div className="bg-gradient-to-r from-indigo-50 to-purple-50 rounded-lg p-6 mb-6">
|
||||
<h2 className="text-2xl font-bold text-gray-800 mb-4">Portfolio Summary</h2>
|
||||
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-semibold text-gray-700 mb-3">All Holdings (Total Portfolio)</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="bg-white rounded-lg p-4 shadow">
|
||||
<div className="text-sm text-gray-600 mb-1">Stocks</div>
|
||||
<div className="text-2xl font-bold text-indigo-600">
|
||||
{results.holdings.length}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg p-4 shadow">
|
||||
<div className="text-sm text-gray-600 mb-1">Total Invested</div>
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
€{results.totalAllInvestedEur.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg p-4 shadow">
|
||||
<div className="text-sm text-gray-600 mb-1">Current Value</div>
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
€{results.totalAllCurrentValueEur.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg p-4 shadow">
|
||||
<div className="text-sm text-gray-600 mb-1">Total Gain/Loss</div>
|
||||
<div className={`text-2xl font-bold ${results.totalAllGainLossEur >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{results.totalAllGainLossEur >= 0 ? '+' : ''}€{results.totalAllGainLossEur.toFixed(2)}
|
||||
</div>
|
||||
<div className={`text-sm font-semibold ${results.totalAllGainLossPercent >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{results.totalAllGainLossPercent >= 0 ? '+' : ''}{results.totalAllGainLossPercent.toFixed(2)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t-2 border-indigo-200 pt-6">
|
||||
<h3 className="text-lg font-semibold text-amber-700 mb-1">
|
||||
🏆 Tax-Eligible Holdings (Bought Before {results.cutoffDate.toLocaleDateString()})
|
||||
</h3>
|
||||
<p className="text-xs text-amber-600 mb-3">Stocks held for more than 2 years</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="bg-amber-50 rounded-lg p-4 shadow border-2 border-amber-200">
|
||||
<div className="text-sm text-amber-700 mb-1">Stocks</div>
|
||||
<div className="text-2xl font-bold text-amber-700">
|
||||
{results.holdings.filter(h => h.oldQuantity > 0).length}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-amber-50 rounded-lg p-4 shadow border-2 border-amber-200">
|
||||
<div className="text-sm text-amber-700 mb-1">Total Invested</div>
|
||||
<div className="text-2xl font-bold text-blue-700">
|
||||
€{results.totalOldInvestedEur.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-amber-50 rounded-lg p-4 shadow border-2 border-amber-200">
|
||||
<div className="text-sm text-amber-700 mb-1">Current Value</div>
|
||||
<div className="text-2xl font-bold text-green-700">
|
||||
€{results.totalOldCurrentValueEur.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-amber-50 rounded-lg p-4 shadow border-2 border-amber-200">
|
||||
<div className="text-sm text-amber-700 mb-1">Total Gain/Loss</div>
|
||||
<div className={`text-2xl font-bold ${results.totalOldGainLossEur >= 0 ? 'text-green-700' : 'text-red-700'}`}>
|
||||
{results.totalOldGainLossEur >= 0 ? '+' : ''}€{results.totalOldGainLossEur.toFixed(2)}
|
||||
</div>
|
||||
<div className={`text-sm font-semibold ${results.totalOldGainLossPercent >= 0 ? 'text-green-700' : 'text-red-700'}`}>
|
||||
{results.totalOldGainLossPercent >= 0 ? '+' : ''}{results.totalOldGainLossPercent.toFixed(2)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-xs text-gray-600">
|
||||
Exchange rate: 1 USD = €{results.exchangeRate.toFixed(4)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="text-xl font-semibold text-gray-800 mb-4">
|
||||
Detailed Stock Breakdown
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
{results.holdings.map(holding => (
|
||||
<div key={holding.ticker} className="bg-white rounded-lg p-6 border-2 border-gray-200 shadow-sm">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold text-indigo-700">{holding.ticker}</h3>
|
||||
{holding.splits.length > 0 && (
|
||||
<div className="text-xs text-green-600 font-semibold mt-1">
|
||||
✓ Split-adjusted ({holding.splits.reduce((acc, s) => acc * s.ratio, 1)}x total)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
{holding.priceAvailable && (
|
||||
<div>
|
||||
<div className="text-xs text-gray-600">Current Price</div>
|
||||
<div className="text-xl font-bold text-gray-900">
|
||||
€{holding.currentPriceEur?.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div className="bg-blue-50 rounded-lg p-4 border border-blue-200">
|
||||
<h4 className="font-bold text-blue-900 mb-3 flex items-center gap-2">
|
||||
📊 Total Holdings
|
||||
<span className="text-xs font-normal text-blue-700">({holding.totalTransactions} purchases)</span>
|
||||
</h4>
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div>
|
||||
<div className="text-blue-700 mb-1">Shares</div>
|
||||
<div className="font-bold text-gray-900">{holding.totalQuantity.toFixed(4)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-blue-700 mb-1">Avg Cost</div>
|
||||
<div className="font-semibold text-gray-800">€{holding.avgPriceEur.toFixed(2)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-blue-700 mb-1">Cost Basis</div>
|
||||
<div className="font-bold text-blue-800">€{holding.totalCostEur.toFixed(2)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-blue-700 mb-1">Current Value</div>
|
||||
<div className="font-bold text-green-700">
|
||||
{holding.priceAvailable ? `€${holding.totalCurrentValueEur.toFixed(2)}` : 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<div className="text-blue-700 mb-1">Gain/Loss</div>
|
||||
<div className={`font-bold text-lg ${holding.totalGainLossEur >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{holding.priceAvailable ? (
|
||||
<>
|
||||
{holding.totalGainLossEur >= 0 ? '+' : ''}€{holding.totalGainLossEur.toFixed(2)}
|
||||
<span className="text-sm ml-2">
|
||||
({holding.totalGainLossPercent >= 0 ? '+' : ''}{holding.totalGainLossPercent.toFixed(1)}%)
|
||||
</span>
|
||||
</>
|
||||
) : 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`rounded-lg p-4 border-2 ${holding.oldQuantity > 0 ? 'bg-amber-50 border-amber-300' : 'bg-gray-50 border-gray-200'}`}>
|
||||
<h4 className={`font-bold mb-3 flex items-center gap-2 ${holding.oldQuantity > 0 ? 'text-amber-900' : 'text-gray-500'}`}>
|
||||
{holding.oldQuantity > 0 ? '🏆 Tax-Eligible (>2 years)' : '⏳ No Old Holdings'}
|
||||
{holding.oldQuantity > 0 && (
|
||||
<span className="text-xs font-normal text-amber-700">
|
||||
({holding.oldTransactions} purchases • {((holding.oldQuantity / holding.totalQuantity) * 100).toFixed(1)}% of total)
|
||||
</span>
|
||||
)}
|
||||
</h4>
|
||||
{holding.oldQuantity > 0 ? (
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div>
|
||||
<div className="text-amber-700 mb-1">Shares</div>
|
||||
<div className="font-bold text-gray-900">{holding.oldQuantity.toFixed(4)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-amber-700 mb-1">Avg Cost</div>
|
||||
<div className="font-semibold text-gray-800">€{holding.oldAvgPriceEur.toFixed(2)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-amber-700 mb-1">Cost Basis</div>
|
||||
<div className="font-bold text-blue-800">€{holding.oldCostEur.toFixed(2)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-amber-700 mb-1">Current Value</div>
|
||||
<div className="font-bold text-green-700">
|
||||
{holding.priceAvailable ? `€${holding.oldCurrentValueEur.toFixed(2)}` : 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<div className="text-amber-700 mb-1">Gain/Loss</div>
|
||||
<div className={`font-bold text-lg ${holding.oldGainLossEur >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{holding.priceAvailable ? (
|
||||
<>
|
||||
{holding.oldGainLossEur >= 0 ? '+' : ''}€{holding.oldGainLossEur.toFixed(2)}
|
||||
<span className="text-sm ml-2">
|
||||
({holding.oldGainLossPercent >= 0 ? '+' : ''}{holding.oldGainLossPercent.toFixed(1)}%)
|
||||
</span>
|
||||
</>
|
||||
) : 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
{holding.oldestDate && (
|
||||
<div className="col-span-2 text-xs text-amber-700 mt-1">
|
||||
Purchased: {holding.oldestDate.toLocaleDateString()} - {holding.newestOldDate.toLocaleDateString()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-gray-500 italic">
|
||||
All shares were bought within the last 2 years
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<StockTaxAnalyzer />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user