Skip to content

Commit eea6a61

Browse files
ok, donc ce qui fait bougée les prix d'action d'une entreprise :
quand
1 parent 2bb583d commit eea6a61

File tree

4 files changed

+177
-76
lines changed

4 files changed

+177
-76
lines changed

src/components/companies-client-page.tsx

Lines changed: 117 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
12
'use client';
23

3-
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
4+
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@/components/ui/card";
45
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
56
import { Badge } from "@/components/ui/badge";
67
import { CreateCompanyDialog } from '@/components/create-company-dialog';
@@ -9,7 +10,8 @@ import Link from 'next/link';
910
import { InvestDialog } from '@/components/invest-dialog';
1011
import type { CompanyWithDetails, ManagedCompany, InvestedCompany, OtherCompany } from '@/lib/actions/companies';
1112
import { SellSharesDialog } from '@/components/sell-shares-dialog';
12-
13+
import { Area, AreaChart, Tooltip } from 'recharts';
14+
import { ChartContainer, ChartTooltipContent } from "@/components/ui/chart";
1315

1416
function CompanyTableRow({ company, type }: { company: any, type: 'managed' | 'invested' | 'other' }) {
1517
const hasShares = company.sharesHeld > 0;
@@ -42,41 +44,10 @@ function CompanyTableRow({ company, type }: { company: any, type: 'managed' | 'i
4244
<Button asChild variant="outline" size="sm">
4345
<Link href={`/companies/${company.id}`}>Détails</Link>
4446
</Button>
45-
46-
{company.isListed ? (
47-
<>
48-
<InvestDialog company={company as CompanyWithDetails} isListed>
49-
<Button size="sm">Acheter</Button>
50-
</InvestDialog>
51-
{hasShares && (
52-
<SellSharesDialog
53-
companyId={company.id}
54-
companyName={company.name}
55-
sharePrice={company.sharePrice}
56-
sharesHeld={company.sharesHeld}
57-
isListed
58-
>
59-
<Button size="sm" variant="destructive">Vendre</Button>
60-
</SellSharesDialog>
61-
)}
62-
</>
63-
) : ( // Not listed
64-
<>
65-
{hasShares && (
66-
<SellSharesDialog
67-
companyId={company.id}
68-
companyName={company.name}
69-
sharePrice={company.sharePrice}
70-
sharesHeld={company.sharesHeld}
71-
>
72-
<Button size="sm" variant="destructive">Vendre</Button>
73-
</SellSharesDialog>
74-
)}
75-
<InvestDialog company={company as CompanyWithDetails}>
76-
<Button size="sm">Investir</Button>
77-
</InvestDialog>
78-
</>
79-
)}
47+
{/* Note: Investing in private companies from this table */}
48+
<InvestDialog company={company as CompanyWithDetails}>
49+
<Button size="sm">Investir</Button>
50+
</InvestDialog>
8051
</TableCell>
8152
</TableRow>
8253
);
@@ -86,8 +57,8 @@ function CompanyTable({ title, description, companies, type }: { title: string,
8657
const headers = {
8758
managed: ["Entreprise", "Mon Rôle", "Mes Parts", "Valeur des Parts", ""],
8859
invested: ["Entreprise", "Parts Détenues", "Valeur des Parts", ""],
89-
other: ["Entreprise", "Trésorerie", "Cap. Boursière", ""]
90-
}
60+
other: ["Entreprise", "Trésorerie", "Cap. Boursière", ""],
61+
};
9162

9263
return (
9364
<Card>
@@ -110,15 +81,95 @@ function CompanyTable({ title, description, companies, type }: { title: string,
11081
) : (
11182
<TableRow>
11283
<TableCell colSpan={headers[type].length} className="h-24 text-center text-muted-foreground">
113-
{type === 'managed' ? "Vous ne gérez aucune entreprise." : type === 'invested' ? "Vous n'avez investi dans aucune entreprise." : "Aucune autre entreprise disponible."}
84+
{type === 'managed' ? "Vous ne gérez aucune entreprise." : type === 'invested' ? "Vous n'avez investi dans aucune entreprise." : "Aucune entreprise privée disponible."}
11485
</TableCell>
11586
</TableRow>
11687
)}
11788
</TableBody>
11889
</Table>
11990
</CardContent>
12091
</Card>
121-
)
92+
);
93+
}
94+
95+
96+
function StockExchangeCard({ company }: { company: OtherCompany }) {
97+
const changeIsPositive = company.change24h.startsWith('+');
98+
const chartConfig = {
99+
price: {
100+
label: 'Prix',
101+
color: changeIsPositive ? 'hsl(var(--chart-1))' : 'hsl(var(--destructive))',
102+
},
103+
};
104+
105+
return (
106+
<Card className="flex flex-col">
107+
<CardHeader>
108+
<div className="flex items-start justify-between">
109+
<div>
110+
<CardTitle className="text-base">{company.name} ({company.ticker})</CardTitle>
111+
<CardDescription>{company.industry}</CardDescription>
112+
</div>
113+
<Badge variant="secondary">En Bourse</Badge>
114+
</div>
115+
</CardHeader>
116+
<CardContent className="flex-grow space-y-4">
117+
<div className="h-[100px] w-full -translate-x-4">
118+
<ChartContainer config={chartConfig}>
119+
<AreaChart
120+
accessibilityLayer
121+
data={company.historicalData}
122+
margin={{ top: 5, right: 10, left: 10, bottom: 0 }}
123+
>
124+
<defs>
125+
<linearGradient id={`fill-${company.ticker}`} x1="0" y1="0" x2="0" y2="1">
126+
<stop offset="5%" stopColor="var(--color-price)" stopOpacity={0.8}/>
127+
<stop offset="95%" stopColor="var(--color-price)" stopOpacity={0.1}/>
128+
</linearGradient>
129+
</defs>
130+
<Tooltip
131+
cursor={false}
132+
content={<ChartTooltipContent indicator="dot" hideLabel />}
133+
/>
134+
<Area
135+
dataKey="price"
136+
type="natural"
137+
fill={`url(#fill-${company.ticker})`}
138+
strokeWidth={2}
139+
stroke="var(--color-price)"
140+
stackId="a"
141+
/>
142+
</AreaChart>
143+
</ChartContainer>
144+
</div>
145+
<div>
146+
<div className="text-xl font-bold">${company.sharePrice.toFixed(4)}</div>
147+
<p className={`text-xs ${changeIsPositive ? 'text-green-500' : 'text-red-500'}`}>
148+
{company.change24h} (24h)
149+
</p>
150+
</div>
151+
</CardContent>
152+
<CardFooter className="flex justify-end gap-2">
153+
<Button asChild variant="outline" size="sm">
154+
<Link href={`/companies/${company.id}`}>Détails</Link>
155+
</Button>
156+
<InvestDialog company={company as any} isListed>
157+
<Button size="sm">Acheter</Button>
158+
</InvestDialog>
159+
{company.sharesHeld > 0 && (
160+
<SellSharesDialog
161+
companyId={company.id}
162+
companyName={company.name}
163+
sharePrice={company.sharePrice}
164+
sharesHeld={company.sharesHeld}
165+
isListed
166+
>
167+
<Button size="sm" variant="secondary">Vendre</Button>
168+
</SellSharesDialog>
169+
)}
170+
</CardFooter>
171+
</Card>
172+
);
122173
}
123174

124175
interface CompaniesClientPageProps {
@@ -128,7 +179,9 @@ interface CompaniesClientPageProps {
128179
}
129180

130181
export function CompaniesClientPage({ managedCompanies, investedCompanies, otherCompanies }: CompaniesClientPageProps) {
131-
182+
const listedCompanies = otherCompanies.filter(c => c.isListed);
183+
const privateCompanies = otherCompanies.filter(c => !c.isListed);
184+
132185
return (
133186
<div className="space-y-6">
134187
<div className="flex items-center justify-between">
@@ -157,12 +210,28 @@ export function CompaniesClientPage({ managedCompanies, investedCompanies, other
157210
/>
158211
)}
159212

160-
<CompanyTable
161-
title="Bourse des Entreprises"
162-
description="Toutes les entreprises disponibles à l'investissement ou au trading."
163-
companies={otherCompanies}
164-
type="other"
165-
/>
213+
<Card>
214+
<CardHeader>
215+
<CardTitle>Bourse des Entreprises</CardTitle>
216+
<CardDescription>Entreprises cotées disponibles pour le trading public.</CardDescription>
217+
</CardHeader>
218+
<CardContent className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
219+
{listedCompanies.length > 0 ? (
220+
listedCompanies.map((company) => <StockExchangeCard key={company.id} company={company} />)
221+
) : (
222+
<p className="col-span-full py-12 text-center text-muted-foreground">Aucune entreprise n'est actuellement cotée en bourse.</p>
223+
)}
224+
</CardContent>
225+
</Card>
226+
227+
{privateCompanies.length > 0 && (
228+
<CompanyTable
229+
title="Autres Entreprises Privées"
230+
description="Entreprises non cotées dans lesquelles vous pouvez réaliser un investissement initial."
231+
companies={privateCompanies}
232+
type="other"
233+
/>
234+
)}
166235
</div>
167236
);
168237
}

src/components/portfolio-client-page.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
'use client';
23

34
import { useMemo } from 'react';
@@ -24,11 +25,10 @@ export default function PortfolioClientPage() {
2425
const isCompany = holding.type === 'Company Share';
2526
const asset = !isCompany ? getAssetByTicker(holding.ticker) : undefined;
2627

27-
// For company shares, the price is stored directly in the holding from the portfolio context
2828
const currentPrice = isCompany ? holding.avgCost : (asset?.price || holding.avgCost);
2929

3030
const currentValue = holding.quantity * currentPrice;
31-
const totalCost = holding.quantity * holding.avgCost; // This might be less accurate for company shares over time, but good for a start
31+
const totalCost = holding.quantity * holding.avgCost;
3232
const pnl = currentValue - totalCost;
3333
const pnlPercent = totalCost > 0 ? (pnl / totalCost) * 100 : 0;
3434
return {
@@ -100,7 +100,7 @@ export default function PortfolioClientPage() {
100100
<TableBody>
101101
{holdingsWithMarketData.length > 0 ? (
102102
holdingsWithMarketData.map(holding => (
103-
<TableRow key={holding.ticker}>
103+
<TableRow key={`${holding.ticker}-${holding.isCompanyShare}`}>
104104
<TableCell>
105105
<div className="font-medium">{holding.name}</div>
106106
<div className="text-sm text-muted-foreground">{holding.ticker}</div>
@@ -119,7 +119,7 @@ export default function PortfolioClientPage() {
119119
companyName={holding.name}
120120
sharePrice={holding.currentPrice}
121121
sharesHeld={holding.quantity}
122-
isListed={true}
122+
isListed={holding.company?.isListed}
123123
>
124124
<Button variant="secondary" size="sm">Vendre</Button>
125125
</SellSharesDialog>

src/lib/actions/companies.ts

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,18 @@ export async function createCompany(values: z.infer<typeof createCompanySchema>)
9090
revalidatePath('/');
9191
return result;
9292
} catch (error: any) {
93-
if (error?.code === '23505') { // Handles unique constraint violations for both name and ticker
93+
if (error?.code === '23505') {
9494
return { error: "Une entreprise avec un nom ou un ticker similaire existe déjà." };
9595
}
9696
return { error: error.message || "Une erreur est survenue lors de la création de l'entreprise." };
9797
}
9898
}
9999

100+
export type CompanyHistoricalPoint = {
101+
date: string;
102+
price: number;
103+
};
104+
100105
export async function getCompaniesForUserDashboard() {
101106
const session = await getSession();
102107

@@ -110,12 +115,34 @@ export async function getCompaniesForUserDashboard() {
110115
const sharePrice = parseFloat(company.sharePrice);
111116
const marketCap = totalShares * sharePrice;
112117

118+
const historicalData: CompanyHistoricalPoint[] = [];
119+
const now = new Date();
120+
const yesterday = new Date(now);
121+
yesterday.setDate(yesterday.getDate() - 1);
122+
123+
let lastPrice = sharePrice / (1 + (Math.random() - 0.45) * 0.1);
124+
125+
for (let i = 0; i < 24; i++) {
126+
const date = new Date(yesterday.getTime() + i * 60 * 60 * 1000);
127+
lastPrice *= (1 + (Math.random() - 0.5) * 0.05);
128+
if (lastPrice <= 0) lastPrice = 0.0001;
129+
historicalData.push({ date: date.toISOString(), price: lastPrice });
130+
}
131+
historicalData.push({ date: now.toISOString(), price: sharePrice });
132+
133+
const startPrice = historicalData[0]?.price || sharePrice;
134+
const change = sharePrice - startPrice;
135+
const changePercent = startPrice > 0 ? (change / startPrice) * 100 : 0;
136+
const change24h = `${changePercent >= 0 ? '+' : ''}${changePercent.toFixed(2)}%`;
137+
113138
return {
114139
...company,
115140
cash: cash,
116141
marketCap: marketCap,
117142
sharePrice: sharePrice,
118143
totalShares: totalShares,
144+
historicalData,
145+
change24h,
119146
}
120147
});
121148

@@ -136,35 +163,33 @@ export async function getCompaniesForUserDashboard() {
136163
const membershipsByCompanyId = new Map(userMemberships.map(m => [m.companyId, m]));
137164

138165
const managedCompanies: any[] = [];
139-
let investedCompanies: any[] = [];
166+
const investedCompanies: any[] = [];
140167
const otherCompanies: any[] = [];
141168

142169
for (const company of companiesWithMarketData) {
143170
const membership = membershipsByCompanyId.get(company.id);
144171
const shareData = sharesByCompanyId.get(company.id);
145172
const sharesHeld = parseFloat(shareData?.quantity || '0');
146173

147-
if (membership) {
148-
managedCompanies.push({
149-
...company,
150-
role: membership.role,
151-
sharesHeld: sharesHeld,
152-
sharesValue: sharesHeld * company.sharePrice,
153-
});
154-
} else if (shareData) {
155-
investedCompanies.push({
156-
...company,
157-
sharesHeld: sharesHeld,
158-
sharesValue: sharesHeld * company.sharePrice,
159-
});
174+
const isManaged = !!membership;
175+
const isInvested = sharesHeld > 0;
176+
177+
const companyData = {
178+
...company,
179+
sharesHeld: sharesHeld,
180+
sharesValue: sharesHeld * company.sharePrice,
181+
role: membership?.role,
182+
};
183+
184+
if (isManaged) {
185+
managedCompanies.push(companyData);
186+
} else if (isInvested) {
187+
investedCompanies.push(companyData);
160188
} else {
161-
otherCompanies.push(company);
189+
otherCompanies.push(companyData);
162190
}
163191
}
164192

165-
const managedCompanyIds = new Set(managedCompanies.map(c => c.id));
166-
investedCompanies = investedCompanies.filter(c => !managedCompanyIds.has(c.id));
167-
168193
return { managedCompanies, investedCompanies, otherCompanies };
169194
}
170195

@@ -870,5 +895,3 @@ export async function claimCompanyBtc(companyId: number): Promise<{ success?: st
870895
return { error: error.message || "Une erreur est survenue lors de la réclamation des récompenses." };
871896
}
872897
}
873-
874-

0 commit comments

Comments
 (0)