240 lines
10 KiB
TypeScript
240 lines
10 KiB
TypeScript
import { useEffect, useMemo, useState } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { AppLayout } from "@/components/AppLayout";
|
|
import { useAuth } from "@/hooks/useAuth";
|
|
import { supabase } from "@/integrations/supabase/client";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import { toast } from "@/hooks/use-toast";
|
|
import { formatIDR, formatDateTime } from "@/lib/format";
|
|
|
|
interface WalletData {
|
|
current_balance: number;
|
|
total_earned: number;
|
|
total_withdrawn: number;
|
|
pending_balance: number;
|
|
}
|
|
|
|
interface ProfitRow {
|
|
order_item_id: string;
|
|
order_id: string;
|
|
created_at: string;
|
|
product_title: string;
|
|
profit_share_percentage: number;
|
|
profit_amount: number;
|
|
profit_status: string | null;
|
|
wallet_transaction_id: string | null;
|
|
}
|
|
|
|
interface WithdrawalRow {
|
|
id: string;
|
|
amount: number;
|
|
status: string;
|
|
requested_at: string;
|
|
processed_at: string | null;
|
|
payment_reference: string | null;
|
|
admin_notes: string | null;
|
|
}
|
|
|
|
export default function MemberProfit() {
|
|
const { user, loading: authLoading } = useAuth();
|
|
const navigate = useNavigate();
|
|
const [loading, setLoading] = useState(true);
|
|
const [wallet, setWallet] = useState<WalletData | null>(null);
|
|
const [profits, setProfits] = useState<ProfitRow[]>([]);
|
|
const [withdrawals, setWithdrawals] = useState<WithdrawalRow[]>([]);
|
|
const [openWithdrawDialog, setOpenWithdrawDialog] = useState(false);
|
|
const [withdrawAmount, setWithdrawAmount] = useState("");
|
|
const [withdrawNotes, setWithdrawNotes] = useState("");
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [settings, setSettings] = useState<{ min_withdrawal_amount: number } | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!authLoading && !user) navigate("/auth");
|
|
if (user) fetchData();
|
|
}, [user, authLoading]);
|
|
|
|
const fetchData = async () => {
|
|
const [walletRes, profitRes, withdrawalRes, settingsRes] = await Promise.all([
|
|
supabase.rpc("get_collaborator_wallet", { p_user_id: user!.id }),
|
|
supabase
|
|
.from("collaborator_profits")
|
|
.select("*")
|
|
.eq("collaborator_user_id", user!.id)
|
|
.order("created_at", { ascending: false }),
|
|
supabase
|
|
.from("withdrawals")
|
|
.select("id, amount, status, requested_at, processed_at, payment_reference, admin_notes")
|
|
.eq("user_id", user!.id)
|
|
.order("requested_at", { ascending: false }),
|
|
supabase.rpc("get_collaboration_settings"),
|
|
]);
|
|
|
|
setWallet((walletRes.data?.[0] as WalletData) || {
|
|
current_balance: 0,
|
|
total_earned: 0,
|
|
total_withdrawn: 0,
|
|
pending_balance: 0,
|
|
});
|
|
setProfits((profitRes.data as ProfitRow[]) || []);
|
|
setWithdrawals((withdrawalRes.data as WithdrawalRow[]) || []);
|
|
setSettings({ min_withdrawal_amount: settingsRes.data?.[0]?.min_withdrawal_amount || 100000 });
|
|
setLoading(false);
|
|
};
|
|
|
|
const canSubmit = useMemo(() => {
|
|
const amount = Number(withdrawAmount || 0);
|
|
const min = settings?.min_withdrawal_amount || 100000;
|
|
const available = Number(wallet?.current_balance || 0);
|
|
return amount >= min && amount <= available;
|
|
}, [withdrawAmount, settings, wallet]);
|
|
|
|
const submitWithdrawal = async () => {
|
|
if (!canSubmit) {
|
|
toast({
|
|
title: "Nominal tidak valid",
|
|
description: "Periksa minimum penarikan dan saldo tersedia",
|
|
variant: "destructive",
|
|
});
|
|
return;
|
|
}
|
|
|
|
setSubmitting(true);
|
|
const { data, error } = await supabase.functions.invoke("create-withdrawal", {
|
|
body: {
|
|
amount: Number(withdrawAmount),
|
|
notes: withdrawNotes || null,
|
|
},
|
|
});
|
|
const response = data as { error?: string } | null;
|
|
|
|
if (error || response?.error) {
|
|
toast({
|
|
title: "Gagal membuat withdrawal",
|
|
description: response?.error || error?.message || "Unknown error",
|
|
variant: "destructive",
|
|
});
|
|
setSubmitting(false);
|
|
return;
|
|
}
|
|
|
|
toast({ title: "Berhasil", description: "Withdrawal request berhasil dibuat" });
|
|
setSubmitting(false);
|
|
setOpenWithdrawDialog(false);
|
|
setWithdrawAmount("");
|
|
setWithdrawNotes("");
|
|
fetchData();
|
|
};
|
|
|
|
if (authLoading || loading) {
|
|
return (
|
|
<AppLayout>
|
|
<div className="container mx-auto px-4 py-8">
|
|
<Skeleton className="h-10 w-1/3 mb-8" />
|
|
<Skeleton className="h-72 w-full" />
|
|
</div>
|
|
</AppLayout>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<AppLayout>
|
|
<div className="container mx-auto px-4 py-8 space-y-6">
|
|
<div>
|
|
<h1 className="text-4xl font-bold mb-2">Profit</h1>
|
|
<p className="text-muted-foreground">Ringkasan pendapatan kolaborasi Anda</p>
|
|
</div>
|
|
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
<Card className="border-2 border-border"><CardContent className="pt-6"><p className="text-sm text-muted-foreground">Total Earnings</p><p className="text-2xl font-bold">{formatIDR(wallet?.total_earned || 0)}</p></CardContent></Card>
|
|
<Card className="border-2 border-border"><CardContent className="pt-6"><p className="text-sm text-muted-foreground">Available Balance</p><p className="text-2xl font-bold">{formatIDR(wallet?.current_balance || 0)}</p></CardContent></Card>
|
|
<Card className="border-2 border-border"><CardContent className="pt-6"><p className="text-sm text-muted-foreground">Total Withdrawn</p><p className="text-2xl font-bold">{formatIDR(wallet?.total_withdrawn || 0)}</p></CardContent></Card>
|
|
<Card className="border-2 border-border"><CardContent className="pt-6"><p className="text-sm text-muted-foreground">Pending Balance</p><p className="text-2xl font-bold">{formatIDR(wallet?.pending_balance || 0)}</p></CardContent></Card>
|
|
</div>
|
|
|
|
<Card className="border-2 border-border">
|
|
<CardHeader className="flex flex-row items-center justify-between">
|
|
<CardTitle>Withdrawal</CardTitle>
|
|
<Button onClick={() => setOpenWithdrawDialog(true)}>Request Withdrawal</Button>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2 text-sm text-muted-foreground">
|
|
<p>Minimum withdrawal: {formatIDR(settings?.min_withdrawal_amount || 100000)}</p>
|
|
<p>Available balance: {formatIDR(wallet?.current_balance || 0)}</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="border-2 border-border">
|
|
<CardHeader><CardTitle>Profit History</CardTitle></CardHeader>
|
|
<CardContent className="p-0">
|
|
<Table>
|
|
<TableHeader><TableRow><TableHead>Date</TableHead><TableHead>Product</TableHead><TableHead>Share</TableHead><TableHead>Amount</TableHead><TableHead>Status</TableHead></TableRow></TableHeader>
|
|
<TableBody>
|
|
{profits.map((row) => (
|
|
<TableRow key={row.order_item_id}>
|
|
<TableCell>{formatDateTime(row.created_at)}</TableCell>
|
|
<TableCell>{row.product_title}</TableCell>
|
|
<TableCell>{row.profit_share_percentage}%</TableCell>
|
|
<TableCell>{formatIDR(row.profit_amount || 0)}</TableCell>
|
|
<TableCell><Badge variant="secondary">{row.profit_status || "-"}</Badge></TableCell>
|
|
</TableRow>
|
|
))}
|
|
{profits.length === 0 && (
|
|
<TableRow><TableCell colSpan={5} className="text-center text-muted-foreground py-8">Belum ada data profit</TableCell></TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="border-2 border-border">
|
|
<CardHeader><CardTitle>Withdrawal History</CardTitle></CardHeader>
|
|
<CardContent className="p-0">
|
|
<Table>
|
|
<TableHeader><TableRow><TableHead>Date</TableHead><TableHead>Amount</TableHead><TableHead>Status</TableHead><TableHead>Reference</TableHead></TableRow></TableHeader>
|
|
<TableBody>
|
|
{withdrawals.map((w) => (
|
|
<TableRow key={w.id}>
|
|
<TableCell>{formatDateTime(w.requested_at)}</TableCell>
|
|
<TableCell>{formatIDR(w.amount || 0)}</TableCell>
|
|
<TableCell><Badge variant={w.status === "completed" ? "default" : w.status === "rejected" ? "destructive" : "secondary"}>{w.status}</Badge></TableCell>
|
|
<TableCell>{w.payment_reference || "-"}</TableCell>
|
|
</TableRow>
|
|
))}
|
|
{withdrawals.length === 0 && (
|
|
<TableRow><TableCell colSpan={4} className="text-center text-muted-foreground py-8">Belum ada withdrawal</TableCell></TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<Dialog open={openWithdrawDialog} onOpenChange={setOpenWithdrawDialog}>
|
|
<DialogContent className="border-2 border-border">
|
|
<DialogHeader><DialogTitle>Request Withdrawal</DialogTitle></DialogHeader>
|
|
<div className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label>Nominal (IDR)</Label>
|
|
<Input type="number" value={withdrawAmount} onChange={(e) => setWithdrawAmount(e.target.value)} />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Notes</Label>
|
|
<Textarea value={withdrawNotes} onChange={(e) => setWithdrawNotes(e.target.value)} />
|
|
</div>
|
|
<Button onClick={submitWithdrawal} disabled={submitting} className="w-full">
|
|
{submitting ? "Submitting..." : "Submit Withdrawal"}
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</AppLayout>
|
|
);
|
|
}
|