Skip to content

Commit 4733ea5

Browse files
ajoute dans admin un truc pour ajouter de la crypto a un user
fait que
1 parent f86c4fd commit 4733ea5

File tree

5 files changed

+292
-6
lines changed

5 files changed

+292
-6
lines changed

src/app/admin/page.tsx

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { useState } from 'react';
44
import { Button } from '@/components/ui/button';
55
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
66
import { useToast } from '@/hooks/use-toast';
7-
import { resetAiNews, resetAllCompanies, resetAllUsers } from '@/lib/actions/admin';
8-
import { Loader2, Trash2 } from 'lucide-react';
7+
import { resetAiNews, resetAllCompanies, resetAllUsers, addCryptoToUserByEmail } from '@/lib/actions/admin';
8+
import { Loader2, Trash2, Coins } from 'lucide-react';
99
import {
1010
AlertDialog,
1111
AlertDialogAction,
@@ -18,13 +18,29 @@ import {
1818
AlertDialogTrigger,
1919
} from "@/components/ui/alert-dialog"
2020
import { useRouter } from 'next/navigation';
21+
import { useForm } from 'react-hook-form';
22+
import { zodResolver } from '@hookform/resolvers/zod';
23+
import { z } from 'zod';
24+
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
25+
import { Input } from '@/components/ui/input';
26+
27+
const addCryptoSchema = z.object({
28+
email: z.string().email({ message: 'Adresse e-mail invalide.' }),
29+
ticker: z.string().min(1, 'Ticker requis.').transform(v => v.toUpperCase()),
30+
quantity: z.coerce.number().positive('La quantité doit être positive.'),
31+
});
2132

2233

2334
export default function AdminPage() {
2435
const [loadingAction, setLoadingAction] = useState<string | null>(null);
2536
const { toast } = useToast();
2637
const router = useRouter();
2738

39+
const cryptoForm = useForm<z.infer<typeof addCryptoSchema>>({
40+
resolver: zodResolver(addCryptoSchema),
41+
defaultValues: { email: '', ticker: '', quantity: undefined },
42+
});
43+
2844
const handleAction = async (action: () => Promise<{ success?: string; error?: string }>, actionName: string) => {
2945
setLoadingAction(actionName);
3046
const result = await action();
@@ -47,8 +63,72 @@ export default function AdminPage() {
4763
setLoadingAction(null);
4864
};
4965

66+
async function handleGrantCrypto(values: z.infer<typeof addCryptoSchema>) {
67+
setLoadingAction('grantCrypto');
68+
const result = await addCryptoToUserByEmail(values);
69+
if (result.error) {
70+
toast({ variant: 'destructive', title: 'Erreur', description: result.error });
71+
} else {
72+
toast({ title: 'Succès', description: result.success });
73+
cryptoForm.reset();
74+
}
75+
setLoadingAction(null);
76+
}
77+
5078
return (
5179
<div className="space-y-6">
80+
<Card>
81+
<CardHeader>
82+
<CardTitle>Accorder des Cryptos</CardTitle>
83+
<CardDescription>Ajouter directement des actifs crypto au portefeuille d'un utilisateur.</CardDescription>
84+
</CardHeader>
85+
<CardContent>
86+
<Form {...cryptoForm}>
87+
<form onSubmit={cryptoForm.handleSubmit(handleGrantCrypto)} className="space-y-4">
88+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
89+
<FormField
90+
control={cryptoForm.control}
91+
name="email"
92+
render={({ field }) => (
93+
<FormItem>
94+
<FormLabel>Email de l'utilisateur</FormLabel>
95+
<FormControl><Input placeholder="utilisateur@exemple.com" {...field} /></FormControl>
96+
<FormMessage />
97+
</FormItem>
98+
)}
99+
/>
100+
<FormField
101+
control={cryptoForm.control}
102+
name="ticker"
103+
render={({ field }) => (
104+
<FormItem>
105+
<FormLabel>Ticker Crypto</FormLabel>
106+
<FormControl><Input placeholder="BTC, ETH, etc." {...field} /></FormControl>
107+
<FormMessage />
108+
</FormItem>
109+
)}
110+
/>
111+
<FormField
112+
control={cryptoForm.control}
113+
name="quantity"
114+
render={({ field }) => (
115+
<FormItem>
116+
<FormLabel>Quantité</FormLabel>
117+
<FormControl><Input type="number" step="any" placeholder="0.5" {...field} value={field.value ?? ''} /></FormControl>
118+
<FormMessage />
119+
</FormItem>
120+
)}
121+
/>
122+
</div>
123+
<Button type="submit" disabled={loadingAction !== null}>
124+
{loadingAction === 'grantCrypto' ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Coins className="mr-2 h-4 w-4" />}
125+
Accorder la Crypto
126+
</Button>
127+
</form>
128+
</Form>
129+
</CardContent>
130+
</Card>
131+
52132
<Card>
53133
<CardHeader>
54134
<CardTitle>Panneau d'Administration</CardTitle>

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { ManageCompanyAssetsDialog } from '@/components/manage-company-assets-di
1313
import { AddCompanyCashDialog } from '@/components/add-company-cash-dialog';
1414
import { getRigById } from '@/lib/mining';
1515
import { ManageMembersDialog } from '@/components/manage-members-dialog';
16+
import { WithdrawCompanyCashDialog } from '@/components/withdraw-company-cash-dialog';
1617

1718

1819
function getInitials(name: string) {
@@ -60,9 +61,14 @@ export default async function CompanyDetailPage({ params }: { params: { companyI
6061
</div>
6162
<div className="flex items-center gap-2">
6263
{isCEO && (
63-
<AddCompanyCashDialog companyId={company.id}>
64-
<Button variant="outline">Ajouter des fonds</Button>
65-
</AddCompanyCashDialog>
64+
<>
65+
<AddCompanyCashDialog companyId={company.id}>
66+
<Button variant="outline">Ajouter des fonds</Button>
67+
</AddCompanyCashDialog>
68+
<WithdrawCompanyCashDialog companyId={company.id} companyCash={company.cash}>
69+
<Button variant="outline">Retirer des fonds</Button>
70+
</WithdrawCompanyCashDialog>
71+
</>
6672
)}
6773
<InvestDialog company={company}>
6874
<Button>Investir</Button>
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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 { withdrawFromCompanyTreasury } from '@/lib/actions/companies';
14+
import { useRouter } from 'next/navigation';
15+
import { usePortfolio } from '@/context/portfolio-context';
16+
17+
interface WithdrawCompanyCashDialogProps {
18+
companyId: number;
19+
companyCash: number;
20+
children: React.ReactNode;
21+
}
22+
23+
const formSchema = z.object({
24+
amount: z.coerce.number().positive({ message: 'Le montant doit être supérieur à zéro.' }),
25+
});
26+
27+
export function WithdrawCompanyCashDialog({ companyId, companyCash, children }: WithdrawCompanyCashDialogProps) {
28+
const [open, setOpen] = useState(false);
29+
const { refreshPortfolio } = usePortfolio();
30+
const { toast } = useToast();
31+
const router = useRouter();
32+
const form = useForm<z.infer<typeof formSchema>>({
33+
resolver: zodResolver(formSchema),
34+
defaultValues: {
35+
amount: undefined,
36+
}
37+
});
38+
39+
const amount = form.watch('amount') || 0;
40+
41+
async function onSubmit(values: z.infer<typeof formSchema>) {
42+
const result = await withdrawFromCompanyTreasury(companyId, values.amount);
43+
if (result.error) {
44+
toast({ variant: 'destructive', title: 'Erreur', description: result.error });
45+
} else if (result.success) {
46+
toast({ title: 'Succès', description: result.success });
47+
await refreshPortfolio();
48+
router.refresh();
49+
setOpen(false);
50+
form.reset();
51+
}
52+
}
53+
54+
return (
55+
<Dialog open={open} onOpenChange={(isOpen) => {
56+
setOpen(isOpen)
57+
if (!isOpen) form.reset();
58+
}}>
59+
<DialogTrigger asChild>{children}</DialogTrigger>
60+
<DialogContent>
61+
<DialogHeader>
62+
<DialogTitle>Retirer des fonds de la Trésorerie</DialogTitle>
63+
<DialogDescription>
64+
Transférez des fonds de la trésorerie de l'entreprise vers votre solde personnel.
65+
Trésorerie disponible : ${companyCash.toFixed(2)}
66+
</DialogDescription>
67+
</DialogHeader>
68+
<Form {...form}>
69+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
70+
<FormField
71+
control={form.control}
72+
name="amount"
73+
render={({ field }) => (
74+
<FormItem>
75+
<FormLabel>Montant à retirer</FormLabel>
76+
<div className="relative">
77+
<FormControl>
78+
<Input
79+
type="number"
80+
step="0.01"
81+
placeholder="0.00"
82+
{...field}
83+
value={field.value ?? ''}
84+
onChange={e => field.onChange(e.target.value === '' ? undefined : e.target.valueAsNumber)} />
85+
</FormControl>
86+
<Button type="button" variant="ghost" size="sm" className="absolute right-1 top-1/2 -translate-y-1/2 h-7" onClick={() => form.setValue('amount', companyCash, { shouldValidate: true })}>
87+
Max
88+
</Button>
89+
</div>
90+
<FormMessage />
91+
</FormItem>
92+
)}
93+
/>
94+
95+
<DialogFooter>
96+
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>Annuler</Button>
97+
<Button type="submit" disabled={form.formState.isSubmitting || amount > companyCash || !form.formState.isValid}>
98+
{form.formState.isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
99+
Retirer ${amount > 0 ? amount.toFixed(2) : '0.00'}
100+
</Button>
101+
</DialogFooter>
102+
</form>
103+
</Form>
104+
</DialogContent>
105+
</Dialog>
106+
)
107+
}

src/lib/actions/admin.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
'use server';
22

33
import { db } from '@/lib/db';
4-
import { aiNews, companies, users } from '@/lib/db/schema';
4+
import { aiNews, companies, users, holdings as holdingsSchema, assets as assetsSchema } from '@/lib/db/schema';
55
import { revalidatePath } from 'next/cache';
6+
import { eq, and, ilike } from 'drizzle-orm';
67

78
export async function resetAiNews(): Promise<{ success?: string; error?: string }> {
89
try {
@@ -39,3 +40,61 @@ export async function resetAllUsers(): Promise<{ success?: string; error?: strin
3940
return { error: 'Une erreur est survenue lors de la suppression des utilisateurs.' };
4041
}
4142
}
43+
44+
export async function addCryptoToUserByEmail(data: { email: string, ticker: string, quantity: number }): Promise<{ success?: string; error?: string }> {
45+
try {
46+
const { email, ticker, quantity } = data;
47+
if (quantity <= 0) {
48+
return { error: 'La quantité doit être positive.' };
49+
}
50+
51+
const result = await db.transaction(async (tx) => {
52+
const user = await tx.query.users.findFirst({
53+
where: ilike(users.email, email)
54+
});
55+
56+
if (!user) {
57+
throw new Error(`Utilisateur avec l'email "${email}" non trouvé.`);
58+
}
59+
60+
const asset = await tx.query.assets.findFirst({
61+
where: eq(assetsSchema.ticker, ticker.toUpperCase())
62+
});
63+
64+
if (!asset || asset.type !== 'Crypto') {
65+
throw new Error(`Actif crypto avec le ticker "${ticker}" non trouvé.`);
66+
}
67+
68+
const existingHolding = await tx.query.holdings.findFirst({
69+
where: and(eq(holdingsSchema.userId, user.id), eq(holdingsSchema.ticker, asset.ticker))
70+
});
71+
72+
if (existingHolding) {
73+
const newQuantity = parseFloat(existingHolding.quantity) + quantity;
74+
// For granted assets, we don't change the average cost. If it's the first grant, avgCost is 0.
75+
await tx.update(holdingsSchema)
76+
.set({ quantity: newQuantity.toString() })
77+
.where(eq(holdingsSchema.id, existingHolding.id));
78+
} else {
79+
await tx.insert(holdingsSchema).values({
80+
userId: user.id,
81+
ticker: asset.ticker,
82+
name: asset.name,
83+
type: asset.type,
84+
quantity: quantity.toString(),
85+
avgCost: '0', // Granted assets have no cost
86+
});
87+
}
88+
return { success: `A accordé avec succès ${quantity} ${asset.ticker} à ${user.displayName}.` };
89+
});
90+
91+
revalidatePath('/portfolio');
92+
revalidatePath('/profile');
93+
94+
return result;
95+
96+
} catch (error: any) {
97+
console.error('Error adding crypto to user:', error);
98+
return { error: error.message || 'Une erreur est survenue.' };
99+
}
100+
}

src/lib/actions/companies.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,40 @@ export async function addCashToCompany(companyId: number, amount: number): Promi
473473
}
474474
}
475475

476+
export async function withdrawFromCompanyTreasury(companyId: number, amount: number): Promise<{ success?: string; error?: string }> {
477+
const session = await getSession();
478+
if (!session?.id) return { error: "Vous devez être connecté." };
479+
if (amount <= 0) return { error: "Le montant doit être positif." };
480+
481+
try {
482+
const result = await db.transaction(async (tx) => {
483+
const member = await tx.query.companyMembers.findFirst({ where: and(eq(companyMembers.companyId, companyId), eq(companyMembers.userId, session.id)) });
484+
if (!member || member.role !== 'ceo') throw new Error("Seul le PDG peut retirer des fonds de la trésorerie.");
485+
486+
const company = await tx.query.companies.findFirst({ where: eq(companies.id, companyId), columns: { cash: true } });
487+
if (!company) throw new Error("Entreprise non trouvée.");
488+
if (parseFloat(company.cash) < amount) throw new Error("Trésorerie de l'entreprise insuffisante.");
489+
490+
const user = await tx.query.users.findFirst({ where: eq(users.id, session.id), columns: { cash: true } });
491+
if (!user) throw new Error("Utilisateur non trouvé.");
492+
493+
await tx.update(companies).set({ cash: (parseFloat(company.cash) - amount).toFixed(2) }).where(eq(companies.id, companyId));
494+
await tx.update(users).set({ cash: (parseFloat(user.cash) + amount).toFixed(2) }).where(eq(users.id, session.id));
495+
496+
return { success: `${amount.toFixed(2)}$ retirés de la trésorerie de l'entreprise.` };
497+
});
498+
499+
revalidatePath(`/companies/${companyId}`);
500+
revalidatePath('/portfolio');
501+
revalidatePath('/profile');
502+
revalidatePath('/');
503+
return result;
504+
505+
} catch (error: any) {
506+
return { error: error.message || "Une erreur est survenue." };
507+
}
508+
}
509+
476510
export async function buyMiningRigForCompany(companyId: number, rigId: string): Promise<{ success?: string; error?: string }> {
477511
const session = await getSession();
478512
if (!session?.id) return { error: "Non autorisé." };

0 commit comments

Comments
 (0)