759 lines
31 KiB
JavaScript
759 lines
31 KiB
JavaScript
import React, { useState, useEffect } from "react";
|
|
import logo from './logo.png';
|
|
import { Card, Title, Text, AreaChart, BarChart } 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, message = "open_tab", context = {}) {
|
|
window.parent.postMessage(
|
|
{
|
|
type: message,
|
|
payload: { model, res_id, view_mode, domain, context },
|
|
},
|
|
"*"
|
|
);
|
|
}
|
|
|
|
/* --------------------------
|
|
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 = Number(lot_a) + Number(lot_m)
|
|
const inv_p = params.get("inv_p");
|
|
const inv_p_p = params.get("inv_p_p");
|
|
const inv_p_np = params.get("inv_p_np");
|
|
const inv_s = params.get("inv_s");
|
|
const inv_s_p = params.get("inv_s_p");
|
|
const inv_s_np = params.get("inv_s_np");
|
|
const pay_posted = params.get("pay_posted");
|
|
const pay_val = params.get("pay_val");
|
|
const eurusd = params.getAll("eurusd").map(Number);
|
|
const eurusd_date = params.getAll("eurusd_date");
|
|
const data = eurusd
|
|
.map((value, i) => ({
|
|
date: eurusd_date[i],
|
|
value
|
|
}))
|
|
.reverse();
|
|
const purchaseData = [
|
|
{ status: "Draft", count: draft_p, color: "bg-teal-500", onClick: () => openInTryton("purchase.purchase", undefined, ['tree', 'form'],[['state', '=', 'draft']])},
|
|
{ status: "Validated", count: val_p, color: "bg-gray-400", onClick: () => openInTryton("purchase.purchase", undefined, ['tree', 'form'],[['state', '=', 'quotation']]) },
|
|
{ status: "Confirmed", count: conf_p, color: "bg-sky-600", onClick: () => openInTryton("purchase.purchase", undefined, ['tree', 'form'],[['state', '=', 'confirmed']]) },
|
|
];
|
|
|
|
const saleData = [
|
|
{ status: "Draft", count: draft_s, color: "bg-teal-500", onClick: () => openInTryton("sale.sale", undefined, ['tree', 'form'],[['state', '=', 'draft']])},
|
|
{ status: "Validated", count: val_s, color: "bg-gray-400", onClick: () => openInTryton("sale.sale", undefined, ['tree', 'form'],[['state', '=', 'quotation']]) },
|
|
{ status: "Confirmed", count: conf_s, color: "bg-sky-600", onClick: () => openInTryton("sale.sale", undefined, ['tree', 'form'],[['state', '=', 'confirmed']]) },
|
|
];
|
|
|
|
const shipmentData = [
|
|
{ status: "Draft", count: shipment_d, color: "bg-teal-500", onClick: () => openInTryton("stock.shipment.in", undefined, ['tree', 'form'],[['state', '=', 'draft']])},
|
|
{ status: "Started", count: shipment_s, color: "bg-gray-400", onClick: () => openInTryton("stock.shipment.in", undefined, ['tree', 'form'],[['state', '=', 'started']]) },
|
|
{ status: "Received", count: shipment_r, color: "bg-sky-600", onClick: () => openInTryton("stock.shipment.in", undefined, ['tree', 'form'],[['state', '=', 'received']]) },
|
|
];
|
|
|
|
const lotData = [
|
|
{ status: "Matched", count: lot_m, color: "bg-teal-500", onClick: () => openInTryton("lot.report", undefined, ['tree', 'form'],[["r_lot_matched", ">", 0]],'exec_window',{
|
|
purchase: null,
|
|
sale: null,
|
|
shipment: null,
|
|
type: 'matched',
|
|
state: 'all',
|
|
wh: 'all',
|
|
group: 'by_physic',
|
|
origin: 'physic'
|
|
})},
|
|
{ status: "Available", count: lot_a, color: "bg-gray-400", onClick: () => openInTryton("lot.report", undefined, ['tree', 'form'],[["r_lot_matched", ">", 0]],'exec_window',{
|
|
purchase: null,
|
|
sale: null,
|
|
shipment: null,
|
|
type: 'not matched',
|
|
state: 'all',
|
|
wh: 'all',
|
|
group: 'by_physic',
|
|
origin: 'physic'
|
|
})},
|
|
{ status: "All", count: lot_al, color: "bg-sky-600", onClick: () => openInTryton("lot.report", undefined, ['tree', 'form'],[["r_lot_matched", ">", 0]],'exec_window',{
|
|
purchase: null,
|
|
sale: null,
|
|
shipment: null,
|
|
type: 'all',
|
|
state: 'all',
|
|
wh: 'all',
|
|
group: 'by_physic',
|
|
origin: 'physic'
|
|
})},
|
|
];
|
|
|
|
const kpis = [
|
|
{
|
|
title: "PNL ($)",
|
|
value: defaultPnlAmount,
|
|
trend: "+12% vs last month",
|
|
trendValue: 4.4,
|
|
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','form'],[['type', '=', 'in']])
|
|
},
|
|
{
|
|
title: "Amount to receive ($)",
|
|
value: toreceive,
|
|
trend: "-1% this month",
|
|
trendValue: -1,
|
|
icon: HandCoins,
|
|
action: () => openInTryton("account.invoice", undefined, ['tree','form'],[['type', '=', 'out']])
|
|
},
|
|
];
|
|
|
|
const latestValue = data.length > 4 ? data[4].value : null;
|
|
const prevValue = data.length > 3 ? data[3].value : null;
|
|
const latestDate = data.length > 4 ? data[4].date : null;
|
|
|
|
let trendValue = null;
|
|
|
|
if (latestValue && prevValue) {
|
|
trendValue = ((latestValue - prevValue) / prevValue) * 100;
|
|
}
|
|
|
|
|
|
const news = [
|
|
{
|
|
type: "Forex",
|
|
pair: "EUR/USD",
|
|
value: latestValue, // nombre
|
|
date: latestDate,
|
|
icon: TrendingUp,
|
|
trendValue,
|
|
},
|
|
// {
|
|
// 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: "All invoices",
|
|
icon: Receipt,
|
|
amountInvoiced: 100523,
|
|
amount30Days: 456147,
|
|
amount60Days: 847512,
|
|
action: () => openInTryton("account.invoice", undefined, ['tree','form'],[['type', '=', 'in']])
|
|
},
|
|
{
|
|
id: "sale_invoices",
|
|
title: "Sale Invoices",
|
|
value: inv_s,
|
|
trend: "All invoices",
|
|
icon: FileText,
|
|
amountInvoiced: 100523,
|
|
amount30Days: 456147,
|
|
amount60Days: 847512,
|
|
action: () => openInTryton("account.invoice", undefined, ['tree','form'],[['type', '=', 'out']])
|
|
},
|
|
// {
|
|
// id: "payments_to_validate",
|
|
// title: "Payments To Validate",
|
|
// value: move_cash,
|
|
// trend: "All payments",
|
|
// icon: HandCoins,
|
|
// amountInvoiced: 100523,
|
|
// amount30Days: 456147,
|
|
// amount60Days: 847512,
|
|
// action: () => openInTryton("account.move", undefined, ['tree','form'],[['journal', '=', 3]])
|
|
// },
|
|
// {
|
|
// 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);
|
|
|
|
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;
|
|
const isPositive = kpi.trendValue >= 0;
|
|
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 cursor-pointer
|
|
hover:-translate-y-0.5 hover:shadow-lg
|
|
transition-all duration-200 ease-out
|
|
"
|
|
>
|
|
|
|
<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>
|
|
|
|
<div className="mt-4 flex flex-col items-center gap-1">
|
|
<span
|
|
className={`
|
|
px-2 py-0.5 text-xs font-semibold rounded-md border
|
|
${
|
|
isPositive
|
|
? "bg-green-50 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800"
|
|
: "bg-red-50 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800"
|
|
}
|
|
`}
|
|
>
|
|
{isPositive ? "+" : ""}
|
|
{kpi.trendValue}%
|
|
</span>
|
|
|
|
<span className="text-xs text-gray-500 dark:text-gray-400">
|
|
vs last month
|
|
</span>
|
|
</div>
|
|
</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">{lot_al}</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 / lot_al) * 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 / lot_al) * 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">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<Title className="text-base">Forex</Title>
|
|
<TrendingUp className="w-6 h-6" />
|
|
</div>
|
|
<div className="space-y-3 text-sm">
|
|
{news.map((n, idx) => {
|
|
return (
|
|
<div
|
|
key={idx}
|
|
className="flex justify-between items-center p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer"
|
|
onClick={() => openInTryton("currency.currency", 2, ["form"])}
|
|
>
|
|
<div className="ml-6">
|
|
{/* LIGNE PRINCIPALE */}
|
|
<div className="flex items-center gap-3 text-gray-500">
|
|
|
|
{/* PAIRE */}
|
|
<span className="text-sm uppercase tracking-wide text-gray-600">
|
|
{n.pair}
|
|
</span>
|
|
|
|
{/* VALEUR */}
|
|
<span className="text-3xl font-bold tabular-nums">
|
|
{n.value?.toFixed(4)}
|
|
</span>
|
|
|
|
{/* DATE */}
|
|
<span className="text-xs text-gray-400">
|
|
{n.date}
|
|
</span>
|
|
|
|
</div>
|
|
|
|
{/* BADGE CENTRÉ */}
|
|
{n.trendValue !== null && (
|
|
<div className="flex justify-center mt-1">
|
|
<span
|
|
className={`
|
|
px-2 py-0.5 text-xs font-semibold rounded-md border
|
|
${
|
|
n.trendValue >= 0
|
|
? "bg-green-50 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800"
|
|
: "bg-red-50 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800"
|
|
}
|
|
`}
|
|
>
|
|
{n.trendValue >= 0 ? "+" : ""}
|
|
{n.trendValue.toFixed(2)}%
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
</div>
|
|
|
|
);
|
|
})}
|
|
</div>
|
|
{/* SEPARATOR */}
|
|
<div className="w-full h-1 bg-gray-200 dark:bg-gray-600 my-4"></div>
|
|
|
|
<AreaChart
|
|
data={data}
|
|
index="date"
|
|
categories={["value"]}
|
|
colors={["teal-300"]}
|
|
className="h-20"
|
|
showXAxis={false}
|
|
showYAxis={false}
|
|
showGridLines={false}
|
|
showLegend={false}
|
|
curve="monotone"
|
|
|
|
yAxisWidth={0}
|
|
minValue={Math.min(...data.map(d => d.value)) * 0.99}
|
|
maxValue={Math.max(...data.map(d => d.value)) * 1.01}
|
|
/>
|
|
|
|
</Card>
|
|
{/* {cards.map((card) => {
|
|
const Icon = card.icon;
|
|
const bar_data = [
|
|
{ label: "Invoiced", invoiced: card.amountInvoiced },
|
|
{ label: "30 days", pay30: card.amount30Days },
|
|
{ label: "60 days", pay60: card.amount60Days },
|
|
];
|
|
|
|
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>
|
|
<BarChart
|
|
data={bar_data}
|
|
index="label"
|
|
categories={["invoiced", "pay30", "pay60"]}
|
|
colors={["teal", "zinc", "slate"]}
|
|
className="h-28"
|
|
showLegend={false}
|
|
showYAxis={false}
|
|
showGridLines={false}
|
|
barRadius={9}
|
|
/>
|
|
|
|
<button onClick={card.action} className="mt-4 text-xs text-teal-600 underline">
|
|
View details
|
|
</button>
|
|
</Card>
|
|
);
|
|
})} */}
|
|
<Card className="p-4 rounded-xl border border-gray-200 dark:border-gray-700 shadow-md bg-white dark:bg-gray-800">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<Title className="text-base">Purchase Invoices</Title>
|
|
<HandCoins className="w-6 h-6" />
|
|
</div>
|
|
|
|
<div className="grid grid-rows-2 gap-3">
|
|
|
|
<button
|
|
onClick={() => openInTryton('account.invoice', undefined, ["tree","form"], [['state','=','paid'],['type','=','in']])}
|
|
className="group rounded-lg border border-gray-200 dark:border-gray-600 p-4 flex flex-col items-center justify-center hover:bg-teal-50 dark:hover:bg-gray-700 transition hover:-translate-y-0.5 hover:shadow-md"
|
|
>
|
|
<span className="text-3xl font-bold text-gray-500 dark:text-white transition-transform group-hover:scale-110">
|
|
{inv_p_p}
|
|
</span>
|
|
<span className="mt-1 text-sm font-semibold tracking-wide text-teal-600">
|
|
Paid
|
|
</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => openInTryton('account.invoice', undefined, ["tree","form"], [['state','!=','paid'],['type','=','in']])}
|
|
className="group rounded-lg border border-gray-200 dark:border-gray-600 p-4 flex flex-col items-center justify-center hover:bg-sky-50 dark:hover:bg-gray-700 transition hover:-translate-y-0.5 hover:shadow-md"
|
|
>
|
|
<span className="text-3xl font-bold text-gray-500 dark:text-white transition-transform group-hover:scale-110">
|
|
{inv_p_np}
|
|
</span>
|
|
<span className="mt-1 text-sm font-semibold tracking-wide text-sky-600">
|
|
Not Paid
|
|
</span>
|
|
</button>
|
|
|
|
</div>
|
|
</Card>
|
|
<Card className="p-4 rounded-xl border border-gray-200 dark:border-gray-700 shadow-md bg-white dark:bg-gray-800">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<Title className="text-base">Sale Invoices</Title>
|
|
<HandCoins className="w-6 h-6" />
|
|
</div>
|
|
|
|
<div className="grid grid-rows-2 gap-3">
|
|
|
|
<button
|
|
onClick={() => openInTryton('account.invoice', undefined, ["tree","form"], [['state','=','paid'],['type','=','out']])}
|
|
className="group rounded-lg border border-gray-200 dark:border-gray-600 p-4 flex flex-col items-center justify-center hover:bg-teal-50 dark:hover:bg-gray-700 transition hover:-translate-y-0.5 hover:shadow-md"
|
|
>
|
|
<span className="text-3xl font-bold text-gray-500 dark:text-white transition-transform group-hover:scale-110">
|
|
{inv_s_p}
|
|
</span>
|
|
<span className="mt-1 text-sm font-semibold tracking-wide text-teal-600">
|
|
Paid
|
|
</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => openInTryton('account.invoice', undefined, ["tree","form"], [['state','!=','paid'],['type','=','out']])}
|
|
className="group rounded-lg border border-gray-200 dark:border-gray-600 p-4 flex flex-col items-center justify-center hover:bg-sky-50 dark:hover:bg-gray-700 transition hover:-translate-y-0.5 hover:shadow-md"
|
|
>
|
|
<span className="text-3xl font-bold text-gray-500 dark:text-white transition-transform group-hover:scale-110">
|
|
{inv_s_np}
|
|
</span>
|
|
<span className="mt-1 text-sm font-semibold tracking-wide text-sky-600">
|
|
Not Paid
|
|
</span>
|
|
</button>
|
|
|
|
</div>
|
|
</Card>
|
|
<Card className="p-4 rounded-xl border border-gray-200 dark:border-gray-700 shadow-md bg-white dark:bg-gray-800">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<Title className="text-base">Payments</Title>
|
|
<HandCoins className="w-6 h-6" />
|
|
</div>
|
|
|
|
<div className="grid grid-rows-2 gap-3">
|
|
|
|
{/* TO VALIDATE */}
|
|
<button
|
|
onClick={() => openInTryton('account.move', undefined, ["tree","form"], [['state','!=','posted'],['journal','=',3]])}
|
|
className="group rounded-lg border border-gray-200 dark:border-gray-600 p-4 flex flex-col items-center justify-center hover:bg-teal-50 dark:hover:bg-gray-700 transition hover:-translate-y-0.5 hover:shadow-md"
|
|
>
|
|
<span className="text-3xl font-bold text-gray-500 dark:text-white transition-transform group-hover:scale-110">
|
|
{pay_val}
|
|
</span>
|
|
<span className="mt-1 text-sm font-semibold tracking-wide text-teal-600">
|
|
To validate
|
|
</span>
|
|
</button>
|
|
|
|
{/* POSTED */}
|
|
<button
|
|
onClick={() => openInTryton('account.move', undefined, ["tree","form"], [['state','=','posted'],['journal','=',3]])}
|
|
className="group rounded-lg border border-gray-200 dark:border-gray-600 p-4 flex flex-col items-center justify-center hover:bg-sky-50 dark:hover:bg-gray-700 transition hover:-translate-y-0.5 hover:shadow-md"
|
|
>
|
|
<span className="text-3xl font-bold text-gray-500 dark:text-white transition-transform group-hover:scale-110">
|
|
{pay_posted}
|
|
</span>
|
|
<span className="mt-1 text-sm font-semibold tracking-wide text-sky-600">
|
|
Posted
|
|
</span>
|
|
</button>
|
|
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|