system-prompts-and-models-o.../salesflow-saas/frontend/src/components/dealix/affiliates-view.tsx
2026-04-04 18:04:21 +03:00

326 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import dynamic from "next/dynamic";
import { useCallback, useEffect, useMemo, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
Users,
Award,
TrendingUp,
Building2,
UserPlus,
Filter,
Download,
Route,
Sparkles,
Loader2,
} from "lucide-react";
import { apiFetch } from "@/lib/api-client";
const AffiliateNetworkOrb = dynamic(
() => import("./affiliate-network-orb").then((m) => m.AffiliateNetworkOrb),
{
ssr: false,
loading: () => (
<div className="flex min-h-[300px] items-center justify-center rounded-2xl border border-border/50 bg-secondary/20">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
),
},
);
type JourneyStep = { step: number; title: string; detail_ar: string };
type ProgramPayload = {
title_ar?: string;
journey_ar: JourneyStep[];
commission_rates: Record<string, { price: number; rate: number }>;
bonus_tiers: { min_deals: number; bonus: number }[];
auto_employ_rule_ar?: string;
};
type LeaderRow = {
name: string;
deals: number;
commission: number;
status: string;
};
function formatSar(n: number) {
return `${n.toLocaleString("ar-SA", { maximumFractionDigits: 0 })} ر.س`;
}
function statusLabelAr(s: string) {
const m: Record<string, string> = {
active: "نشط",
employed: "مُوظّف / مرشح توظيف",
pending: "قيد المراجعة",
suspended: "معلّق",
terminated: "منتهي",
};
return m[s] ?? s;
}
export function AffiliatesView() {
const [program, setProgram] = useState<ProgramPayload | null>(null);
const [leaderboard, setLeaderboard] = useState<LeaderRow[]>([]);
const [loadErr, setLoadErr] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const load = useCallback(async () => {
setLoading(true);
setLoadErr(null);
try {
const [pRes, lRes] = await Promise.all([
apiFetch("/api/v1/affiliates/program"),
apiFetch("/api/v1/affiliates/leaderboard/top?limit=20"),
]);
if (!pRes.ok) throw new Error("program");
if (!lRes.ok) throw new Error("leaderboard");
setProgram((await pRes.json()) as ProgramPayload);
setLeaderboard((await lRes.json()) as LeaderRow[]);
} catch {
setLoadErr("تعذر تحميل بيانات البرنامج أو لوحة الصدارة. تحقق من الاتصال بالـ API.");
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
void load();
}, [load]);
const stats = useMemo(() => {
const n = leaderboard.length;
const totalComm = leaderboard.reduce((a, r) => a + (r.commission || 0), 0);
const hireReady = leaderboard.filter((r) => r.status === "employed" || r.deals >= 10).length;
return { n, totalComm, hireReady };
}, [leaderboard]);
const shareOnboarding = (name: string, hint: string) => {
const text = `مرحباً ${name}، رابط انضمامك كشريك Dealix: https://dealix.sa/affiliate — مرجع: ${hint}`;
if (typeof navigator !== "undefined" && navigator.share) {
void navigator.share({ title: "Dealix — شراكة", text, url: "https://dealix.sa" });
} else {
window.open(`https://wa.me/?text=${encodeURIComponent(text)}`, "_blank");
}
};
return (
<div className="p-8 max-w-7xl mx-auto space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex flex-col gap-6 lg:flex-row lg:items-end lg:justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight mb-2">👥 الشركاء والمسوقين</h1>
<p className="text-muted-foreground max-w-xl">
رحلة كاملة من التسجيل إلى العمولة والترقية، مع لوحة صدارة حية من الـ API ومشهد ثلاثي الأبعاد تفاعلي.
</p>
</div>
<div className="flex flex-wrap gap-3">
<button
type="button"
className="flex items-center gap-2 px-5 py-2.5 rounded-xl border border-border bg-card hover:bg-secondary/50 transition-colors text-sm font-medium"
>
<Download className="w-4 h-4" />
تصدير
</button>
<button
type="button"
className="flex items-center gap-2 px-5 py-2.5 rounded-xl bg-primary hover:bg-primary/90 text-primary-foreground font-medium shadow-lg shadow-primary/25 transition-all"
>
<UserPlus className="w-5 h-5" />
إضافة مسوق
</button>
</div>
</div>
{loadErr && (
<div className="rounded-xl border border-amber-500/40 bg-amber-500/10 px-4 py-3 text-sm text-amber-200 flex items-center justify-between gap-4">
<span>{loadErr}</span>
<button type="button" onClick={() => void load()} className="shrink-0 text-primary font-medium underline">
إعادة المحاولة
</button>
</div>
)}
<div className="grid grid-cols-1 gap-8 lg:grid-cols-2 lg:items-stretch">
<div className="space-y-6">
<div className="glass-card border border-border/50 p-6">
<div className="mb-4 flex items-center gap-2 text-primary">
<Route className="h-5 w-5" />
<h2 className="text-lg font-bold">{program?.title_ar ?? "رحلة المسوق"}</h2>
</div>
{loading && !program ? (
<div className="flex items-center gap-2 text-muted-foreground text-sm">
<Loader2 className="h-4 w-4 animate-spin" /> جاري تحميل الخطوات
</div>
) : (
<ol className="space-y-4">
<AnimatePresence>
{(program?.journey_ar ?? []).map((j, i) => (
<motion.li
key={j.step}
initial={{ opacity: 0, x: -8 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.05 }}
className="flex gap-4 rounded-xl border border-border/40 bg-secondary/10 p-4"
>
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary/20 text-sm font-bold text-primary">
{j.step}
</span>
<div>
<div className="font-semibold">{j.title}</div>
<p className="mt-1 text-sm text-muted-foreground leading-relaxed">{j.detail_ar}</p>
</div>
</motion.li>
))}
</AnimatePresence>
</ol>
)}
{program?.auto_employ_rule_ar && (
<p className="mt-4 text-xs text-muted-foreground border-t border-border/40 pt-4">{program.auto_employ_rule_ar}</p>
)}
</div>
{program?.commission_rates && (
<div className="glass-card border border-primary/25 bg-primary/5 p-6">
<div className="mb-3 flex items-center gap-2 text-primary">
<Sparkles className="h-5 w-5" />
<h3 className="font-bold">شرائح العمولة (من الـ API)</h3>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
{Object.entries(program.commission_rates).map(([plan, v]) => (
<div key={plan} className="rounded-xl border border-border/50 bg-card/60 p-4 text-center">
<div className="text-xs uppercase text-muted-foreground">{plan}</div>
<div className="mt-1 text-lg font-bold">{formatSar(v.price)}</div>
<div className="text-sm text-emerald-500 font-medium">{(v.rate * 100).toFixed(0)}% عمولة</div>
</div>
))}
</div>
{program.bonus_tiers?.length ? (
<ul className="mt-4 space-y-1 text-xs text-muted-foreground">
{program.bonus_tiers.map((t) => (
<li key={t.min_deals}>
من {t.min_deals} صفقات: مكافأة {formatSar(t.bonus)}
</li>
))}
</ul>
) : null}
</div>
)}
</div>
<div className="flex flex-col gap-6">
<AffiliateNetworkOrb />
<p className="text-center text-xs text-muted-foreground px-2">
تفاعل ثلاثي الأبعاد عبر React Three Fiber مناسب لصفحات التسويق والشراكة دون إعادة تحميل كاملة للصفحة.
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="glass-card p-6 border border-border/50">
<div className="flex justify-between items-center mb-4">
<div className="p-3 rounded-xl bg-blue-500/10 text-blue-500">
<Users className="w-6 h-6" />
</div>
</div>
<h3 className="text-2xl font-bold mb-1">{stats.n}</h3>
<p className="text-sm text-muted-foreground font-medium">في لوحة الصدارة (نشط / مُوظّف)</p>
</div>
<div className="glass-card p-6 border border-border/50">
<div className="flex justify-between items-center mb-4">
<div className="p-3 rounded-xl bg-emerald-500/10 text-emerald-500">
<TrendingUp className="w-6 h-6" />
</div>
</div>
<h3 className="text-2xl font-bold mb-1">{formatSar(stats.totalComm)}</h3>
<p className="text-sm text-muted-foreground font-medium">مجموع عمولات المعروضين</p>
</div>
<div className="glass-card p-6 border border-primary/30 bg-primary/5">
<div className="flex justify-between items-center mb-4">
<div className="p-3 rounded-xl bg-primary text-primary-foreground shadow-lg">
<Building2 className="w-6 h-6" />
</div>
</div>
<h3 className="text-2xl font-bold mb-1 text-primary">{stats.hireReady}</h3>
<p className="text-sm text-muted-foreground font-medium">بمعايير أداء عالية (10+ صفقات أو employed)</p>
</div>
</div>
<div className="glass-card overflow-hidden border border-border/50">
<div className="flex justify-between items-center p-6 border-b border-border/50 bg-secondary/10">
<h2 className="text-lg font-bold">لوحة الصدارة (من الـ API)</h2>
<button
type="button"
className="flex items-center gap-2 p-2 rounded-lg text-muted-foreground hover:bg-secondary/50 transition-colors"
>
<Filter className="w-5 h-5" />
<span className="text-sm">تصفية</span>
</button>
</div>
<div className="overflow-x-auto">
<table className="w-full text-right text-sm">
<thead className="bg-secondary/30 text-muted-foreground">
<tr>
<th className="py-4 px-6 font-medium">#</th>
<th className="py-4 px-6 font-medium">الاسم</th>
<th className="py-4 px-6 font-medium">الحالة</th>
<th className="py-4 px-6 font-medium">الصفقات</th>
<th className="py-4 px-6 font-medium">العمولة المتراكمة</th>
<th className="py-4 px-6 font-medium">إجراء</th>
</tr>
</thead>
<tbody className="divide-y divide-border/30">
{leaderboard.length === 0 && !loading ? (
<tr>
<td colSpan={6} className="py-12 text-center text-muted-foreground">
لا بيانات بعد سجّل أول مسوق عبر{" "}
<code className="rounded bg-secondary px-1.5 py-0.5 text-xs">POST /api/v1/affiliates/register</code>
</td>
</tr>
) : (
leaderboard.map((aff, i) => (
<tr key={`${aff.name}-${i}`} className="hover:bg-white/5 transition-colors group">
<td className="py-4 px-6 font-mono text-muted-foreground">{i + 1}</td>
<td className="py-4 px-6">
<div className="font-bold text-foreground">{aff.name}</div>
</td>
<td className="py-4 px-6">
<span className="rounded px-2.5 py-1 text-xs font-medium bg-secondary/80">{statusLabelAr(aff.status)}</span>
</td>
<td className="py-4 px-6 font-bold">{aff.deals}</td>
<td className="py-4 px-6 font-mono text-emerald-500">{formatSar(aff.commission)}</td>
<td className="py-4 px-6">
<div className="flex flex-wrap items-center gap-2">
{(aff.deals >= 10 || aff.status === "employed") && (
<button
type="button"
className="flex items-center gap-1.5 px-3 py-1.5 bg-emerald-500 text-white rounded-lg text-xs font-bold hover:bg-emerald-600 transition-colors shadow-lg shadow-emerald-500/20"
>
<Award className="w-3.5 h-3.5" />
ترقية
</button>
)}
<button
type="button"
onClick={() => shareOnboarding(aff.name, `#${i + 1}`)}
className="p-1.5 rounded-lg border border-border bg-card hover:bg-secondary/50 text-muted-foreground hover:text-primary transition-all"
title="مشاركة رابط الانضمام"
>
<UserPlus className="w-4 h-4" />
</button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</div>
);
}