Skip to content

Commit b70bc9f

Browse files
heuu par contre quand je regarde le portefeuille, y'a plus le truc qui d
1 parent eea6a61 commit b70bc9f

File tree

5 files changed

+71
-94
lines changed

5 files changed

+71
-94
lines changed

src/components/portfolio-client-page.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,21 @@ import { Button } from '@/components/ui/button';
1212
import { Loader2 } from 'lucide-react';
1313
import type { AssetFromDb } from '@/lib/actions/assets';
1414
import { SellSharesDialog } from './sell-shares-dialog';
15-
import { InvestDialog } from './invest-dialog';
16-
import type { CompanyWithDetails } from '@/lib/actions/companies';
1715

1816
export default function PortfolioClientPage() {
19-
const { holdings, cash, initialCash, loading } = usePortfolio();
17+
const { holdings, cash, initialCash, loading, userProfile } = usePortfolio();
2018
const { getAssetByTicker, assets: marketAssets, loading: marketLoading } = useMarketData();
2119

2220
const holdingsWithMarketData = useMemo(() => {
23-
if (marketLoading && !marketAssets.length) return [];
21+
if (marketLoading && !marketAssets.length && !userProfile) return [];
22+
2423
return holdings.map(holding => {
2524
const isCompany = holding.type === 'Company Share';
2625
const asset = !isCompany ? getAssetByTicker(holding.ticker) : undefined;
2726

28-
const currentPrice = isCompany ? holding.avgCost : (asset?.price || holding.avgCost);
27+
const currentPrice = isCompany
28+
? (holding.company?.sharePrice || holding.avgCost)
29+
: (asset?.price || holding.avgCost);
2930

3031
const currentValue = holding.quantity * currentPrice;
3132
const totalCost = holding.quantity * holding.avgCost;
@@ -41,7 +42,7 @@ export default function PortfolioClientPage() {
4142
pnlPercent
4243
};
4344
}).sort((a, b) => b.currentValue - a.currentValue);
44-
}, [holdings, getAssetByTicker, marketAssets, marketLoading]);
45+
}, [holdings, getAssetByTicker, marketAssets, marketLoading, userProfile]);
4546

4647
const assetsValue = useMemo(() => holdingsWithMarketData.reduce((sum, holding) => sum + holding.currentValue, 0), [holdingsWithMarketData]);
4748
const portfolioValue = assetsValue + cash;
@@ -94,6 +95,7 @@ export default function PortfolioClientPage() {
9495
<TableHead>Quantité</TableHead>
9596
<TableHead>Prix Actuel</TableHead>
9697
<TableHead>Valeur Actuelle</TableHead>
98+
<TableHead>Gains/Pertes</TableHead>
9799
<TableHead className="text-right">Actions</TableHead>
98100
</TableRow>
99101
</TableHeader>
@@ -108,6 +110,10 @@ export default function PortfolioClientPage() {
108110
<TableCell>{holding.quantity.toLocaleString(undefined, { maximumFractionDigits: 8 })}</TableCell>
109111
<TableCell>${holding.currentPrice.toFixed(holding.currentPrice > 10 ? 2 : 4)}</TableCell>
110112
<TableCell>${holding.currentValue.toFixed(2)}</TableCell>
113+
<TableCell className={holding.pnl >= 0 ? 'text-green-500' : 'text-red-500'}>
114+
<div className="font-semibold">{holding.pnl >= 0 ? '+' : '-'}${Math.abs(holding.pnl).toFixed(2)}</div>
115+
<div className="text-xs">({holding.pnlPercent.toFixed(2)}%)</div>
116+
</TableCell>
111117
<TableCell className="text-right space-x-2">
112118
{holding.isCompanyShare ? (
113119
<>
@@ -143,7 +149,7 @@ export default function PortfolioClientPage() {
143149
))
144150
) : (
145151
<TableRow>
146-
<TableCell colSpan={7} className="text-center h-24 text-muted-foreground">
152+
<TableCell colSpan={6} className="text-center h-24 text-muted-foreground">
147153
Vous ne possédez aucun actif pour le moment.
148154
</TableCell>
149155
</TableRow>

src/context/portfolio-context.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ export interface Holding {
1818
avgCost: number;
1919
updatedAt: Date;
2020
isCompanyShare: boolean;
21+
company?: {
22+
id: number;
23+
name: string;
24+
ticker: string;
25+
isListed: boolean;
26+
sharePrice: number;
27+
} | null;
2128
}
2229

2330
export interface Transaction {
@@ -98,7 +105,7 @@ export const PortfolioProvider = ({ children }: { children: ReactNode }) => {
98105
setLoading(true);
99106
const data = await getAuthenticatedUserProfile();
100107
if (data) {
101-
setPortfolioData(data);
108+
setPortfolioData(data as any);
102109
setUnclaimedRewards(data.unclaimedBtc);
103110
} else {
104111
toast({ variant: 'destructive', title: 'Erreur', description: "Impossible de charger les données du portefeuille." });

src/lib/actions/companies.ts

Lines changed: 48 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export async function createCompany(values: z.infer<typeof createCompanySchema>)
7979
companyId: newCompany.id,
8080
userId: session.id,
8181
quantity: '1000.00000000',
82+
avgCost: '1.00',
8283
});
8384

8485
return { success: `L'entreprise "${name}" a été créée avec succès ! ${creationCost.toLocaleString()}$ ont été transférés à la trésorerie.` };
@@ -109,10 +110,16 @@ export async function getCompaniesForUserDashboard() {
109110
orderBy: (companies, { desc }) => [desc(companies.createdAt)],
110111
});
111112

112-
const companiesWithMarketData = allCompanies.map(company => {
113-
const cash = parseFloat(company.cash);
113+
const companiesWithMarketData = await Promise.all(allCompanies.map(async (company) => {
114+
const nav = await getCompanyNAV(company.id, db);
114115
const totalShares = parseFloat(company.totalShares);
115-
const sharePrice = parseFloat(company.sharePrice);
116+
const sharePrice = totalShares > 0 ? nav / totalShares : 0;
117+
118+
// Update share price in DB if it has changed
119+
if (Math.abs(sharePrice - parseFloat(company.sharePrice)) > 1e-9) {
120+
await db.update(companies).set({ sharePrice: sharePrice.toString() }).where(eq(companies.id, company.id));
121+
}
122+
116123
const marketCap = totalShares * sharePrice;
117124

118125
const historicalData: CompanyHistoricalPoint[] = [];
@@ -137,14 +144,15 @@ export async function getCompaniesForUserDashboard() {
137144

138145
return {
139146
...company,
140-
cash: cash,
147+
cash: parseFloat(company.cash),
141148
marketCap: marketCap,
142149
sharePrice: sharePrice,
143150
totalShares: totalShares,
144151
historicalData,
145152
change24h,
146153
}
147-
});
154+
}));
155+
148156

149157
if (!session?.id) {
150158
return {
@@ -171,19 +179,16 @@ export async function getCompaniesForUserDashboard() {
171179
const shareData = sharesByCompanyId.get(company.id);
172180
const sharesHeld = parseFloat(shareData?.quantity || '0');
173181

174-
const isManaged = !!membership;
175-
const isInvested = sharesHeld > 0;
176-
177182
const companyData = {
178183
...company,
179184
sharesHeld: sharesHeld,
180185
sharesValue: sharesHeld * company.sharePrice,
181186
role: membership?.role,
182187
};
183188

184-
if (isManaged) {
189+
if (membership) {
185190
managedCompanies.push(companyData);
186-
} else if (isInvested) {
191+
} else if (sharesHeld > 0) {
187192
investedCompanies.push(companyData);
188193
} else {
189194
otherCompanies.push(companyData);
@@ -265,9 +270,11 @@ export async function getCompanyById(companyId: number) {
265270
}
266271
}
267272

268-
// Calculate market cap based on the stored share price, not NAV
269-
const sharePrice = parseFloat(company.sharePrice);
273+
// Recalculate share price based on NAV
274+
const nav = await getCompanyNAV(companyId, db);
270275
const totalShares = parseFloat(company.totalShares);
276+
const sharePrice = totalShares > 0 ? nav / totalShares : 0;
277+
271278
const marketCap = sharePrice * totalShares;
272279

273280
const miningRigsValue = company.miningRigs.reduce((total, ownedRig) => {
@@ -301,7 +308,7 @@ export async function getCompanyById(companyId: number) {
301308
export type CompanyWithDetails = NonNullable<Awaited<ReturnType<typeof getCompanyById>>>;
302309

303310
// Universal function to calculate a company's Net Asset Value (NAV)
304-
async function getCompanyNAV(companyId: number, tx: any) {
311+
export async function getCompanyNAV(companyId: number, tx: any) {
305312
const company = await tx.query.companies.findFirst({
306313
where: eq(companies.id, companyId),
307314
with: { holdings: true, miningRigs: true }
@@ -373,16 +380,6 @@ export async function buyAssetForCompany(companyId: number, ticker: string, quan
373380
avgCost: asset.price,
374381
});
375382
}
376-
377-
// Recalculate NAV and new share price because asset values can change
378-
const companyData = await tx.query.companies.findFirst({where: eq(companies.id, companyId)});
379-
const nav = await getCompanyNAV(companyId, tx);
380-
const totalShares = parseFloat(companyData!.totalShares);
381-
if (totalShares > 0) {
382-
const newSharePrice = nav / totalShares;
383-
await tx.update(companies).set({ sharePrice: newSharePrice.toString() }).where(eq(companies.id, companyId));
384-
}
385-
386383
return { success: `L'entreprise a acheté ${quantity} de ${ticker}.` };
387384
});
388385

@@ -423,15 +420,6 @@ export async function sellAssetForCompany(companyId: number, holdingId: number,
423420
} else {
424421
await tx.delete(companyHoldings).where(eq(companyHoldings.id, holdingId));
425422
}
426-
427-
const companyData = await tx.query.companies.findFirst({where: eq(companies.id, companyId)});
428-
const nav = await getCompanyNAV(companyId, tx);
429-
const totalShares = parseFloat(companyData!.totalShares);
430-
if (totalShares > 0) {
431-
const newSharePrice = nav / totalShares;
432-
await tx.update(companies).set({ sharePrice: newSharePrice.toString() }).where(eq(companies.id, companyId));
433-
}
434-
435423
return { success: `L'entreprise a vendu ${quantity} de ${asset.ticker}.` };
436424
});
437425

@@ -473,14 +461,6 @@ export async function buyMiningRigForCompany(companyId: number, rigId: string):
473461
await tx.insert(companyMiningRigs).values({ companyId, rigId, quantity: 1 });
474462
}
475463

476-
const companyData = await tx.query.companies.findFirst({where: eq(companies.id, companyId)});
477-
const newNav = await getCompanyNAV(companyId, tx);
478-
const totalShares = parseFloat(companyData!.totalShares);
479-
if (totalShares > 0) {
480-
const newSharePrice = newNav / totalShares;
481-
await tx.update(companies).set({ sharePrice: newSharePrice.toString() }).where(eq(companies.id, companyId));
482-
}
483-
484464
return { success: `L'entreprise a acheté un ${rigToBuy.name}.` };
485465
});
486466

@@ -508,7 +488,11 @@ export async function investInCompany(companyId: number, amount: number): Promis
508488
const companyData = await tx.query.companies.findFirst({ where: eq(companies.id, companyId) });
509489
if (!companyData) throw new Error("Entreprise non trouvée.");
510490

511-
const sharePrice = parseFloat(companyData.sharePrice);
491+
// Recalculate share price based on NAV before transaction
492+
const currentNav = await getCompanyNAV(companyId, tx);
493+
const currentTotalShares = parseFloat(companyData.totalShares);
494+
const sharePrice = currentTotalShares > 0 ? currentNav / currentTotalShares : 1; // Fallback to 1 if no shares
495+
512496
if (sharePrice <= 0) throw new Error("Prix de l'action non valide, impossible d'investir.");
513497

514498
const sharesToBuy = amount / sharePrice;
@@ -520,10 +504,22 @@ export async function investInCompany(companyId: number, amount: number): Promis
520504
});
521505

522506
if (existingShares) {
523-
const newQuantity = parseFloat(existingShares.quantity) + sharesToBuy;
524-
await tx.update(companyShares).set({ quantity: newQuantity.toString() }).where(eq(companyShares.id, existingShares.id));
507+
const oldQuantity = parseFloat(existingShares.quantity);
508+
const oldAvgCost = parseFloat(existingShares.avgCost);
509+
const newTotalQuantity = oldQuantity + sharesToBuy;
510+
const newAvgCost = ((oldAvgCost * oldQuantity) + amount) / newTotalQuantity;
511+
512+
await tx.update(companyShares).set({
513+
quantity: newTotalQuantity.toString(),
514+
avgCost: newAvgCost.toString(),
515+
}).where(eq(companyShares.id, existingShares.id));
525516
} else {
526-
await tx.insert(companyShares).values({ userId: session.id, companyId: companyId, quantity: sharesToBuy.toString() });
517+
await tx.insert(companyShares).values({
518+
userId: session.id,
519+
companyId: companyId,
520+
quantity: sharesToBuy.toString(),
521+
avgCost: sharePrice.toString(),
522+
});
527523
}
528524

529525
// Update company state
@@ -534,11 +530,6 @@ export async function investInCompany(companyId: number, amount: number): Promis
534530
cash: newCompanyCash.toString(),
535531
totalShares: newTotalShares.toString(),
536532
}).where(eq(companies.id, companyId));
537-
538-
// Recalculate NAV and new share price
539-
const newNav = await getCompanyNAV(companyId, tx);
540-
const newSharePrice = newNav / newTotalShares;
541-
await tx.update(companies).set({ sharePrice: newSharePrice.toString() }).where(eq(companies.id, companyId));
542533

543534
await tx.insert(transactions).values({
544535
userId: session.id,
@@ -575,15 +566,17 @@ export async function sellShares(companyId: number, quantity: number): Promise<{
575566
const company = await tx.query.companies.findFirst({ where: eq(companies.id, companyId) });
576567
if (!company) throw new Error("Entreprise non trouvée.");
577568

578-
const sharePrice = parseFloat(company.sharePrice);
579-
const proceeds = sharePrice * quantity;
580-
581569
const userShareHolding = await tx.query.companyShares.findFirst({
582570
where: and(eq(companyShares.userId, session.id), eq(companyShares.companyId, companyId))
583571
});
584572
const sharesHeld = parseFloat(userShareHolding?.quantity || '0');
585573
if (sharesHeld < quantity) throw new Error("Vous ne possédez pas assez de parts.");
586-
574+
575+
const currentNav = await getCompanyNAV(companyId, tx);
576+
const currentTotalShares = parseFloat(company.totalShares);
577+
const sharePrice = currentTotalShares > 0 ? currentNav / currentTotalShares : 0;
578+
const proceeds = sharePrice * quantity;
579+
587580
const companyCash = parseFloat(company.cash);
588581
if(companyCash < proceeds) throw new Error("La trésorerie de l'entreprise est insuffisante pour racheter ces parts.");
589582

@@ -607,11 +600,6 @@ export async function sellShares(companyId: number, quantity: number): Promise<{
607600
totalShares: newTotalShares.toString(),
608601
}).where(eq(companies.id, companyId));
609602

610-
// Recalculate NAV and new share price
611-
const newNav = await getCompanyNAV(companyId, tx);
612-
const newSharePrice = newTotalShares > 0 ? newNav / newTotalShares : 0;
613-
await tx.update(companies).set({ sharePrice: newSharePrice.toString() }).where(eq(companies.id, companyId));
614-
615603
await tx.insert(transactions).values({
616604
userId: session.id,
617605
type: 'Sell',
@@ -655,15 +643,6 @@ export async function addCashToCompany(companyId: number, amount: number): Promi
655643
await tx.update(users).set({ cash: (parseFloat(user.cash) - amount).toString() }).where(eq(users.id, session.id));
656644
await tx.update(companies).set({ cash: sql`${companies.cash} + ${amount}` }).where(eq(companies.id, companyId));
657645

658-
// Recalculate NAV and new share price
659-
const company = await tx.query.companies.findFirst({where: eq(companies.id, companyId)});
660-
const newNav = await getCompanyNAV(companyId, tx);
661-
const totalShares = parseFloat(company!.totalShares);
662-
if (totalShares > 0) {
663-
const newSharePrice = newNav / totalShares;
664-
await tx.update(companies).set({ sharePrice: newSharePrice.toString() }).where(eq(companies.id, companyId));
665-
}
666-
667646
return { success: `${amount.toFixed(2)}$ ajoutés à la trésorerie de l'entreprise.` };
668647
});
669648

@@ -697,16 +676,7 @@ export async function withdrawFromCompanyTreasury(companyId: number, amount: num
697676

698677
await tx.update(companies).set({ cash: sql`${companies.cash} - ${amount}` }).where(eq(companies.id, companyId));
699678
await tx.update(users).set({ cash: (parseFloat(user.cash) + amount).toString() }).where(eq(users.id, session.id));
700-
701-
// Recalculate NAV and new share price
702-
const fullCompany = await tx.query.companies.findFirst({where: eq(companies.id, companyId)});
703-
const newNav = await getCompanyNAV(companyId, tx);
704-
const totalShares = parseFloat(fullCompany!.totalShares);
705-
if (totalShares > 0) {
706-
const newSharePrice = newNav / totalShares;
707-
await tx.update(companies).set({ sharePrice: newSharePrice.toString() }).where(eq(companies.id, companyId));
708-
}
709-
679+
710680
return { success: `${amount.toFixed(2)}$ retirés de la trésorerie de l'entreprise.` };
711681
});
712682

@@ -815,22 +785,15 @@ export async function listCompanyOnMarket(companyId: number): Promise<{ success?
815785
if (!company) throw new Error("Entreprise non trouvée.");
816786
if (company.isListed) throw new Error("L'entreprise est déjà cotée.");
817787

818-
// Recalculate NAV and share price one last time before listing
819-
const nav = await getCompanyNAV(companyId, tx);
820-
const totalShares = parseFloat(company.totalShares);
821-
const sharePrice = totalShares > 0 ? nav / totalShares : 0;
822-
823788
await tx.update(companies).set({
824789
isListed: true,
825-
sharePrice: sharePrice.toString()
826790
}).where(eq(companies.id, companyId));
827791

828792
return { success: 'Entreprise mise en bourse avec succès !' };
829793
});
830794

831-
revalidatePath('/trading');
795+
revalidatePath(`/companies`);
832796
revalidatePath(`/companies/${companyId}`);
833-
revalidatePath('/companies');
834797

835798
return result;
836799
} catch (error: any) {

src/lib/actions/portfolio.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export async function getAuthenticatedUserProfile() {
114114
type: 'Company Share',
115115
isCompanyShare: true,
116116
quantity: parseFloat(cs.quantity),
117-
avgCost: sharePrice, // Using current price as avgCost for simplicity here
117+
avgCost: parseFloat(cs.avgCost),
118118
updatedAt: new Date(cs.company.createdAt),
119119
company: {
120120
...cs.company,

src/lib/db/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ export const companyShares = pgTable('company_shares', {
236236
companyId: integer('company_id').notNull().references(() => companies.id, { onDelete: 'cascade' }),
237237
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
238238
quantity: numeric('quantity', { precision: 20, scale: 8 }).notNull(),
239+
avgCost: numeric('avg_cost', { precision: 20, scale: 8 }).default('0').notNull(),
239240
}, (table) => {
240241
return {
241242
companyUserSharesIdx: uniqueIndex('company_user_shares_idx').on(table.companyId, table.userId),

0 commit comments

Comments
 (0)