first commit
This commit is contained in:
526
src/App.jsx
Normal file
526
src/App.jsx
Normal file
@@ -0,0 +1,526 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import logo from './logo.png';
|
||||
import { Card, Title, Text, AreaChart } from "@tremor/react";
|
||||
import {
|
||||
CreditCard,
|
||||
Wallet,
|
||||
Wallet2,
|
||||
HandCoins,
|
||||
Boxes,
|
||||
Box,
|
||||
Truck,
|
||||
Gauge,
|
||||
TrendingUp,
|
||||
PiggyBank,
|
||||
ShoppingCart,
|
||||
ShoppingBag,
|
||||
BadgeCheck,
|
||||
DollarSign,
|
||||
Ship,
|
||||
Package,
|
||||
ArrowDownCircle,
|
||||
ArrowUpCircle,
|
||||
LineChart,
|
||||
BarChart3,
|
||||
FileText,
|
||||
Receipt,
|
||||
Newspaper,
|
||||
ScrollText,
|
||||
} from "lucide-react";
|
||||
|
||||
function SplashScreen() {
|
||||
return (
|
||||
<div className="fixed inset-0 flex items-center justify-center bg-white dark:bg-gray-900 z-50">
|
||||
<div className="animate-splash opacity-0 scale-75">
|
||||
<img src={logo} alt="Logo" className="h-32 object-contain" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* --------------------------
|
||||
Communication Tryton
|
||||
-------------------------- */
|
||||
|
||||
function openInTryton(model, res_id, view_mode = "tree", domain) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: "open_tab",
|
||||
payload: { model, res_id, view_mode, domain },
|
||||
},
|
||||
"*"
|
||||
);
|
||||
}
|
||||
|
||||
/* --------------------------
|
||||
Données
|
||||
-------------------------- */
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const defaultSaleId = params.get("sale_id") || 249;
|
||||
const defaultPnlAmount = params.get("pnl_amount");
|
||||
const exposure = params.get("exposure");
|
||||
const topay = params.get("topay");
|
||||
const toreceive = params.get("toreceive");
|
||||
const draft_p = params.get("draft_p");
|
||||
const val_p = params.get("val_p");
|
||||
const conf_p = params.get("conf_p");
|
||||
const draft_s = params.get("draft_s");
|
||||
const val_s = params.get("val_s");
|
||||
const conf_s = params.get("conf_s");
|
||||
const shipment_d = params.get("shipment_d");
|
||||
const shipment_s = params.get("shipment_s");
|
||||
const shipment_r = params.get("shipment_r");
|
||||
const lot_m = params.get("lot_m");
|
||||
const lot_a = params.get("lot_a");
|
||||
const lot_al = params.get("lot_al");
|
||||
const inv_p = params.get("inv_p");
|
||||
const inv_s = params.get("inv_s");
|
||||
const move_cash = params.get("move_cash");
|
||||
|
||||
const data = [
|
||||
{ date: "2025-01-01", value: 12 },
|
||||
{ date: "2025-01-02", value: 14 },
|
||||
{ date: "2025-01-03", value: 10 },
|
||||
{ date: "2025-01-04", value: 18 },
|
||||
{ date: "2025-01-05", value: 22 },
|
||||
];
|
||||
|
||||
const purchaseData = [
|
||||
{ status: "Draft", count: draft_p, color: "bg-teal-500", onClick: () => openInTryton("purchase.purchase", undefined, "tree")},
|
||||
{ status: "Validated", count: val_p, color: "bg-cyan-500", onClick: () => openInTryton("purchase.purchase", undefined, "tree") },
|
||||
{ status: "Confirmed", count: conf_p, color: "bg-blue-500", onClick: () => openInTryton("purchase.purchase", undefined, "tree") },
|
||||
];
|
||||
|
||||
const saleData = [
|
||||
{ status: "Draft", count: draft_s, color: "bg-teal-500", onClick: () => openInTryton("sale.sale", undefined, "tree")},
|
||||
{ status: "Validated", count: val_s, color: "bg-cyan-500", onClick: () => openInTryton("sale.sale", undefined, "tree") },
|
||||
{ status: "Confirmed", count: conf_s, color: "bg-blue-500", onClick: () => openInTryton("sale.sale", undefined, "tree") },
|
||||
];
|
||||
|
||||
const shipmentData = [
|
||||
{ status: "Draft", count: shipment_d, color: "bg-teal-500", onClick: () => openInTryton("stock.shipment.in", undefined, "tree")},
|
||||
{ status: "Started", count: shipment_s, color: "bg-cyan-500", onClick: () => openInTryton("stock.shipment.in", undefined, "tree") },
|
||||
{ status: "Received", count: shipment_r, color: "bg-blue-500", onClick: () => openInTryton("stock.shipment.in", undefined, "tree") },
|
||||
];
|
||||
|
||||
const lotData = [
|
||||
{ status: "Matched", count: lot_m, color: "bg-teal-500", onClick: () => openInTryton("lot.report", undefined, "tree")},
|
||||
{ status: "Available", count: lot_a, color: "bg-cyan-500", onClick: () => openInTryton("lot.report", undefined, "tree") },
|
||||
{ status: "All", count: lot_al, color: "bg-blue-500", onClick: () => openInTryton("lot.report", undefined, "tree") },
|
||||
];
|
||||
|
||||
const kpis = [
|
||||
{
|
||||
title: "PNL ($)",
|
||||
value: defaultPnlAmount,
|
||||
trend: "+12% vs last month",
|
||||
trendValue: 12,
|
||||
icon: BarChart3,
|
||||
action: () => openInTryton("pnl.bi", [1], "form")
|
||||
},
|
||||
{
|
||||
title: "Exposure (Mt)",
|
||||
value: exposure,
|
||||
trend: "-3% this month",
|
||||
trendValue: -3,
|
||||
icon: Gauge,
|
||||
action: () => openInTryton("open.position.report", undefined, "tree")
|
||||
},
|
||||
{
|
||||
title: "Amount to pay ($)",
|
||||
value: topay,
|
||||
trend: "+5% this month",
|
||||
trendValue: 5,
|
||||
icon: DollarSign,
|
||||
action: () => openInTryton("account.invoice", undefined, "tree")
|
||||
},
|
||||
{
|
||||
title: "Amount to receive ($)",
|
||||
value: toreceive,
|
||||
trend: "-1% this month",
|
||||
trendValue: -1,
|
||||
icon: HandCoins,
|
||||
action: () => openInTryton("account.invoice", undefined, "tree")
|
||||
},
|
||||
];
|
||||
|
||||
const news = [
|
||||
{
|
||||
type: "Forex",
|
||||
label: "EUR/USD: 1.1400",
|
||||
trend: "+0.88%",
|
||||
color: "#1E3A8A", // bleu foncé
|
||||
date: "30-11-2025",
|
||||
icon: TrendingUp, // icône tendance boursière
|
||||
},
|
||||
{
|
||||
type: "Logistic",
|
||||
label: "INTHIRA NAREE loaded",
|
||||
date: "08-10-2025",
|
||||
color: "#1E3A8A",
|
||||
icon: Newspaper,
|
||||
},
|
||||
];
|
||||
|
||||
const cards = [
|
||||
// {
|
||||
// id: "sales_pending",
|
||||
// title: "Sales Pending",
|
||||
// value: 7,
|
||||
// trend: "+3% this week",
|
||||
// icon: Receipt,
|
||||
// action: () => openInTryton("sale.sale", undefined, "tree")
|
||||
// },
|
||||
{
|
||||
id: "purchase_invoices",
|
||||
title: "Purchase Invoices",
|
||||
value: inv_p,
|
||||
trend: "-2% this week",
|
||||
icon: Receipt,
|
||||
action: () => openInTryton("account.invoice", undefined, "tree")
|
||||
},
|
||||
{
|
||||
id: "sale_invoices",
|
||||
title: "Sale Invoices",
|
||||
value: inv_s,
|
||||
trend: "+2% this week",
|
||||
icon: FileText,
|
||||
action: () => openInTryton("account.invoice", undefined, "tree")
|
||||
},
|
||||
{
|
||||
id: "payments_to_validate",
|
||||
title: "Payments To Validate",
|
||||
value: move_cash,
|
||||
trend: "+12% this week",
|
||||
icon: HandCoins,
|
||||
action: () => openInTryton("account.move", undefined, "tree")
|
||||
},
|
||||
// {
|
||||
// id: "prepayments",
|
||||
// title: "Prepayments To Validate",
|
||||
// value: 9,
|
||||
// trend: "+5% this week",
|
||||
// icon: PiggyBank,
|
||||
// action: () => openInTryton("account.move", undefined, "tree")
|
||||
// },
|
||||
];
|
||||
|
||||
/* --------------------------
|
||||
Dashboard Component
|
||||
-------------------------- */
|
||||
|
||||
export default function App() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// durée du splash
|
||||
const timer = setTimeout(() => setLoading(false), 1500);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
if (loading) return <SplashScreen />;
|
||||
|
||||
const saleId = defaultSaleId;
|
||||
const total_p = purchaseData.reduce((sum, item) => sum + Number(item.count), 0);
|
||||
const total_s = saleData.reduce((sum, item) => sum + Number(item.count), 0);
|
||||
const total_sh = shipmentData.reduce((sum, item) => sum + Number(item.count), 0);
|
||||
const total_l = lotData.reduce((sum, item) => sum + Number(item.count), 0);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
|
||||
<main className="flex-1">
|
||||
|
||||
{/* KPIs en tête */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
{kpis.map((kpi) => {
|
||||
const Icon = kpi.icon;
|
||||
return (
|
||||
<Card
|
||||
key={kpi.title}
|
||||
onClick={kpi.action}
|
||||
className="relative p-4 flex flex-col items-center gap-2 shadow-md rounded-xl bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 overflow-hidden"
|
||||
>
|
||||
|
||||
<Icon className="absolute top-2 right-2 w-10 h-10 opacity-10" />
|
||||
|
||||
<Title className="text-sm text-center">{kpi.title}</Title>
|
||||
<Text className="text-4xl font-bold text-center">{kpi.value}</Text>
|
||||
|
||||
<button onClick={kpi.action} className="mt-4 text-xs text-teal-600 underline">
|
||||
View details
|
||||
</button>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
|
||||
{/* Purchase */}
|
||||
<Card className="p-4 rounded-xl border border-gray-200 dark:border-gray-700 shadow-md bg-white dark:bg-gray-800 hover:shadow-lg transition">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Title className="text-base">Purchases</Title>
|
||||
<ShoppingCart className="w-6 h-6" />
|
||||
</div>
|
||||
<Text className="text-3xl font-bold mb-1">{total_p}</Text>
|
||||
<Text className="text-xs text-gray-500 mb-4">Across all statuses</Text>
|
||||
|
||||
<div className="w-full h-1 bg-gray-200 dark:bg-gray-600 mb-4"></div>
|
||||
|
||||
<div className="flex w-full h-3 rounded overflow-hidden mb-4 bg-gray-200 dark:bg-gray-600">
|
||||
{purchaseData.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`${item.color} h-3`}
|
||||
style={{
|
||||
width: `${(item.count / total_p) * 100}%`,
|
||||
marginRight: i < purchaseData.length - 1 ? "2px" : "0",
|
||||
}}
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-xs">
|
||||
{purchaseData.map((item) => (
|
||||
<div
|
||||
key={item.status}
|
||||
onClick={item.onClick}
|
||||
className="flex items-center gap-2 cursor-pointer p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<span className={`w-3 h-3 rounded ${item.color}`}></span>
|
||||
<span className="flex-1">{item.status}</span>
|
||||
<span className="text-gray-500">
|
||||
({item.count} / {((item.count / total_p) * 100).toFixed(1)}%)
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
{/* Sale */}
|
||||
<Card className="p-4 rounded-xl border border-gray-200 dark:border-gray-700 shadow-md bg-white dark:bg-gray-800 hover:shadow-lg transition">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Title className="text-base">Sales</Title>
|
||||
<BadgeCheck className="w-6 h-6" />
|
||||
</div>
|
||||
<Text className="text-3xl font-bold mb-1">{total_s}</Text>
|
||||
<Text className="text-xs text-gray-500 mb-4">Across all statuses</Text>
|
||||
|
||||
<div className="w-full h-1 bg-gray-200 dark:bg-gray-600 mb-4"></div>
|
||||
|
||||
<div className="flex w-full h-3 rounded overflow-hidden mb-4 bg-gray-200 dark:bg-gray-600">
|
||||
{saleData.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`${item.color} h-3`}
|
||||
style={{
|
||||
width: `${(item.count / total_s) * 100}%`,
|
||||
marginRight: i < saleData.length - 1 ? "2px" : "0",
|
||||
}}
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-xs">
|
||||
{saleData.map((item) => (
|
||||
<div
|
||||
key={item.status}
|
||||
onClick={item.onClick}
|
||||
className="flex items-center gap-2 cursor-pointer p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<span className={`w-3 h-3 rounded ${item.color}`}></span>
|
||||
<span className="flex-1">{item.status}</span>
|
||||
<span className="text-gray-500">
|
||||
({item.count} / {((item.count / total_s) * 100).toFixed(1)}%)
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
{/* Shipment */}
|
||||
<Card className="p-4 rounded-xl border border-gray-200 dark:border-gray-700 shadow-md bg-white dark:bg-gray-800 hover:shadow-lg transition">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Title className="text-base">Shipments</Title>
|
||||
<Ship className="w-6 h-6" />
|
||||
</div>
|
||||
<Text className="text-3xl font-bold mb-1">{total_sh}</Text>
|
||||
<Text className="text-xs text-gray-500 mb-4">Across all statuses</Text>
|
||||
|
||||
<div className="w-full h-1 bg-gray-200 dark:bg-gray-600 mb-4"></div>
|
||||
|
||||
<div className="flex w-full h-3 rounded overflow-hidden mb-4 bg-gray-200 dark:bg-gray-600">
|
||||
{shipmentData.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`${item.color} h-3`}
|
||||
style={{
|
||||
width: `${(item.count / total_sh) * 100}%`,
|
||||
marginRight: i < shipmentData.length - 1 ? "2px" : "0",
|
||||
}}
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-xs">
|
||||
{shipmentData.map((item) => (
|
||||
<div
|
||||
key={item.status}
|
||||
onClick={item.onClick}
|
||||
className="flex items-center gap-2 cursor-pointer p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<span className={`w-3 h-3 rounded ${item.color}`}></span>
|
||||
<span className="flex-1">{item.status}</span>
|
||||
<span className="text-gray-500">
|
||||
({item.count} / {((item.count / total_sh) * 100).toFixed(1)}%)
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
{/* Lot */}
|
||||
<Card className="p-4 rounded-xl border border-gray-200 dark:border-gray-700 shadow-md bg-white dark:bg-gray-800 hover:shadow-lg transition">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Title className="text-base">Lots</Title>
|
||||
<Package className="w-6 h-6" />
|
||||
</div>
|
||||
<Text className="text-3xl font-bold mb-1">{total_l}</Text>
|
||||
<Text className="text-xs text-gray-500 mb-4">Across all statuses</Text>
|
||||
|
||||
<div className="w-full h-1 bg-gray-200 dark:bg-gray-600 mb-4"></div>
|
||||
|
||||
<div className="flex w-full h-3 rounded overflow-hidden mb-4 bg-gray-200 dark:bg-gray-600">
|
||||
{lotData.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`${item.color} h-3`}
|
||||
style={{
|
||||
width: `${(item.count / total_l) * 100}%`,
|
||||
marginRight: i < lotData.length - 1 ? "2px" : "0",
|
||||
}}
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-xs">
|
||||
{lotData.map((item) => (
|
||||
<div
|
||||
key={item.status}
|
||||
onClick={item.onClick}
|
||||
className="flex items-center gap-2 cursor-pointer p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<span className={`w-3 h-3 rounded ${item.color}`}></span>
|
||||
<span className="flex-1">{item.status}</span>
|
||||
<span className="text-gray-500">
|
||||
({item.count} / {((item.count / total_l) * 100).toFixed(1)}%)
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
{/* Analytics Card (Rows written style) */}
|
||||
{/* <Card className="p-4 rounded-xl border border-gray-200 dark:border-gray-700 shadow-md bg-white dark:bg-gray-800 hover:shadow-lg transition">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Rows written
|
||||
</h2>
|
||||
<span className="text-xs bg-red-100 text-red-600 px-2 py-1 rounded-md font-semibold">
|
||||
-3.9%
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-3xl font-bold">83,197</span>
|
||||
<span className="text-gray-400 ml-2 text-sm">from 86,580</span>
|
||||
</div>
|
||||
<div className="mt-4 h-20 relative">
|
||||
<svg viewBox="0 0 200 60" className="w-full h-full">
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="#c1c7d0"
|
||||
strokeWidth="2"
|
||||
points="0,30 20,28 40,25 60,27 80,32 100,35 120,33 140,30 160,29 180,32 200,35"
|
||||
/>
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="#3b5bdb"
|
||||
strokeWidth="2"
|
||||
points="0,40 20,45 40,42 60,38 80,45 100,48 120,44 140,40 160,42 180,38 200,36"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-gray-400 mt-2">
|
||||
<span>16/04/2024</span>
|
||||
<span>16/05/2024</span>
|
||||
</div>
|
||||
</Card> */}
|
||||
{/* Card Last News (taille standard card) */}
|
||||
<Card className="p-4 rounded-xl border border-gray-200 dark:border-gray-700 shadow-md bg-white dark:bg-gray-800 hover:shadow-lg transition">
|
||||
<Title className="text-base mb-4">Last News</Title>
|
||||
|
||||
<div className="space-y-3 text-sm">
|
||||
{news.map((n, idx) => {
|
||||
const Icon = n.icon;
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex justify-between items-center p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer"
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="w-4 h-4" />
|
||||
<span className="font-semibold">{n.type}</span>
|
||||
</div>
|
||||
<div className="ml-6">
|
||||
<span style={{ color: n.color }}>{n.label}</span>
|
||||
{n.trend && (
|
||||
<span className="ml-2 text-green-600">{n.trend}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-gray-500 text-xs">{n.date}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
{/* Cards KPI */}
|
||||
{cards.map((card) => {
|
||||
const Icon = card.icon;
|
||||
return (
|
||||
<Card
|
||||
key={card.id}
|
||||
className="p-4 rounded-xl border border-gray-200 dark:border-gray-700 shadow-md bg-white dark:bg-gray-800 hover:shadow-lg transition"
|
||||
>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Title className="text-base">{card.title}</Title>
|
||||
<Icon className="w-6 h-6" />
|
||||
</div>
|
||||
|
||||
<Text className="text-3xl font-bold mb-1">{card.value}</Text>
|
||||
<Text className="text-xs text-gray-500 mb-4">{card.trend}</Text>
|
||||
|
||||
<div className="w-full h-1 bg-gray-200 dark:bg-gray-600 mb-4"></div>
|
||||
|
||||
<AreaChart
|
||||
data={data}
|
||||
index="date"
|
||||
categories={["value"]}
|
||||
colors={["teal-300"]}
|
||||
className="h-16"
|
||||
showXAxis={false}
|
||||
showYAxis={false}
|
||||
showGridLines={false}
|
||||
showLegend={false}
|
||||
curve="monotone"
|
||||
/>
|
||||
|
||||
<button onClick={card.action} className="mt-4 text-xs text-teal-600 underline">
|
||||
View details
|
||||
</button>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
316
src/App_.jsx
Normal file
316
src/App_.jsx
Normal file
@@ -0,0 +1,316 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import logo from './logo.png';
|
||||
import { Card, Title, Text, AreaChart } from "@tremor/react";
|
||||
import {
|
||||
ShoppingCart,
|
||||
Receipt,
|
||||
PackageSearch,
|
||||
Truck,
|
||||
CreditCard,
|
||||
Boxes,
|
||||
TrendingUp,
|
||||
Coins,
|
||||
LineChart,
|
||||
DollarSign,
|
||||
Newspaper,
|
||||
} from "lucide-react";
|
||||
|
||||
function SplashScreen() {
|
||||
return (
|
||||
<div className="fixed inset-0 flex items-center justify-center bg-white dark:bg-gray-900 z-50">
|
||||
<div className="animate-splash opacity-0 scale-75">
|
||||
<img src={logo} alt="Logo" className="h-32 object-contain" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* --------------------------
|
||||
Communication Tryton
|
||||
-------------------------- */
|
||||
|
||||
function openInTryton(model, res_id, view_mode = "tree", domain) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: "open_tab",
|
||||
payload: { model, res_id, view_mode, domain },
|
||||
},
|
||||
"*"
|
||||
);
|
||||
}
|
||||
|
||||
/* --------------------------
|
||||
Données
|
||||
-------------------------- */
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const defaultSaleId = params.get("sale_id") || 249;
|
||||
const defaultPnlAmount = params.get("pnl_amount");
|
||||
const exposure = params.get("exposure");
|
||||
const topay = params.get("topay");
|
||||
const toreceive = params.get("toreceive");
|
||||
|
||||
const data = [
|
||||
{ date: "2025-01-01", value: 12 },
|
||||
{ date: "2025-01-02", value: 14 },
|
||||
{ date: "2025-01-03", value: 10 },
|
||||
{ date: "2025-01-04", value: 18 },
|
||||
{ date: "2025-01-05", value: 22 },
|
||||
];
|
||||
|
||||
const statusData = [
|
||||
{ status: "Draft", count: 10, color: "bg-teal-500" },
|
||||
{ status: "Confirmed", count: 7, color: "bg-cyan-500" },
|
||||
{ status: "Shipped", count: 4, color: "bg-blue-500" },
|
||||
];
|
||||
|
||||
const kpis = [
|
||||
{
|
||||
title: "PNL",
|
||||
value: defaultPnlAmount,
|
||||
trend: "+12% vs last month",
|
||||
icon: TrendingUp,
|
||||
action: () => openInTryton("pnl.bi", [1], "form")
|
||||
},
|
||||
{
|
||||
title: "Exposure",
|
||||
value: exposure,
|
||||
trend: "+8% this month",
|
||||
icon: Coins,
|
||||
action: () => openInTryton("open.position.report", undefined, "tree")
|
||||
},
|
||||
{
|
||||
title: "Amount to pay",
|
||||
value: topay,
|
||||
trend: "+5% this month",
|
||||
icon: LineChart,
|
||||
action: () => openInTryton("account.invoice", undefined, "tree")
|
||||
},
|
||||
{
|
||||
title: "Amount to receive",
|
||||
value: toreceive,
|
||||
trend: "+3% this month",
|
||||
icon: DollarSign,
|
||||
action: () => openInTryton("account.invoice", undefined, "tree")
|
||||
},
|
||||
];
|
||||
|
||||
const news = [
|
||||
{
|
||||
type: "Forex",
|
||||
label: "EUR/USD: 1.1400",
|
||||
trend: "+0.88%",
|
||||
color: "#1E3A8A", // bleu foncé
|
||||
date: "30-11-2025",
|
||||
icon: TrendingUp, // icône tendance boursière
|
||||
},
|
||||
{
|
||||
type: "Logistic",
|
||||
label: "INTHIRA NAREE loaded",
|
||||
date: "08-10-2025",
|
||||
color: "#1E3A8A",
|
||||
icon: Newspaper,
|
||||
},
|
||||
];
|
||||
|
||||
const cards = [
|
||||
{
|
||||
id: "purchases_not_confirmed",
|
||||
title: "Purchases Not Confirmed",
|
||||
value: 12,
|
||||
trend: "+8% this week",
|
||||
icon: ShoppingCart,
|
||||
},
|
||||
{
|
||||
id: "sales_pending",
|
||||
title: "Sales Pending",
|
||||
value: 7,
|
||||
trend: "+3% this week",
|
||||
icon: Receipt,
|
||||
},
|
||||
{
|
||||
id: "invoices_unpaid",
|
||||
title: "Invoices Unpaid",
|
||||
value: 15,
|
||||
trend: "-2% this week",
|
||||
icon: CreditCard,
|
||||
},
|
||||
{
|
||||
id: "payments_to_validate",
|
||||
title: "Payments To Validate",
|
||||
value: 4,
|
||||
trend: "+12% this week",
|
||||
icon: PackageSearch,
|
||||
},
|
||||
{
|
||||
id: "lots_to_produce",
|
||||
title: "Lots",
|
||||
value: 9,
|
||||
trend: "+5% this week",
|
||||
icon: Boxes,
|
||||
},
|
||||
{
|
||||
id: "shipments_pending",
|
||||
title: "Shipments Pending",
|
||||
value: 6,
|
||||
trend: "-1% this week",
|
||||
icon: Truck,
|
||||
},
|
||||
];
|
||||
|
||||
/* --------------------------
|
||||
Dashboard Component
|
||||
-------------------------- */
|
||||
|
||||
export default function App() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// durée du splash
|
||||
const timer = setTimeout(() => setLoading(false), 1500);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
if (loading) return <SplashScreen />;
|
||||
|
||||
const saleId = defaultSaleId;
|
||||
const total = statusData.reduce((sum, item) => sum + item.count, 0);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
|
||||
<main className="flex-1">
|
||||
|
||||
{/* KPIs en tête */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
{kpis.map((kpi) => {
|
||||
const Icon = kpi.icon;
|
||||
return (
|
||||
<Card
|
||||
key={kpi.title}
|
||||
onClick={kpi.action}
|
||||
className="p-4 flex flex-col gap-2 shadow-md rounded-xl bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<Title className="text-sm">{kpi.title}</Title>
|
||||
<Icon className="w-6 h-6" />
|
||||
</div>
|
||||
<Text className="text-2xl font-bold">{kpi.value}</Text>
|
||||
<Text className="text-xs text-gray-500">{kpi.trend}</Text>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
|
||||
{/* Card Last News (taille standard card) */}
|
||||
<Card className="p-4 rounded-xl border border-gray-200 dark:border-gray-700 shadow-md bg-white dark:bg-gray-800 hover:shadow-lg transition">
|
||||
<Title className="text-base mb-4">Last News</Title>
|
||||
|
||||
<div className="space-y-3 text-sm">
|
||||
{news.map((n, idx) => {
|
||||
const Icon = n.icon;
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex justify-between items-center p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer"
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="w-4 h-4" />
|
||||
<span className="font-semibold">{n.type}</span>
|
||||
</div>
|
||||
<div className="ml-6">
|
||||
<span style={{ color: n.color }}>{n.label}</span>
|
||||
{n.trend && (
|
||||
<span className="ml-2 text-green-600">{n.trend}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-gray-500 text-xs">{n.date}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Status Distribution */}
|
||||
<Card className="p-4 rounded-xl border border-gray-200 dark:border-gray-700 shadow-md bg-white dark:bg-gray-800 hover:shadow-lg transition">
|
||||
<Title className="text-base mb-4">Status Distribution</Title>
|
||||
|
||||
<Text className="text-3xl font-bold mb-1">{total}</Text>
|
||||
<Text className="text-xs text-gray-500 mb-4">Across all statuses</Text>
|
||||
|
||||
<div className="w-full h-1 bg-gray-200 dark:bg-gray-600 mb-4"></div>
|
||||
|
||||
<div className="flex w-full h-3 rounded overflow-hidden mb-4 bg-gray-200 dark:bg-gray-600">
|
||||
{statusData.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`${item.color} h-3`}
|
||||
style={{
|
||||
width: `${(item.count / total) * 100}%`,
|
||||
marginRight: i < statusData.length - 1 ? "2px" : "0",
|
||||
}}
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-xs">
|
||||
{statusData.map((item) => (
|
||||
<div
|
||||
key={item.status}
|
||||
className="flex items-center gap-2 cursor-pointer p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<span className={`w-3 h-3 rounded ${item.color}`}></span>
|
||||
<span className="flex-1">{item.status}</span>
|
||||
<span className="text-gray-500">
|
||||
({item.count} / {((item.count / total) * 100).toFixed(1)}%)
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Cards KPI */}
|
||||
{cards.map((card) => {
|
||||
const Icon = card.icon;
|
||||
return (
|
||||
<Card
|
||||
key={card.id}
|
||||
className="p-4 rounded-xl border border-gray-200 dark:border-gray-700 shadow-md bg-white dark:bg-gray-800 hover:shadow-lg transition"
|
||||
>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Title className="text-base">{card.title}</Title>
|
||||
<Icon className="w-6 h-6" />
|
||||
</div>
|
||||
|
||||
<Text className="text-3xl font-bold mb-1">{card.value}</Text>
|
||||
<Text className="text-xs text-gray-500 mb-4">{card.trend}</Text>
|
||||
|
||||
<div className="w-full h-1 bg-gray-200 dark:bg-gray-600 mb-4"></div>
|
||||
|
||||
<AreaChart
|
||||
data={data}
|
||||
index="date"
|
||||
categories={["value"]}
|
||||
colors={["teal-300"]}
|
||||
className="h-16"
|
||||
showXAxis={false}
|
||||
showYAxis={false}
|
||||
showGridLines={false}
|
||||
showLegend={false}
|
||||
curve="monotone"
|
||||
/>
|
||||
|
||||
<button className="mt-4 text-xs text-teal-600 underline">
|
||||
View details
|
||||
</button>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
264
src/components/Dashboard.jsx
Normal file
264
src/components/Dashboard.jsx
Normal file
@@ -0,0 +1,264 @@
|
||||
import React from "react";
|
||||
import { Card, Title, Text, AreaChart } from "@tremor/react";
|
||||
import {
|
||||
ShoppingCart,
|
||||
Receipt,
|
||||
PackageSearch,
|
||||
Truck,
|
||||
CreditCard,
|
||||
Boxes,
|
||||
TrendingUp,
|
||||
Coins,
|
||||
LineChart,
|
||||
DollarSign,
|
||||
Newspaper,
|
||||
} from "lucide-react";
|
||||
|
||||
/* --------------------------
|
||||
Communication Tryton
|
||||
-------------------------- */
|
||||
|
||||
function openInTryton(model, res_id, view_mode = "tree", domain) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: "open_tab",
|
||||
payload: { model, res_id, view_mode, domain },
|
||||
},
|
||||
"*"
|
||||
);
|
||||
}
|
||||
|
||||
/* --------------------------
|
||||
Données
|
||||
-------------------------- */
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const defaultSaleId = params.get("sale_id") || 249;
|
||||
|
||||
const data = [
|
||||
{ date: "2025-01-01", value: 12 },
|
||||
{ date: "2025-01-02", value: 14 },
|
||||
{ date: "2025-01-03", value: 10 },
|
||||
{ date: "2025-01-04", value: 18 },
|
||||
{ date: "2025-01-05", value: 22 },
|
||||
];
|
||||
|
||||
const statusData = [
|
||||
{ status: "Draft", count: 10, color: "bg-teal-500" },
|
||||
{ status: "Confirmed", count: 7, color: "bg-cyan-500" },
|
||||
{ status: "Shipped", count: 4, color: "bg-blue-500" },
|
||||
];
|
||||
|
||||
const kpis = [
|
||||
{ title: "PNL", value: "€ 42,800", trend: "+12% vs last month", icon: TrendingUp },
|
||||
{ title: "Sale Amount", value: "€ 12,400", trend: "+8% this month", icon: Coins },
|
||||
{ title: "Invoiced", value: "€ 9,870", trend: "+5% this month", icon: LineChart },
|
||||
{ title: "Paid", value: "€ 7,320", trend: "+3% this month", icon: DollarSign },
|
||||
];
|
||||
|
||||
const news = [
|
||||
{
|
||||
type: "Forex",
|
||||
label: "EUR/USD: 1.1400",
|
||||
trend: "+0.88%",
|
||||
color: "#1E3A8A",
|
||||
date: "30-11-2025",
|
||||
icon: TrendingUp,
|
||||
},
|
||||
{
|
||||
type: "Logistic",
|
||||
label: "INTHIRA NAREE loaded",
|
||||
date: "08-10-2025",
|
||||
color: "#1E3A8A",
|
||||
icon: Newspaper,
|
||||
},
|
||||
];
|
||||
|
||||
const cards = [
|
||||
{
|
||||
id: "purchases_not_confirmed",
|
||||
title: "Purchases Not Confirmed",
|
||||
value: 12,
|
||||
trend: "+8% this week",
|
||||
icon: ShoppingCart,
|
||||
},
|
||||
{
|
||||
id: "sales_pending",
|
||||
title: "Sales Pending",
|
||||
value: 7,
|
||||
trend: "+3% this week",
|
||||
icon: Receipt,
|
||||
},
|
||||
{
|
||||
id: "invoices_unpaid",
|
||||
title: "Invoices Unpaid",
|
||||
value: 15,
|
||||
trend: "-2% this week",
|
||||
icon: CreditCard,
|
||||
},
|
||||
{
|
||||
id: "payments_to_validate",
|
||||
title: "Payments To Validate",
|
||||
value: 4,
|
||||
trend: "+12% this week",
|
||||
icon: PackageSearch,
|
||||
},
|
||||
{
|
||||
id: "lots_to_produce",
|
||||
title: "Lots",
|
||||
value: 9,
|
||||
trend: "+5% this week",
|
||||
icon: Boxes,
|
||||
},
|
||||
{
|
||||
id: "shipments_pending",
|
||||
title: "Shipments Pending",
|
||||
value: 6,
|
||||
trend: "-1% this week",
|
||||
icon: Truck,
|
||||
},
|
||||
];
|
||||
|
||||
/* --------------------------
|
||||
Dashboard Component
|
||||
-------------------------- */
|
||||
|
||||
export default function Dashboard() {
|
||||
const saleId = defaultSaleId;
|
||||
const total = statusData.reduce((sum, item) => sum + item.count, 0);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
|
||||
<main className="flex-1">
|
||||
|
||||
{/* KPIs */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
{kpis.map((kpi) => {
|
||||
const Icon = kpi.icon;
|
||||
return (
|
||||
<Card
|
||||
key={kpi.title}
|
||||
className="p-4 flex flex-col gap-2 shadow-md rounded-xl bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<Title className="text-sm">{kpi.title}</Title>
|
||||
<Icon className="w-6 h-6" />
|
||||
</div>
|
||||
<Text className="text-2xl font-bold">{kpi.value}</Text>
|
||||
<Text className="text-xs text-gray-500">{kpi.trend}</Text>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
|
||||
{/* Last News */}
|
||||
<Card className="p-4 rounded-xl border border-gray-200 dark:border-gray-700 shadow-md bg-white dark:bg-gray-800 hover:shadow-lg transition">
|
||||
<Title className="text-base mb-4">Last News</Title>
|
||||
|
||||
<div className="space-y-3 text-sm">
|
||||
{news.map((n, idx) => {
|
||||
const Icon = n.icon;
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex justify-between items-center p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer"
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="w-4 h-4" />
|
||||
<span className="font-semibold">{n.type}</span>
|
||||
</div>
|
||||
<div className="ml-6">
|
||||
<span style={{ color: n.color }}>{n.label}</span>
|
||||
{n.trend && <span className="ml-2 text-green-600">{n.trend}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-gray-500 text-xs">{n.date}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Status Distribution */}
|
||||
<Card className="p-4 rounded-xl border border-gray-200 dark:border-gray-700 shadow-md bg-white dark:bg-gray-800 hover:shadow-lg transition">
|
||||
<Title className="text-base mb-4">Status Distribution</Title>
|
||||
|
||||
<Text className="text-3xl font-bold mb-1">{total}</Text>
|
||||
<Text className="text-xs text-gray-500 mb-4">Across all statuses</Text>
|
||||
|
||||
<div className="w-full h-1 bg-gray-200 dark:bg-gray-600 mb-4"></div>
|
||||
|
||||
<div className="flex w-full h-3 rounded overflow-hidden mb-4 bg-gray-200 dark:bg-gray-600">
|
||||
{statusData.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`${item.color} h-3`}
|
||||
style={{
|
||||
width: `${(item.count / total) * 100}%`,
|
||||
marginRight: i < statusData.length - 1 ? "2px" : "0",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-xs">
|
||||
{statusData.map((item) => (
|
||||
<div
|
||||
key={item.status}
|
||||
className="flex items-center gap-2 cursor-pointer p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<span className={`w-3 h-3 rounded ${item.color}`} />
|
||||
<span className="flex-1">{item.status}</span>
|
||||
<span className="text-gray-500">
|
||||
({item.count} / {((item.count / total) * 100).toFixed(1)}%)
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* KPI Cards */}
|
||||
{cards.map((card) => {
|
||||
const Icon = card.icon;
|
||||
return (
|
||||
<Card
|
||||
key={card.id}
|
||||
className="p-4 rounded-xl border border-gray-200 dark:border-gray-700 shadow-md bg-white dark:bg-gray-800 hover:shadow-lg transition"
|
||||
>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Title className="text-base">{card.title}</Title>
|
||||
<Icon className="w-6 h-6" />
|
||||
</div>
|
||||
|
||||
<Text className="text-3xl font-bold mb-1">{card.value}</Text>
|
||||
<Text className="text-xs text-gray-500 mb-4">{card.trend}</Text>
|
||||
|
||||
<div className="w-full h-1 bg-gray-200 dark:bg-gray-600 mb-4"></div>
|
||||
|
||||
<AreaChart
|
||||
data={data}
|
||||
index="date"
|
||||
categories={["value"]}
|
||||
colors={["teal-300"]}
|
||||
className="h-16"
|
||||
showXAxis={false}
|
||||
showYAxis={false}
|
||||
showGridLines={false}
|
||||
showLegend={false}
|
||||
curve="monotone"
|
||||
/>
|
||||
|
||||
<button className="mt-4 text-xs text-teal-600 underline">
|
||||
View details
|
||||
</button>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
15
src/components/SplashScreen.jsx
Normal file
15
src/components/SplashScreen.jsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
export default function SplashScreen() {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen bg-white">
|
||||
<motion.img
|
||||
src="/logo.png"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 1.2 }}
|
||||
className="w-32"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
138
src/components/TradonDashboard.jsx
Normal file
138
src/components/TradonDashboard.jsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { Card, Title, Text, AreaChart } from "@tremor/react";
|
||||
import { ShoppingCart, Receipt, PackageSearch, Truck, CreditCard, Boxes } from "lucide-react";
|
||||
|
||||
/* --------------------------
|
||||
Communication Tryton
|
||||
-------------------------- */
|
||||
function openInTryton(model, res_id, view_mode = "form") {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: "open_tab",
|
||||
payload: { model, res_id, view_mode }
|
||||
},
|
||||
"*" // remplacer par l’URL exacte de Tryton en production
|
||||
);
|
||||
}
|
||||
|
||||
/* --------------------------
|
||||
Paramètres URL
|
||||
-------------------------- */
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const defaultSaleId = params.get("sale_id");
|
||||
|
||||
/* --------------------------
|
||||
Données temporaires
|
||||
-------------------------- */
|
||||
const data = [
|
||||
{ date: "2025-01-01", value: 12 },
|
||||
{ date: "2025-01-02", value: 14 },
|
||||
{ date: "2025-01-03", value: 10 },
|
||||
{ date: "2025-01-04", value: 18 },
|
||||
{ date: "2025-01-05", value: 22 }
|
||||
];
|
||||
|
||||
/* --------------------------
|
||||
Carte et couleurs statiques
|
||||
-------------------------- */
|
||||
const cards = [
|
||||
{ id: "purchases_not_confirmed", title: "Purchases Not Confirmed", value: 12, trend: "+8%", icon: ShoppingCart, color: "blue" },
|
||||
{ id: "sales_pending", title: "Sales Pending", value: 5, trend: "+3%", icon: Receipt, color: "orange" },
|
||||
{ id: "invoices_unpaid", title: "Invoices Unpaid", value: 15, trend: "-2%", icon: CreditCard, color: "green" },
|
||||
{ id: "payments_to_validate", title: "Payments To Validate", value: 4, trend: "+12%", icon: PackageSearch, color: "red" },
|
||||
{ id: "lots_to_produce", title: "Lots To Produce", value: 9, trend: "+5%", icon: Boxes, color: "purple" },
|
||||
{ id: "shipments_pending", title: "Shipments Pending", value: 6, trend: "-1%", icon: Truck, color: "yellow" }
|
||||
];
|
||||
|
||||
/* Map couleurs statiques */
|
||||
const colorMap = {
|
||||
blue: { text: "text-blue-500", bg: "bg-blue-500", textDark: "text-blue-400" },
|
||||
orange: { text: "text-orange-500", bg: "bg-orange-500", textDark: "text-orange-400" },
|
||||
green: { text: "text-green-500", bg: "bg-green-500", textDark: "text-green-400" },
|
||||
red: { text: "text-red-500", bg: "bg-red-500", textDark: "text-red-400" },
|
||||
purple: { text: "text-purple-500", bg: "bg-purple-500", textDark: "text-purple-400" },
|
||||
yellow: { text: "text-yellow-800", bg: "bg-yellow-800", textDark: "text-yellow-700" }
|
||||
};
|
||||
|
||||
/* --------------------------
|
||||
Dashboard Component
|
||||
-------------------------- */
|
||||
export default function TradonDashboard() {
|
||||
const saleId = defaultSaleId || 1;
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", minHeight: "100vh", backgroundColor: "#f3f4f6", color: "#111827" }}>
|
||||
|
||||
{/* Sidebar */}
|
||||
<aside style={{ width: 256, backgroundColor: "#ffffff", padding: 24, display: "flex", flexDirection: "column", gap: 24 }}>
|
||||
<h1 style={{ fontSize: 24, fontWeight: 600, color: "#14b8a6" }}>Tradon</h1>
|
||||
<nav style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
<a style={{ cursor: "pointer" }}>Dashboard</a>
|
||||
<a style={{ cursor: "pointer" }}>Sales</a>
|
||||
<a style={{ cursor: "pointer" }}>Purchases</a>
|
||||
<a style={{ cursor: "pointer" }}>Invoices</a>
|
||||
<a style={{ cursor: "pointer" }}>Stock</a>
|
||||
<a style={{ cursor: "pointer" }}>Shipments</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
{/* Main content */}
|
||||
<main style={{ flex: 1, padding: 40 }}>
|
||||
<header style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 40 }}>
|
||||
<Title style={{ fontSize: 32, fontWeight: 700 }}>Dashboard</Title>
|
||||
<input style={{ padding: "8px 16px", borderRadius: 12, border: "none", backgroundColor: "#e5e7eb" }} placeholder="Search..." />
|
||||
</header>
|
||||
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(300px, 1fr))", gap: 32 }}>
|
||||
{cards.map(card => {
|
||||
const Icon = card.icon;
|
||||
const cardColor = colorMap[card.color];
|
||||
|
||||
return (
|
||||
<Card key={card.title} style={{ padding: 16, borderRadius: 24, border: "1px solid #e5e7eb", boxShadow: "0 4px 6px rgba(0,0,0,0.1)", backgroundColor: "#ffffff" }}>
|
||||
|
||||
{/* Title + icon */}
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
|
||||
<Title>{card.title}</Title>
|
||||
<Icon style={{ width: 24, height: 24, color: cardColor.text.replace("text-", "") }} />
|
||||
</div>
|
||||
|
||||
{/* Value + trend */}
|
||||
<Text style={{ fontSize: 32, fontWeight: 600, marginBottom: 8 }}>{card.value}</Text>
|
||||
{card.trend && <Text style={{ fontSize: 12, color: cardColor.textDark.replace("text-", ""), marginBottom: 16 }}>{card.trend}</Text>}
|
||||
|
||||
{/* Bar */}
|
||||
<div style={{ width: "100%", height: 8, backgroundColor: "#e5e7eb", borderRadius: 8, marginBottom: 16 }}>
|
||||
<div style={{ width: `${Math.min(card.value * 3, 100)}%`, height: "100%", backgroundColor: cardColor.bg.replace("bg-", ""), borderRadius: 8 }}></div>
|
||||
</div>
|
||||
|
||||
{/* Chart */}
|
||||
<AreaChart
|
||||
data={data}
|
||||
index="date"
|
||||
categories={["value"]}
|
||||
colors={[card.color]}
|
||||
className="h-24"
|
||||
showXAxis={false}
|
||||
showYAxis={false}
|
||||
showGridLines={false}
|
||||
curve="monotone"
|
||||
/>
|
||||
|
||||
{/* Bouton Tryton */}
|
||||
<button
|
||||
onClick={() => openInTryton("sale.sale", saleId)}
|
||||
style={{ marginTop: 16, fontSize: 12, color: cardColor.textDark.replace("text-", ""), cursor: "pointer", background: "none", border: "none", textDecoration: "underline" }}
|
||||
>
|
||||
Open in Tryton
|
||||
</button>
|
||||
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
23
src/components/ui/card.jsx
Normal file
23
src/components/ui/card.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
export function Card({ className = "", children }) {
|
||||
return (
|
||||
<div className={`rounded-xl shadow bg-white dark:bg-gray-800 p-4 ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CardHeader({ children }) {
|
||||
return <div className="mb-3">{children}</div>;
|
||||
}
|
||||
|
||||
export function CardTitle({ className = "", children }) {
|
||||
return (
|
||||
<h2 className={`text-lg font-semibold text-gray-800 dark:text-gray-100 ${className}`}>
|
||||
{children}
|
||||
</h2>
|
||||
);
|
||||
}
|
||||
|
||||
export function CardContent({ children }) {
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
24
src/index.css
Normal file
24
src/index.css
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@keyframes splashFade {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.75);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.5);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(2.1);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-splash {
|
||||
animation: splashFade 2.5s ease-in-out forwards;
|
||||
}
|
||||
|
||||
|
||||
BIN
src/logo.png
Normal file
BIN
src/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
6
src/main.jsx
Normal file
6
src/main.jsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App.jsx";
|
||||
import "./index.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
|
||||
Reference in New Issue
Block a user