Latest fix
This commit is contained in:
934
static/index.html
Normal file
934
static/index.html
Normal file
@ -0,0 +1,934 @@
|
||||
<!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>
|
||||
);
|
||||
|
||||
// Format number with thousand separators
|
||||
const formatCurrency = (value) => {
|
||||
if (value === null || value === undefined || isNaN(value))
|
||||
return "0.00";
|
||||
return value.toLocaleString("en-US", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
};
|
||||
|
||||
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: [],
|
||||
VWCE: [], // Vanguard FTSE All-World UCITS ETF - no splits
|
||||
};
|
||||
|
||||
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">
|
||||
€
|
||||
{formatCurrency(
|
||||
results.totalAllInvestedEur,
|
||||
)}
|
||||
</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">
|
||||
€
|
||||
{formatCurrency(
|
||||
results.totalAllCurrentValueEur,
|
||||
)}
|
||||
</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
|
||||
? "+"
|
||||
: ""}
|
||||
€
|
||||
{formatCurrency(
|
||||
results.totalAllGainLossEur,
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`text-sm font-semibold ${results.totalAllGainLossPercent >= 0 ? "text-green-600" : "text-red-600"}`}
|
||||
>
|
||||
{results.totalAllGainLossPercent >=
|
||||
0
|
||||
? "+"
|
||||
: ""}
|
||||
{(
|
||||
results.totalAllGainLossPercent ||
|
||||
0
|
||||
).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">
|
||||
€
|
||||
{formatCurrency(
|
||||
results.totalOldInvestedEur,
|
||||
)}
|
||||
</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">
|
||||
€
|
||||
{formatCurrency(
|
||||
results.totalOldCurrentValueEur,
|
||||
)}
|
||||
</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
|
||||
? "+"
|
||||
: ""}
|
||||
€
|
||||
{formatCurrency(
|
||||
results.totalOldGainLossEur,
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`text-sm font-semibold ${results.totalOldGainLossPercent >= 0 ? "text-green-700" : "text-red-700"}`}
|
||||
>
|
||||
{results.totalOldGainLossPercent >=
|
||||
0
|
||||
? "+"
|
||||
: ""}
|
||||
{(
|
||||
results.totalOldGainLossPercent ||
|
||||
0
|
||||
).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">
|
||||
€
|
||||
{formatCurrency(
|
||||
holding.currentPriceEur,
|
||||
)}
|
||||
</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 ||
|
||||
0
|
||||
).toFixed(
|
||||
4,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-blue-700 mb-1">
|
||||
Avg Cost
|
||||
</div>
|
||||
<div className="font-semibold text-gray-800">
|
||||
€
|
||||
{formatCurrency(
|
||||
holding.avgPriceEur,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-blue-700 mb-1">
|
||||
Cost
|
||||
Basis
|
||||
</div>
|
||||
<div className="font-bold text-blue-800">
|
||||
€
|
||||
{formatCurrency(
|
||||
holding.totalCostEur,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-blue-700 mb-1">
|
||||
Current
|
||||
Value
|
||||
</div>
|
||||
<div className="font-bold text-green-700">
|
||||
{holding.priceAvailable
|
||||
? `€${formatCurrency(holding.totalCurrentValueEur)}`
|
||||
: "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
|
||||
? "+"
|
||||
: ""}
|
||||
€
|
||||
{formatCurrency(
|
||||
holding.totalGainLossEur,
|
||||
)}
|
||||
<span className="text-sm ml-2">
|
||||
(
|
||||
{holding.totalGainLossPercent >=
|
||||
0
|
||||
? "+"
|
||||
: ""}
|
||||
{(
|
||||
holding.totalGainLossPercent ||
|
||||
0
|
||||
).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 ||
|
||||
0
|
||||
).toFixed(
|
||||
4,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-amber-700 mb-1">
|
||||
Avg
|
||||
Cost
|
||||
</div>
|
||||
<div className="font-semibold text-gray-800">
|
||||
€
|
||||
{formatCurrency(
|
||||
holding.oldAvgPriceEur,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-amber-700 mb-1">
|
||||
Cost
|
||||
Basis
|
||||
</div>
|
||||
<div className="font-bold text-blue-800">
|
||||
€
|
||||
{formatCurrency(
|
||||
holding.oldCostEur,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-amber-700 mb-1">
|
||||
Current
|
||||
Value
|
||||
</div>
|
||||
<div className="font-bold text-green-700">
|
||||
{holding.priceAvailable
|
||||
? `€${formatCurrency(holding.oldCurrentValueEur)}`
|
||||
: "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
|
||||
? "+"
|
||||
: ""}
|
||||
€
|
||||
{formatCurrency(
|
||||
holding.oldGainLossEur,
|
||||
)}
|
||||
<span className="text-sm ml-2">
|
||||
(
|
||||
{holding.oldGainLossPercent >=
|
||||
0
|
||||
? "+"
|
||||
: ""}
|
||||
{(
|
||||
holding.oldGainLossPercent ||
|
||||
0
|
||||
).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