Skip to content

Commit 034735b

Browse files
ok, mtn ajoute les investicement, ou un user peut exactement les mêmes c
1 parent 4733ea5 commit 034735b

File tree

3 files changed

+195
-1
lines changed

3 files changed

+195
-1
lines changed

src/app/companies/page.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
'use client';
2+
13
import { getCompaniesForUserDashboard, ManagedCompany, InvestedCompany, OtherCompany } from '@/lib/actions/companies';
24
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
35
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
@@ -7,6 +9,7 @@ import { Button } from '@/components/ui/button';
79
import Link from 'next/link';
810
import { InvestDialog } from '@/components/invest-dialog';
911
import type { CompanyWithDetails } from '@/lib/actions/companies';
12+
import { SellSharesDialog } from '@/components/sell-shares-dialog';
1013

1114

1215
function CompanyTableRow({ company, type }: { company: any, type: 'managed' | 'invested' | 'other' }) {
@@ -37,7 +40,17 @@ function CompanyTableRow({ company, type }: { company: any, type: 'managed' | 'i
3740
<Button asChild variant="outline" size="sm">
3841
<Link href={`/companies/${company.id}`}>Détails</Link>
3942
</Button>
40-
{type === 'other' && (
43+
{type === 'invested' && (
44+
<SellSharesDialog
45+
companyId={company.id}
46+
companyName={company.name}
47+
sharePrice={company.sharePrice}
48+
sharesHeld={company.sharesHeld}
49+
>
50+
<Button size="sm" variant="destructive">Vendre</Button>
51+
</SellSharesDialog>
52+
)}
53+
{type !== 'managed' && (
4154
<InvestDialog company={company as CompanyWithDetails}>
4255
<Button size="sm">Investir</Button>
4356
</InvestDialog>
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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 { useToast } from '@/hooks/use-toast';
12+
import { Loader2 } from 'lucide-react';
13+
import { sellShares } from '@/lib/actions/companies';
14+
import { useRouter } from 'next/navigation';
15+
16+
interface SellSharesDialogProps {
17+
companyId: number;
18+
companyName: string;
19+
sharePrice: number;
20+
sharesHeld: number;
21+
children: React.ReactNode;
22+
}
23+
24+
const formSchema = z.object({
25+
quantity: z.coerce.number().positive({ message: 'La quantité doit être supérieure à zéro.' }),
26+
});
27+
28+
export function SellSharesDialog({ companyId, companyName, sharePrice, sharesHeld, children }: SellSharesDialogProps) {
29+
const [open, setOpen] = useState(false);
30+
const { toast } = useToast();
31+
const router = useRouter();
32+
const form = useForm<z.infer<typeof formSchema>>({
33+
resolver: zodResolver(formSchema),
34+
defaultValues: {
35+
quantity: undefined,
36+
}
37+
});
38+
39+
const quantity = form.watch('quantity') || 0;
40+
const proceeds = quantity * sharePrice;
41+
42+
async function onSubmit(values: z.infer<typeof formSchema>) {
43+
if (values.quantity > sharesHeld) {
44+
form.setError('quantity', { message: `Vous ne pouvez pas vendre plus que vos ${sharesHeld.toFixed(4)} parts.` });
45+
return;
46+
}
47+
const result = await sellShares(companyId, values.quantity);
48+
if (result.error) {
49+
toast({ variant: 'destructive', title: 'Erreur', description: result.error });
50+
} else if (result.success) {
51+
toast({ title: 'Succès', description: result.success });
52+
router.refresh();
53+
setOpen(false);
54+
form.reset();
55+
}
56+
}
57+
58+
return (
59+
<Dialog open={open} onOpenChange={(isOpen) => {
60+
setOpen(isOpen)
61+
if (!isOpen) form.reset();
62+
}}>
63+
<DialogTrigger asChild>{children}</DialogTrigger>
64+
<DialogContent>
65+
<DialogHeader>
66+
<DialogTitle>Vendre des Parts de {companyName}</DialogTitle>
67+
<DialogDescription>
68+
Prix de rachat par part : ${sharePrice.toFixed(2)}. Parts détenues : {sharesHeld.toLocaleString(undefined, {maximumFractionDigits: 4})}
69+
</DialogDescription>
70+
</DialogHeader>
71+
<Form {...form}>
72+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
73+
<FormField
74+
control={form.control}
75+
name="quantity"
76+
render={({ field }) => (
77+
<FormItem>
78+
<FormLabel>Quantité à vendre</FormLabel>
79+
<div className="relative">
80+
<FormControl>
81+
<Input
82+
type="number"
83+
step="any"
84+
placeholder="0.0000"
85+
{...field}
86+
value={field.value ?? ''}
87+
onChange={e => field.onChange(e.target.value === '' ? undefined : e.target.valueAsNumber)} />
88+
</FormControl>
89+
<Button type="button" variant="ghost" size="sm" className="absolute right-1 top-1/2 -translate-y-1/2 h-7" onClick={() => form.setValue('quantity', sharesHeld, { shouldValidate: true })}>
90+
Max
91+
</Button>
92+
</div>
93+
<FormMessage />
94+
</FormItem>
95+
)}
96+
/>
97+
98+
<div className="text-sm text-muted-foreground">
99+
{quantity > 0 ? `Vous recevrez ≈ ${proceeds.toFixed(2)}$ de la trésorerie de l'entreprise.` : 'Entrez une quantité à vendre.'}
100+
</div>
101+
102+
<DialogFooter>
103+
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>Annuler</Button>
104+
<Button type="submit" disabled={form.formState.isSubmitting || quantity > sharesHeld || !form.formState.isValid}>
105+
{form.formState.isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
106+
Vendre pour ${proceeds > 0 ? proceeds.toFixed(2) : '0.00'}
107+
</Button>
108+
</DialogFooter>
109+
</form>
110+
</Form>
111+
</DialogContent>
112+
</Dialog>
113+
)
114+
}

src/lib/actions/companies.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,73 @@ export async function investInCompany(companyId: number, amount: number): Promis
324324
}
325325
}
326326

327+
export async function sellShares(companyId: number, quantity: number): Promise<{ success?: string; error?: string }> {
328+
const session = await getSession();
329+
if (!session?.id) {
330+
return { error: "Vous devez être connecté pour vendre des parts." };
331+
}
332+
if (quantity <= 0) {
333+
return { error: "La quantité doit être positive." };
334+
}
335+
336+
try {
337+
const result = await db.transaction(async (tx) => {
338+
const user = await tx.query.users.findFirst({ where: eq(users.id, session.id), columns: { cash: true } });
339+
if (!user) throw new Error("Utilisateur non trouvé.");
340+
341+
const companyData = await getCompanyById(companyId); // This function calculates current share price
342+
if (!companyData) throw new Error("Entreprise non trouvée.");
343+
344+
const userShareHolding = await tx.query.companyShares.findFirst({
345+
where: and(eq(companyShares.userId, session.id), eq(companyShares.companyId, companyId))
346+
});
347+
348+
const sharesHeld = parseFloat(userShareHolding?.quantity || '0');
349+
if (sharesHeld < quantity) {
350+
throw new Error("Vous ne possédez pas assez de parts pour cette vente.");
351+
}
352+
353+
const proceeds = quantity * companyData.sharePrice;
354+
if (companyData.cash < proceeds) {
355+
throw new Error("La trésorerie de l'entreprise est insuffisante pour racheter ces parts.");
356+
}
357+
358+
// Perform transaction
359+
const newCompanyCash = companyData.cash - proceeds;
360+
const newTotalShares = companyData.totalShares - quantity;
361+
const newUserCash = parseFloat(user.cash) + proceeds;
362+
363+
await tx.update(companies).set({
364+
cash: newCompanyCash.toFixed(2),
365+
totalShares: newTotalShares.toString(),
366+
}).where(eq(companies.id, companyId));
367+
368+
await tx.update(users).set({ cash: newUserCash.toFixed(2) }).where(eq(users.id, session.id));
369+
370+
const newSharesHeld = sharesHeld - quantity;
371+
if (newSharesHeld < 1e-9) { // If selling all shares
372+
await tx.delete(companyShares).where(eq(companyShares.id, userShareHolding!.id));
373+
} else {
374+
await tx.update(companyShares)
375+
.set({ quantity: newSharesHeld.toString() })
376+
.where(eq(companyShares.id, userShareHolding!.id));
377+
}
378+
379+
return { success: `Vous avez vendu ${quantity.toFixed(4)} parts de ${companyData.name} pour ${proceeds.toFixed(2)}$.` };
380+
});
381+
382+
revalidatePath(`/companies`);
383+
revalidatePath(`/companies/${companyId}`);
384+
revalidatePath('/portfolio');
385+
revalidatePath('/profile');
386+
revalidatePath('/');
387+
return result;
388+
389+
} catch (error: any) {
390+
return { error: error.message || "Une erreur est survenue lors de la vente." };
391+
}
392+
}
393+
327394
export async function buyAssetForCompany(companyId: number, ticker: string, quantity: number): Promise<{ success?: string; error?: string }> {
328395
const session = await getSession();
329396
if (!session?.id) return { error: "Vous devez être connecté pour effectuer cette action." };

0 commit comments

Comments
 (0)