Skip to content

Commit 36dc85a

Browse files
continue le dev les entreprise, et améliore la page principale des entre
1 parent aee7301 commit 36dc85a

File tree

5 files changed

+186
-36
lines changed

5 files changed

+186
-36
lines changed

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Avatar, AvatarFallback } from '@/components/ui/avatar';
1010
import { InvestDialog } from '@/components/invest-dialog';
1111
import { getSession } from '@/lib/session';
1212
import { ManageCompanyAssetsDialog } from '@/components/manage-company-assets-dialog';
13+
import { AddCompanyCashDialog } from '@/components/add-company-cash-dialog';
1314

1415

1516
function getInitials(name: string) {
@@ -44,9 +45,16 @@ export default async function CompanyDetailPage({ params }: { params: { companyI
4445
<h1 className="text-2xl font-bold tracking-tight">{company.name}</h1>
4546
<p className="text-muted-foreground">{company.description}</p>
4647
</div>
47-
<InvestDialog company={company}>
48-
<Button>Investir</Button>
49-
</InvestDialog>
48+
<div className="flex items-center gap-2">
49+
{isCEO && (
50+
<AddCompanyCashDialog companyId={company.id}>
51+
<Button variant="outline">Ajouter des fonds</Button>
52+
</AddCompanyCashDialog>
53+
)}
54+
<InvestDialog company={company}>
55+
<Button>Investir</Button>
56+
</InvestDialog>
57+
</div>
5058
</div>
5159

5260
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
'use client';
2+
3+
import { useState } from 'react';
4+
import { useForm } from 'react-hook-form';
5+
import { zodResolver } from '@hookform/resolvers/zod';
6+
import { z } from 'zod';
7+
import { Button } from '@/components/ui/button';
8+
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
9+
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
10+
import { Input } from '@/components/ui/input';
11+
import { usePortfolio } from '@/context/portfolio-context';
12+
import { useToast } from '@/hooks/use-toast';
13+
import { Loader2 } from 'lucide-react';
14+
import { addCashToCompany } from '@/lib/actions/companies';
15+
16+
interface AddCompanyCashDialogProps {
17+
companyId: number;
18+
children: React.ReactNode;
19+
}
20+
21+
const formSchema = z.object({
22+
amount: z.coerce.number().positive({ message: 'Le montant doit être supérieur à zéro.' }),
23+
});
24+
25+
export function AddCompanyCashDialog({ companyId, children }: AddCompanyCashDialogProps) {
26+
const [open, setOpen] = useState(false);
27+
const { cash } = usePortfolio();
28+
const { toast } = useToast();
29+
const form = useForm<z.infer<typeof formSchema>>({
30+
resolver: zodResolver(formSchema),
31+
defaultValues: {
32+
amount: undefined,
33+
}
34+
});
35+
36+
const amount = form.watch('amount') || 0;
37+
38+
async function onSubmit(values: z.infer<typeof formSchema>) {
39+
const result = await addCashToCompany(companyId, values.amount);
40+
if (result.error) {
41+
toast({ variant: 'destructive', title: 'Erreur', description: result.error });
42+
} else if (result.success) {
43+
toast({ title: 'Succès', description: result.success });
44+
setOpen(false);
45+
form.reset();
46+
}
47+
}
48+
49+
return (
50+
<Dialog open={open} onOpenChange={(isOpen) => {
51+
setOpen(isOpen)
52+
if (!isOpen) form.reset();
53+
}}>
54+
<DialogTrigger asChild>{children}</DialogTrigger>
55+
<DialogContent>
56+
<DialogHeader>
57+
<DialogTitle>Ajouter des fonds à la Trésorerie</DialogTitle>
58+
<DialogDescription>
59+
Transférez des fonds de votre solde personnel vers la trésorerie de l'entreprise.
60+
Fonds disponibles : ${cash.toFixed(2)}
61+
</DialogDescription>
62+
</DialogHeader>
63+
<Form {...form}>
64+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
65+
<FormField
66+
control={form.control}
67+
name="amount"
68+
render={({ field }) => (
69+
<FormItem>
70+
<FormLabel>Montant à ajouter</FormLabel>
71+
<div className="relative">
72+
<FormControl>
73+
<Input
74+
type="number"
75+
step="0.01"
76+
placeholder="0.00"
77+
{...field}
78+
value={field.value ?? ''}
79+
onChange={e => field.onChange(e.target.value === '' ? undefined : e.target.valueAsNumber)} />
80+
</FormControl>
81+
<Button type="button" variant="ghost" size="sm" className="absolute right-1 top-1/2 -translate-y-1/2 h-7" onClick={() => form.setValue('amount', cash, { shouldValidate: true })}>
82+
Max
83+
</Button>
84+
</div>
85+
<FormMessage />
86+
</FormItem>
87+
)}
88+
/>
89+
90+
<DialogFooter>
91+
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>Annuler</Button>
92+
<Button type="submit" disabled={form.formState.isSubmitting || amount > cash || !form.formState.isValid}>
93+
{form.formState.isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
94+
Ajouter ${amount > 0 ? amount.toFixed(2) : '0.00'}
95+
</Button>
96+
</DialogFooter>
97+
</form>
98+
</Form>
99+
</DialogContent>
100+
</Dialog>
101+
)
102+
}

src/components/create-company-dialog.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { Textarea } from '@/components/ui/textarea';
2828
import { useToast } from '@/hooks/use-toast';
2929
import { createCompany } from '@/lib/actions/companies';
3030
import { Loader2 } from 'lucide-react';
31+
import { usePortfolio } from '@/context/portfolio-context';
3132

3233
const companyFormSchema = z.object({
3334
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."),
@@ -42,6 +43,8 @@ interface CreateCompanyDialogProps {
4243
export function CreateCompanyDialog({ onCompanyCreated }: CreateCompanyDialogProps) {
4344
const [open, setOpen] = useState(false);
4445
const { toast } = useToast();
46+
const { cash } = usePortfolio();
47+
const creationCost = 1000;
4548

4649
const form = useForm<z.infer<typeof companyFormSchema>>({
4750
resolver: zodResolver(companyFormSchema),
@@ -77,7 +80,8 @@ export function CreateCompanyDialog({ onCompanyCreated }: CreateCompanyDialogPro
7780
<DialogHeader>
7881
<DialogTitle>Lancer une Nouvelle Entreprise</DialogTitle>
7982
<DialogDescription>
80-
Créez votre propre entreprise virtuelle. Gérez des actifs et distribuez des dividendes à vos actionnaires.
83+
La création d'une entreprise coûte {creationCost.toLocaleString()}$. Cette somme constituera sa trésorerie initiale.
84+
Vous serez nommé PDG.
8185
</DialogDescription>
8286
</DialogHeader>
8387
<Form {...form}>
@@ -125,9 +129,9 @@ export function CreateCompanyDialog({ onCompanyCreated }: CreateCompanyDialogPro
125129

126130
<DialogFooter>
127131
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>Annuler</Button>
128-
<Button type="submit" disabled={form.formState.isSubmitting}>
132+
<Button type="submit" disabled={form.formState.isSubmitting || cash < creationCost}>
129133
{form.formState.isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
130-
Créer l'entreprise
134+
Créer l'entreprise (${creationCost.toLocaleString()})
131135
</Button>
132136
</DialogFooter>
133137
</form>

src/lib/actions/companies.ts

Lines changed: 64 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export async function createCompany(values: z.infer<typeof createCompanySchema>)
1919
if (!session?.id) {
2020
return { error: "Vous devez être connecté pour créer une entreprise." };
2121
}
22+
23+
const creationCost = 1000;
2224

2325
const validatedFields = createCompanySchema.safeParse(values);
2426
if (!validatedFields.success) {
@@ -27,13 +29,26 @@ export async function createCompany(values: z.infer<typeof createCompanySchema>)
2729
const { name, industry, description } = validatedFields.data;
2830

2931
try {
30-
await db.transaction(async (tx) => {
31-
// Create the company
32+
const result = await db.transaction(async (tx) => {
33+
const user = await tx.query.users.findFirst({ where: eq(users.id, session.id), columns: { cash: true } });
34+
if (!user) throw new Error("Utilisateur non trouvé.");
35+
36+
const userCash = parseFloat(user.cash);
37+
if (userCash < creationCost) throw new Error(`Fonds insuffisants. La création d'une entreprise coûte ${creationCost.toLocaleString()}$.`);
38+
39+
// Deduct cost from user
40+
const newUserCash = userCash - creationCost;
41+
await tx.update(users).set({ cash: newUserCash.toFixed(2) }).where(eq(users.id, session.id));
42+
43+
// Create the company with initial treasury
3244
const [newCompany] = await tx.insert(companies).values({
3345
name,
3446
industry,
3547
description,
3648
creatorId: session.id,
49+
cash: creationCost.toFixed(2),
50+
totalShares: '10000.00000000',
51+
sharePrice: '0.10'
3752
}).returning();
3853

3954
// Add the creator as the CEO
@@ -42,17 +57,20 @@ export async function createCompany(values: z.infer<typeof createCompanySchema>)
4257
userId: session.id,
4358
role: 'ceo',
4459
});
60+
61+
return { success: `L'entreprise "${name}" a été créée avec succès ! ${creationCost.toLocaleString()}$ ont été transférés à la trésorerie.` };
4562
});
4663

4764
revalidatePath('/companies');
48-
return { success: `L'entreprise "${name}" a été créée avec succès !` };
65+
revalidatePath('/portfolio');
66+
return result;
4967
} catch (error: any) {
5068
// Check for unique constraint violation
5169
if (error?.code === '23505') {
5270
return { error: "Une entreprise avec ce nom existe déjà." };
5371
}
5472
console.error("Error creating company:", error);
55-
return { error: "Une erreur est survenue lors de la création de l'entreprise." };
73+
return { error: error.message || "Une erreur est survenue lors de la création de l'entreprise." };
5674
}
5775
}
5876

@@ -128,7 +146,7 @@ export async function getCompanyById(companyId: number) {
128146
const companyCash = parseFloat(company.cash);
129147
const companyValue = companyCash + portfolioValue;
130148
const totalShares = parseFloat(company.totalShares);
131-
const sharePrice = totalShares > 0 ? companyValue / totalShares : 0;
149+
const sharePrice = totalShares > 0 ? companyValue / totalShares : parseFloat(company.sharePrice);
132150

133151
return {
134152
...company,
@@ -165,33 +183,17 @@ export async function investInCompany(companyId: number, amount: number): Promis
165183
if (!user) throw new Error("Utilisateur non trouvé.");
166184
if (parseFloat(user.cash) < amount) throw new Error("Fonds insuffisants.");
167185

168-
const company = await tx.query.companies.findFirst({
169-
where: eq(companies.id, companyId),
170-
with: { holdings: true }
171-
});
172-
if (!company) throw new Error("Entreprise non trouvée.");
173-
174-
const allAssets = await tx.query.assets.findMany();
175-
const priceMap = allAssets.reduce((map, asset) => {
176-
map[asset.ticker] = parseFloat(asset.price);
177-
return map;
178-
}, {} as Record<string, number>);
179-
180-
const portfolioValue = company.holdings.reduce((sum, holding) => {
181-
const currentPrice = priceMap[holding.ticker] || parseFloat(holding.avgCost);
182-
return sum + (parseFloat(holding.quantity) * currentPrice);
183-
}, 0);
184-
185-
const companyValue = parseFloat(company.cash) + portfolioValue;
186-
const totalShares = parseFloat(company.totalShares);
187-
const preInvestmentSharePrice = totalShares > 0 ? companyValue / totalShares : parseFloat(company.sharePrice);
186+
const companyData = await getCompanyById(companyId);
187+
if (!companyData) throw new Error("Entreprise non trouvée.");
188+
189+
const preInvestmentSharePrice = companyData.sharePrice;
188190

189191
if (amount <= 0) throw new Error("Le montant de l'investissement doit être positif.");
190192
if (preInvestmentSharePrice <= 0) throw new Error("Le prix de l'action est nul, l'investissement est impossible.");
191193

192194
const sharesToBuy = amount / preInvestmentSharePrice;
193-
const newCompanyCash = parseFloat(company.cash) + amount;
194-
const newTotalShares = totalShares + sharesToBuy;
195+
const newCompanyCash = companyData.cash + amount;
196+
const newTotalShares = companyData.totalShares + sharesToBuy;
195197

196198
await tx.update(users).set({ cash: (parseFloat(user.cash) - amount).toFixed(2) }).where(eq(users.id, session.id));
197199
await tx.update(companies).set({
@@ -216,7 +218,7 @@ export async function investInCompany(companyId: number, amount: number): Promis
216218
});
217219
}
218220

219-
return { success: `Vous avez investi ${amount.toFixed(2)}$ dans ${company.name} !` };
221+
return { success: `Vous avez investi ${amount.toFixed(2)}$ dans ${companyData.name} !` };
220222
});
221223

222224
revalidatePath(`/companies/${companyId}`);
@@ -343,3 +345,37 @@ export async function sellAssetForCompany(companyId: number, holdingId: number,
343345
return { error: error.message || "Une erreur est survenue lors de la vente." };
344346
}
345347
}
348+
349+
350+
export async function addCashToCompany(companyId: number, amount: number): Promise<{ success?: string; error?: string }> {
351+
const session = await getSession();
352+
if (!session?.id) return { error: "Vous devez être connecté." };
353+
if (amount <= 0) return { error: "Le montant doit être positif." };
354+
355+
try {
356+
const result = await db.transaction(async (tx) => {
357+
const member = await tx.query.companyMembers.findFirst({ where: and(eq(companyMembers.companyId, companyId), eq(companyMembers.userId, session.id)) });
358+
if (!member || member.role !== 'ceo') throw new Error("Seul le PDG peut ajouter des fonds à la trésorerie.");
359+
360+
const user = await tx.query.users.findFirst({ where: eq(users.id, session.id), columns: { cash: true } });
361+
if (!user) throw new Error("Utilisateur non trouvé.");
362+
if (parseFloat(user.cash) < amount) throw new Error("Fonds personnels insuffisants.");
363+
364+
const company = await tx.query.companies.findFirst({ where: eq(companies.id, companyId), columns: { cash: true } });
365+
if (!company) throw new Error("Entreprise non trouvée.");
366+
367+
await tx.update(users).set({ cash: (parseFloat(user.cash) - amount).toFixed(2) }).where(eq(users.id, session.id));
368+
await tx.update(companies).set({ cash: (parseFloat(company.cash) + amount).toFixed(2) }).where(eq(companies.id, companyId));
369+
370+
return { success: `${amount.toFixed(2)}$ ajoutés à la trésorerie de l'entreprise.` };
371+
});
372+
373+
revalidatePath(`/companies/${companyId}`);
374+
revalidatePath('/portfolio');
375+
revalidatePath('/profile');
376+
return result;
377+
378+
} catch (error: any) {
379+
return { error: error.message || "Une erreur est survenue." };
380+
}
381+
}

src/lib/db/schema.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,8 @@ export const companies = pgTable('companies', {
187187
description: text('description').notNull(),
188188
cash: numeric('cash', { precision: 15, scale: 2 }).default('0.00').notNull(),
189189
creatorId: integer('creator_id').notNull().references(() => users.id, { onDelete: 'restrict' }),
190-
sharePrice: numeric('share_price', { precision: 10, scale: 2 }).default('10.00').notNull(),
191-
totalShares: numeric('total_shares', { precision: 20, scale: 2 }).default('1000000').notNull(),
190+
sharePrice: numeric('share_price', { precision: 10, scale: 2 }).default('0.10').notNull(),
191+
totalShares: numeric('total_shares', { precision: 20, scale: 8 }).default('10000.00').notNull(),
192192
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
193193
});
194194

0 commit comments

Comments
 (0)