Skip to content

Commit 21ddb48

Browse files
et ajoute le truc pour que les owner d'entreprise puisse a jour leur ent
1 parent f6e8406 commit 21ddb48

File tree

6 files changed

+168
-13
lines changed

6 files changed

+168
-13
lines changed

src/app/companies/[companyId]/page.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { notFound } from 'next/navigation';
33
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
44
import { Badge } from '@/components/ui/badge';
55
import { Button } from '@/components/ui/button';
6-
import { ArrowLeft, Landmark, Users, DollarSign, LineChart, Briefcase, Percent, Package, Cpu, Settings, Server, Bitcoin } from 'lucide-react';
6+
import { ArrowLeft, Landmark, Users, DollarSign, LineChart, Briefcase, Percent, Package, Cpu, Settings, Server, Bitcoin, TrendingUp } from 'lucide-react';
77
import Link from 'next/link';
88
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
99
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
@@ -14,6 +14,7 @@ import { AddCompanyCashDialog } from '@/components/add-company-cash-dialog';
1414
import { getRigById } from '@/lib/mining';
1515
import { ManageMembersDialog } from '@/components/manage-members-dialog';
1616
import { WithdrawCompanyCashDialog } from '@/components/withdraw-company-cash-dialog';
17+
import { ListCompanyButton } from '@/components/list-company-button';
1718

1819

1920
function getInitials(name: string) {
@@ -56,7 +57,14 @@ export default async function CompanyDetailPage({ params }: { params: { companyI
5657
</Link>
5758
</Button>
5859
<div className="flex-1">
59-
<h1 className="text-2xl font-bold tracking-tight">{company.name}</h1>
60+
<div className="flex items-center gap-3">
61+
<h1 className="text-2xl font-bold tracking-tight">{company.name} ({company.ticker})</h1>
62+
{company.isListed ? (
63+
<Badge variant="secondary">En Bourse</Badge>
64+
) : (
65+
<Badge variant="outline">Non Cotée</Badge>
66+
)}
67+
</div>
6068
<p className="text-muted-foreground">{company.description}</p>
6169
</div>
6270
<div className="flex items-center gap-2">
@@ -68,6 +76,7 @@ export default async function CompanyDetailPage({ params }: { params: { companyI
6876
<WithdrawCompanyCashDialog companyId={company.id} companyCash={company.cash}>
6977
<Button variant="outline">Retirer des fonds</Button>
7078
</WithdrawCompanyCashDialog>
79+
{!company.isListed && <ListCompanyButton companyId={company.id} />}
7180
</>
7281
)}
7382
<InvestDialog company={company}>
@@ -93,7 +102,7 @@ export default async function CompanyDetailPage({ params }: { params: { companyI
93102
<LineChart className="h-4 w-4 text-muted-foreground" />
94103
</CardHeader>
95104
<CardContent>
96-
<div className="text-2xl font-bold">${company.sharePrice.toFixed(2)}</div>
105+
<div className="text-2xl font-bold">${company.sharePrice.toFixed(4)}</div>
97106
<p className="text-xs text-muted-foreground">Prix par part de l'entreprise</p>
98107
</CardContent>
99108
</Card>
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
'use client';
2+
import { Button } from './ui/button';
3+
import { listCompanyOnMarket } from '@/lib/actions/companies';
4+
import { useToast } from '@/hooks/use-toast';
5+
import { useRouter } from 'next/navigation';
6+
import { useState } from 'react';
7+
import { Loader2, TrendingUp } from 'lucide-react';
8+
import {
9+
AlertDialog,
10+
AlertDialogAction,
11+
AlertDialogCancel,
12+
AlertDialogContent,
13+
AlertDialogDescription,
14+
AlertDialogFooter,
15+
AlertDialogHeader,
16+
AlertDialogTitle,
17+
AlertDialogTrigger,
18+
} from "@/components/ui/alert-dialog"
19+
20+
export function ListCompanyButton({ companyId }: { companyId: number }) {
21+
const { toast } = useToast();
22+
const router = useRouter();
23+
const [isLoading, setIsLoading] = useState(false);
24+
25+
const handleList = async () => {
26+
setIsLoading(true);
27+
const result = await listCompanyOnMarket(companyId);
28+
if (result.error) {
29+
toast({ variant: 'destructive', title: 'Erreur', description: result.error });
30+
} else {
31+
toast({ title: 'Succès !', description: result.success });
32+
router.refresh();
33+
}
34+
setIsLoading(false);
35+
}
36+
37+
return (
38+
<AlertDialog>
39+
<AlertDialogTrigger asChild>
40+
<Button variant="outline">
41+
<TrendingUp className="mr-2 h-4 w-4" />
42+
Mettre en Bourse
43+
</Button>
44+
</AlertDialogTrigger>
45+
<AlertDialogContent>
46+
<AlertDialogHeader>
47+
<AlertDialogTitle>Mettre l'entreprise en bourse ?</AlertDialogTitle>
48+
<AlertDialogDescription>
49+
Cette action est irréversible. Une fois cotée, les actions de votre entreprise apparaîtront dans la Salle des Marchés et pourront être échangées par tous les joueurs. Le prix de l'action sera déterminé par la valeur totale de l'entreprise (trésorerie + actifs).
50+
</AlertDialogDescription>
51+
</AlertDialogHeader>
52+
<AlertDialogFooter>
53+
<AlertDialogCancel>Annuler</AlertDialogCancel>
54+
<AlertDialogAction onClick={handleList} disabled={isLoading}>
55+
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
56+
Confirmer la Cotation
57+
</AlertDialogAction>
58+
</AlertDialogFooter>
59+
</AlertDialogContent>
60+
</AlertDialog>
61+
)
62+
}

src/context/market-data-context.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ export const MarketDataProvider = ({ children }: { children: ReactNode }) => {
8080

8181
for (const ticker in newAssets) {
8282
const asset = { ...newAssets[ticker] };
83+
84+
// Prevent simulation for company shares as their price is calculated
85+
if (asset.type === 'Company Share') {
86+
continue;
87+
}
88+
8389
const initialPrice = initialAssets[ticker]?.price;
8490
if (!initialPrice) continue;
8591

src/lib/actions/assets.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use server';
22

33
import { db } from '@/lib/db';
4-
import { assets as assetsSchema } from '@/lib/db/schema';
4+
import { assets as assetsSchema, companies as companiesSchema } from '@/lib/db/schema';
55
import { assets as initialAssets } from '@/lib/assets';
66
import { eq } from 'drizzle-orm';
77
import { revalidatePath } from 'next/cache';
@@ -21,7 +21,40 @@ export async function getAssets() {
2121
return seededAssets.map(a => ({...a, price: parseFloat(a.price)}));
2222
}
2323

24-
return assetsInDb.map(a => ({...a, price: parseFloat(a.price)}));
24+
const baseAssets = assetsInDb.map(a => ({...a, price: parseFloat(a.price)}));
25+
26+
const listedCompanies = await db.query.companies.findMany({
27+
where: eq(companiesSchema.isListed, true),
28+
});
29+
30+
const companyAssets = listedCompanies.map(c => {
31+
const sharePrice = parseFloat(c.sharePrice);
32+
const totalShares = parseFloat(c.totalShares);
33+
const marketCap = sharePrice * totalShares;
34+
35+
let marketCapString = `$${marketCap.toLocaleString(undefined, {maximumFractionDigits: 0})}`;
36+
if (marketCap >= 1e12) {
37+
marketCapString = `$${(marketCap / 1e12).toFixed(2)}T`;
38+
} else if (marketCap >= 1e9) {
39+
marketCapString = `$${(marketCap / 1e9).toFixed(2)}B`;
40+
} else if (marketCap >= 1e6) {
41+
marketCapString = `$${(marketCap / 1e6).toFixed(2)}M`;
42+
}
43+
44+
45+
return {
46+
ticker: c.ticker,
47+
name: c.name,
48+
description: c.description,
49+
type: 'Company Share',
50+
price: sharePrice,
51+
change24h: '+0.00%', // Placeholder, as company value change is not tracked over 24h yet
52+
marketCap: marketCapString,
53+
}
54+
});
55+
56+
return [...baseAssets, ...companyAssets];
57+
2558
} catch (error) {
2659
console.error("Error getting assets:", error);
2760
return [];
@@ -46,7 +79,7 @@ export async function updatePriceFromTrade(ticker: string, tradeValue: number) {
4679
where: eq(assetsSchema.ticker, ticker),
4780
});
4881

49-
if (!asset) return;
82+
if (!asset || asset.type === 'Company Share') return;
5083

5184
const marketCap = parseMarketCap(asset.marketCap);
5285
if (marketCap === 0) return;

src/lib/actions/companies.ts

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import { updatePriceFromTrade } from './assets';
1010
import { getRigById } from '@/lib/mining';
1111

1212
const createCompanySchema = z.object({
13-
name: z.string().min(3, "Le nom doit faire au moins 3 caractères.").max(50),
14-
industry: z.string().min(3, "L'industrie doit faire au moins 3 caractères.").max(50),
15-
description: z.string().min(10, "La description doit faire au moins 10 caractères.").max(200),
13+
name: z.string().min(3, "Le nom doit faire au moins 3 caractères.").max(50, "Le nom ne doit pas dépasser 50 caractères."),
14+
industry: z.string().min(3, "L'industrie doit faire au moins 3 caractères.").max(50, "L'industrie ne doit pas dépasser 50 caractères."),
15+
description: z.string().min(10, "La description doit faire au moins 10 caractères.").max(200, "La description ne doit pas dépasser 200 caractères."),
1616
});
1717

1818
export async function createCompany(values: z.infer<typeof createCompanySchema>): Promise<{ success?: string; error?: string }> {
@@ -28,6 +28,7 @@ export async function createCompany(values: z.infer<typeof createCompanySchema>)
2828
return { error: "Données invalides." };
2929
}
3030
const { name, industry, description } = validatedFields.data;
31+
const ticker = name.replace(/[^a-zA-Z0-9]/g, '').substring(0, 4).toUpperCase();
3132

3233
try {
3334
const result = await db.transaction(async (tx) => {
@@ -37,6 +38,11 @@ export async function createCompany(values: z.infer<typeof createCompanySchema>)
3738
const userCash = parseFloat(user.cash);
3839
if (userCash < creationCost) throw new Error(`Fonds insuffisants. La création d'une entreprise coûte ${creationCost.toLocaleString()}$.`);
3940

41+
const existingTicker = await tx.query.companies.findFirst({ where: eq(companies.ticker, ticker) });
42+
if (existingTicker) {
43+
throw new Error("Une entreprise avec un ticker similaire existe déjà. Veuillez choisir un nom légèrement différent.");
44+
}
45+
4046
// Deduct cost from user
4147
const newUserCash = userCash - creationCost;
4248
await tx.update(users).set({ cash: newUserCash.toFixed(2) }).where(eq(users.id, session.id));
@@ -49,7 +55,8 @@ export async function createCompany(values: z.infer<typeof createCompanySchema>)
4955
creatorId: session.id,
5056
cash: creationCost.toFixed(2),
5157
totalShares: '1000.00000000',
52-
sharePrice: '1.00'
58+
sharePrice: '1.00',
59+
ticker: ticker,
5360
}).returning();
5461

5562
// Add the creator as the CEO
@@ -68,8 +75,8 @@ export async function createCompany(values: z.infer<typeof createCompanySchema>)
6875
revalidatePath('/');
6976
return result;
7077
} catch (error: any) {
71-
// Check for unique constraint violation
72-
if (error?.code === '23505') {
78+
// Check for unique constraint violation on name
79+
if (error?.code === '23505' && error.constraint === 'companies_name_key') {
7380
return { error: "Une entreprise avec ce nom existe déjà." };
7481
}
7582
console.error("Error creating company:", error);
@@ -237,6 +244,14 @@ export async function getCompanyById(companyId: number) {
237244
const totalShares = parseFloat(company.totalShares);
238245
const sharePrice = totalShares > 0 ? companyValue / totalShares : parseFloat(company.sharePrice);
239246

247+
// Update the stored share price to reflect the latest calculation
248+
if (Math.abs(sharePrice - parseFloat(company.sharePrice)) > 0.00001) { // Only update if there's a meaningful change
249+
await db.update(companies)
250+
.set({ sharePrice: sharePrice.toString() })
251+
.where(eq(companies.id, company.id));
252+
}
253+
254+
240255
return {
241256
...company,
242257
cash: companyCash,
@@ -701,3 +716,30 @@ export async function removeMemberFromCompany(companyId: number, memberIdToRemov
701716
return { error: error.message || "Une erreur est survenue." };
702717
}
703718
}
719+
720+
721+
export async function listCompanyOnMarket(companyId: number): Promise<{ success?: string; error?: string }> {
722+
const session = await getSession();
723+
if (!session?.id) {
724+
return { error: 'Non autorisé.' };
725+
}
726+
727+
try {
728+
const member = await db.query.companyMembers.findFirst({
729+
where: and(eq(companyMembers.companyId, companyId), eq(companyMembers.userId, session.id)),
730+
});
731+
732+
if (!member || member.role !== 'ceo') {
733+
return { error: 'Seul le PDG peut mettre une entreprise en bourse.' };
734+
}
735+
736+
await db.update(companies).set({ isListed: true }).where(eq(companies.id, companyId));
737+
738+
revalidatePath('/trading');
739+
revalidatePath(`/companies/${companyId}`);
740+
741+
return { success: 'Entreprise mise en bourse avec succès !' };
742+
} catch (error: any) {
743+
return { error: error.message || "Une erreur est survenue." };
744+
}
745+
}

src/lib/db/schema.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
integer,
99
uniqueIndex,
1010
index,
11+
boolean,
1112
} from 'drizzle-orm/pg-core';
1213
import { relations, desc } from 'drizzle-orm';
1314

@@ -183,12 +184,14 @@ export const userMiningRigsRelations = relations(userMiningRigs, ({ one }) => ({
183184
export const companies = pgTable('companies', {
184185
id: serial('id').primaryKey(),
185186
name: varchar('name', { length: 256 }).notNull().unique(),
187+
ticker: varchar('ticker', { length: 10 }).notNull().unique(),
186188
industry: varchar('industry', { length: 100 }).notNull(),
187189
description: text('description').notNull(),
188190
cash: numeric('cash', { precision: 15, scale: 2 }).default('0.00').notNull(),
189191
creatorId: integer('creator_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
190-
sharePrice: numeric('share_price', { precision: 10, scale: 2 }).default('1.00').notNull(),
192+
sharePrice: numeric('share_price', { precision: 20, scale: 8 }).default('1.00').notNull(),
191193
totalShares: numeric('total_shares', { precision: 20, scale: 8 }).default('1000.00').notNull(),
194+
isListed: boolean('is_listed').default(false).notNull(),
192195
unclaimedBtc: numeric('unclaimed_btc', { precision: 18, scale: 8 }).default('0').notNull(),
193196
lastMiningUpdateAt: timestamp('last_mining_update_at', { withTimezone: true }).defaultNow().notNull(),
194197
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),

0 commit comments

Comments
 (0)