Skip to content

Commit e5b1e81

Browse files
authored
Merge pull request #37 from GiuseppeDM98/feature/cost-basis-tracking
Add cost basis and tax tracking for assets
2 parents ffaa0ec + 5c08605 commit e5b1e81

File tree

6 files changed

+272
-4
lines changed

6 files changed

+272
-4
lines changed

README.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ Built with Next.js, Firebase, and TypeScript. Designed to replace spreadsheet-ba
5555
- **Asset allocation tracking** with target vs current comparison and rebalancing recommendations
5656
- **Composite assets** for pension funds and mixed-allocation investments
5757
- **Liquidity tracking** to separate liquid and illiquid net worth
58+
- **Cost basis tracking** with unrealized gains and tax estimation
59+
- Track average cost per share/unit for each asset
60+
- Automatic calculation of unrealized gains (current value - cost basis)
61+
- Estimated tax calculation based on configurable tax rates
62+
- Gross and net net worth visualization (before/after taxes)
5863

5964
### 📈 **Historical Analysis**
6065
- **Automated monthly snapshots** via scheduled cron jobs
@@ -336,6 +341,21 @@ See [Infrastructure Alternatives](./SETUP.md#infrastructure-alternatives) for mi
336341
5. Compare current vs planned scenarios
337342
6. Track progress month-over-month with historical chart
338343

344+
### Tracking Cost Basis and Taxes
345+
346+
1. Navigate to **"Patrimonio"** (Assets)
347+
2. Click on an existing asset or create a new one
348+
3. Enable **"Tracciamento Cost Basis"** toggle
349+
4. Enter:
350+
- **Costo Medio per Azione**: €85.50 (your average purchase price)
351+
- **Aliquota Fiscale**: 26 (tax rate percentage)
352+
5. Save the asset
353+
6. View on Dashboard:
354+
- **Patrimonio Totale Lordo/Netto**: Gross and net total worth
355+
- **Patrimonio Liquido Lordo/Netto**: Gross and net liquid worth
356+
- **Plusvalenze Non Realizzate**: Unrealized gains (green) or losses (red)
357+
- **Tasse Stimate**: Estimated taxes on gains
358+
339359
---
340360

341361
## 🤝 Contributing
@@ -428,12 +448,12 @@ See the [LICENSE](./LICENSE) file for the full license text.
428448
- ✅ FIRE calculator and progress tracker
429449
- ✅ Hall of Fame personal financial rankings
430450
- ✅ Registration control system
451+
- ✅ Cost basis tracking with unrealized gains and tax estimation
431452

432453
### Future Enhancements (Planned 🔜)
433454
- 🔜 PDF export of portfolio reports
434455
- 🔜 Email notifications (monthly summary)
435456
- 🔜 Multi-currency full conversion support
436-
- 🔜 Cost basis tracking (average cost per share)
437457
- 🔜 Performance metrics (ROI, IRR, CAGR, Sharpe ratio)
438458
- 🔜 Internationalization (i18n) for multi-language support
439459

app/dashboard/history/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ export default function HistoryPage() {
313313
<div>
314314
<h1 className="text-3xl font-bold text-gray-900">Storico</h1>
315315
<p className="mt-2 text-gray-600">
316-
Analizza l'evoluzione del tuo patrimonio nel tempo
316+
Analizza l'evoluzione del tuo patrimonio (lordo) nel tempo
317317
</p>
318318
</div>
319319
<div className="flex gap-2">

app/dashboard/page.tsx

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import {
88
calculateTotalValue,
99
calculateLiquidNetWorth,
1010
calculateIlliquidNetWorth,
11+
calculateTotalUnrealizedGains,
12+
calculateTotalEstimatedTaxes,
13+
calculateLiquidEstimatedTaxes,
14+
calculateGrossTotal,
15+
calculateNetTotal,
1116
} from '@/lib/services/assetService';
1217
import {
1318
formatCurrency,
@@ -213,6 +218,17 @@ export default function DashboardPage() {
213218
const illiquidNetWorth = calculateIlliquidNetWorth(assets);
214219
const assetCount = assets.length;
215220

221+
// Cost basis tracking calculations
222+
const unrealizedGains = calculateTotalUnrealizedGains(assets);
223+
const estimatedTaxes = calculateTotalEstimatedTaxes(assets);
224+
const liquidEstimatedTaxes = calculateLiquidEstimatedTaxes(assets);
225+
const grossTotal = calculateGrossTotal(assets);
226+
const netTotal = calculateNetTotal(assets);
227+
const liquidNetTotal = liquidNetWorth - liquidEstimatedTaxes;
228+
229+
// Check if any asset has cost basis tracking enabled
230+
const hasCostBasisTracking = assets.some(a => (a.averageCost && a.averageCost > 0) || (a.taxRate && a.taxRate > 0));
231+
216232
// Prepare chart data
217233
const assetClassData = prepareAssetClassDistributionData(assets);
218234
const assetData = prepareAssetDistributionData(assets);
@@ -263,7 +279,7 @@ export default function DashboardPage() {
263279
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
264280
<Card>
265281
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
266-
<CardTitle className="text-sm font-medium">Patrimonio Totale</CardTitle>
282+
<CardTitle className="text-sm font-medium">Patrimonio Totale Lordo</CardTitle>
267283
<DollarSign className="h-4 w-4 text-muted-foreground" />
268284
</CardHeader>
269285
<CardContent>
@@ -276,7 +292,7 @@ export default function DashboardPage() {
276292

277293
<Card>
278294
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
279-
<CardTitle className="text-sm font-medium">Patrimonio Liquido</CardTitle>
295+
<CardTitle className="text-sm font-medium">Patrimonio Liquido Lordo</CardTitle>
280296
<Wallet className="h-4 w-4 text-muted-foreground" />
281297
</CardHeader>
282298
<CardContent>
@@ -298,6 +314,79 @@ export default function DashboardPage() {
298314
</Card>
299315
</div>
300316

317+
{/* Cost Basis Cards - only show if any asset has cost basis tracking */}
318+
{hasCostBasisTracking && (
319+
<>
320+
{/* Net Worth Cards */}
321+
<div className="grid gap-6 md:grid-cols-2">
322+
<Card>
323+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
324+
<CardTitle className="text-sm font-medium">Patrimonio Totale Netto</CardTitle>
325+
<DollarSign className="h-4 w-4 text-muted-foreground" />
326+
</CardHeader>
327+
<CardContent>
328+
<div className="text-2xl font-bold text-blue-600">
329+
{formatCurrency(netTotal)}
330+
</div>
331+
<p className="text-xs text-muted-foreground">
332+
Dopo tasse stimate
333+
</p>
334+
</CardContent>
335+
</Card>
336+
337+
<Card>
338+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
339+
<CardTitle className="text-sm font-medium">Patrimonio Liquido Netto</CardTitle>
340+
<Wallet className="h-4 w-4 text-muted-foreground" />
341+
</CardHeader>
342+
<CardContent>
343+
<div className="text-2xl font-bold text-blue-600">
344+
{formatCurrency(liquidNetTotal)}
345+
</div>
346+
<p className="text-xs text-muted-foreground">
347+
Liquidità dopo tasse stimate
348+
</p>
349+
</CardContent>
350+
</Card>
351+
</div>
352+
353+
{/* Gains and Taxes Cards */}
354+
<div className="grid gap-6 md:grid-cols-2">
355+
<Card>
356+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
357+
<CardTitle className="text-sm font-medium">Plusvalenze Non Realizzate</CardTitle>
358+
<TrendingUp className="h-4 w-4 text-muted-foreground" />
359+
</CardHeader>
360+
<CardContent>
361+
<div className={`text-2xl font-bold ${
362+
unrealizedGains >= 0 ? 'text-green-600' : 'text-red-600'
363+
}`}>
364+
{unrealizedGains >= 0 ? '+' : ''}{formatCurrency(unrealizedGains)}
365+
</div>
366+
<p className="text-xs text-muted-foreground">
367+
Guadagno/perdita rispetto al costo medio
368+
</p>
369+
</CardContent>
370+
</Card>
371+
372+
<Card>
373+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
374+
<CardTitle className="text-sm font-medium">Tasse Stimate</CardTitle>
375+
<Receipt className="h-4 w-4 text-muted-foreground" />
376+
</CardHeader>
377+
<CardContent>
378+
<div className="text-2xl font-bold text-orange-600">
379+
{formatCurrency(estimatedTaxes)}
380+
</div>
381+
<p className="text-xs text-muted-foreground">
382+
Imposte su plusvalenze non realizzate
383+
</p>
384+
</CardContent>
385+
</Card>
386+
</div>
387+
</>
388+
)}
389+
301390
{/* Variazioni Cards */}
302391
<div className="grid gap-6 md:grid-cols-2">
303392
<Card>

components/assets/AssetDialog.tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ const assetSchema = z.object({
5454
currency: z.string().min(1, 'Valuta è obbligatoria'),
5555
quantity: z.number().positive('Quantità deve essere positiva'),
5656
manualPrice: z.number().positive('Il prezzo deve essere positivo').optional().or(z.nan()),
57+
averageCost: z.number().positive('Il costo medio deve essere positivo').optional().or(z.nan()),
58+
taxRate: z.number().min(0, 'L\'aliquota fiscale deve essere almeno 0').max(100, 'L\'aliquota fiscale deve essere massimo 100').optional().or(z.nan()),
5759
isLiquid: z.boolean().optional(),
5860
autoUpdatePrice: z.boolean().optional(),
5961
isComposite: z.boolean().optional(),
@@ -98,6 +100,7 @@ export function AssetDialog({ open, onClose, asset }: AssetDialogProps) {
98100
const [composition, setComposition] = useState<AssetComposition[]>([]);
99101
const [isComposite, setIsComposite] = useState(false);
100102
const [hasOutstandingDebt, setHasOutstandingDebt] = useState(false);
103+
const [showCostBasis, setShowCostBasis] = useState(false);
101104
const {
102105
register,
103106
handleSubmit,
@@ -190,6 +193,8 @@ export function AssetDialog({ open, onClose, asset }: AssetDialogProps) {
190193
currency: asset.currency,
191194
quantity: asset.quantity,
192195
manualPrice: asset.currentPrice > 0 ? asset.currentPrice : undefined,
196+
averageCost: asset.averageCost || undefined,
197+
taxRate: asset.taxRate || undefined,
193198
isLiquid: defaultIsLiquid,
194199
autoUpdatePrice: asset.autoUpdatePrice !== undefined ? asset.autoUpdatePrice : shouldUpdatePrice(asset.type, asset.subCategory),
195200
isComposite: !!(asset.composition && asset.composition.length > 0),
@@ -206,6 +211,9 @@ export function AssetDialog({ open, onClose, asset }: AssetDialogProps) {
206211

207212
// Set hasOutstandingDebt state based on asset data
208213
setHasOutstandingDebt(!!(asset.outstandingDebt && asset.outstandingDebt > 0));
214+
215+
// Set showCostBasis state based on asset data
216+
setShowCostBasis(!!((asset.averageCost && asset.averageCost > 0) || (asset.taxRate && asset.taxRate > 0)));
209217
} else {
210218
reset({
211219
ticker: '',
@@ -216,6 +224,8 @@ export function AssetDialog({ open, onClose, asset }: AssetDialogProps) {
216224
currency: 'EUR',
217225
quantity: 0,
218226
manualPrice: undefined,
227+
averageCost: undefined,
228+
taxRate: undefined,
219229
isLiquid: true,
220230
autoUpdatePrice: true,
221231
isComposite: false,
@@ -224,6 +234,7 @@ export function AssetDialog({ open, onClose, asset }: AssetDialogProps) {
224234
setComposition([]);
225235
setIsComposite(false);
226236
setHasOutstandingDebt(false);
237+
setShowCostBasis(false);
227238
}
228239
}, [asset, reset]);
229240

@@ -371,6 +382,8 @@ export function AssetDialog({ open, onClose, asset }: AssetDialogProps) {
371382
subCategory: data.subCategory || undefined,
372383
currency: data.currency,
373384
quantity: data.quantity,
385+
averageCost: data.averageCost && !isNaN(data.averageCost) && data.averageCost > 0 ? data.averageCost : undefined,
386+
taxRate: data.taxRate && !isNaN(data.taxRate) && data.taxRate >= 0 ? data.taxRate : undefined,
374387
currentPrice,
375388
isLiquid: data.isLiquid,
376389
autoUpdatePrice: data.autoUpdatePrice,
@@ -778,6 +791,71 @@ export function AssetDialog({ open, onClose, asset }: AssetDialogProps) {
778791
</div>
779792
)}
780793

794+
{/* Cost Basis Tracking */}
795+
<div className="space-y-2 rounded-lg border p-4">
796+
<div className="flex items-center justify-between">
797+
<div className="space-y-0.5">
798+
<Label htmlFor="showCostBasis">Tracciamento Cost Basis</Label>
799+
<p className="text-xs text-gray-500">
800+
Abilita il calcolo di plusvalenze non realizzate e tasse stimate
801+
</p>
802+
</div>
803+
<Switch
804+
id="showCostBasis"
805+
checked={showCostBasis}
806+
onCheckedChange={(checked) => {
807+
setShowCostBasis(checked);
808+
if (!checked) {
809+
setValue('averageCost', undefined);
810+
setValue('taxRate', undefined);
811+
}
812+
}}
813+
/>
814+
</div>
815+
816+
{showCostBasis && (
817+
<div className="mt-4 space-y-4">
818+
<div className="grid grid-cols-2 gap-4">
819+
<div className="space-y-2">
820+
<Label htmlFor="averageCost">Costo Medio per Azione ({watch('currency')})</Label>
821+
<Input
822+
id="averageCost"
823+
type="number"
824+
step="0.01"
825+
min="0"
826+
{...register('averageCost', { valueAsNumber: true })}
827+
placeholder="es. 85.50"
828+
/>
829+
{errors.averageCost && (
830+
<p className="text-sm text-red-500">{errors.averageCost.message}</p>
831+
)}
832+
<p className="text-xs text-gray-500">
833+
Il costo medio di acquisto per singola azione/unità
834+
</p>
835+
</div>
836+
<div className="space-y-2">
837+
<Label htmlFor="taxRate">Aliquota Fiscale (%)</Label>
838+
<Input
839+
id="taxRate"
840+
type="number"
841+
step="0.01"
842+
min="0"
843+
max="100"
844+
{...register('taxRate', { valueAsNumber: true })}
845+
placeholder="es. 26"
846+
/>
847+
{errors.taxRate && (
848+
<p className="text-sm text-red-500">{errors.taxRate.message}</p>
849+
)}
850+
<p className="text-xs text-gray-500">
851+
Percentuale di tassazione sulle plusvalenze (es. 26 per 26%)
852+
</p>
853+
</div>
854+
</div>
855+
</div>
856+
)}
857+
</div>
858+
781859
{shouldUpdatePrice(selectedType, selectedSubCategory) && (
782860
<div className="space-y-2">
783861
<Label htmlFor="manualPrice">Prezzo Manuale (opzionale)</Label>

0 commit comments

Comments
 (0)