Files
vaishnavi-ecommerce-admin-p…/src/pages/Reports/InventoryReport.jsx
2026-03-10 14:55:59 +05:30

578 lines
16 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// import React from "react";
// import { useGetInventoryStatsQuery } from "../../features/report/inventoryAPI";
// const StockBadge = ({ stock }) => {
// if (stock === 0) return <span className="px-2 py-1 text-xs bg-red-100 text-red-700 rounded-full">Out of Stock</span>;
// if (stock <= 5) return <span className="px-2 py-1 text-xs bg-yellow-100 text-yellow-800 rounded-full">Low Stock</span>;
// return <span className="px-2 py-1 text-xs bg-green-100 text-green-700 rounded-full">{stock}</span>;
// };
// const ProductCard = ({ product }) => (
// <div className="bg-white shadow-md rounded-xl p-4 hover:shadow-xl transition transform hover:scale-105">
// <div className="flex justify-between items-center mb-2">
// <h3 className="font-semibold text-gray-800">{product.name}</h3>
// <StockBadge stock={product.stock} />
// </div>
// <p className="text-gray-500 text-sm">{product.category?.name || "Unknown Category"}</p>
// </div>
// );
// const InventorySection = ({ title, products }) => (
// <div className="flex flex-col gap-4">
// <h2 className="text-xl font-bold text-gray-700">{title}</h2>
// {products.length === 0 ? (
// <p className="text-gray-400 italic">No products found</p>
// ) : (
// <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
// {products.map((p) => (
// <ProductCard key={p._id} product={p} />
// ))}
// </div>
// )}
// </div>
// );
// const InventoryReport = () => {
// const { data, isLoading, isError } = useGetInventoryStatsQuery();
// if (isLoading) return <div className="text-center p-6 text-gray-500">Loading inventory...</div>;
// if (isError) return <div className="text-center p-6 text-red-500">Failed to load inventory</div>;
// const { lowStock, outOfStock, fastMoving } = data.data;
// return (
// <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 p-6">
// <InventorySection title="Low Stock" products={lowStock} />
// <InventorySection title="Out of Stock" products={outOfStock} />
// <InventorySection title="Fast Moving" products={fastMoving} />
// </div>
// );
// };
// export default InventoryReport;
// InventoryReport v2 - Editorial warm theme
import React, { useState } from "react";
import { useGetInventoryStatsQuery } from "../../features/report/inventoryAPI";
const styles = `
@import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,600;9..144,700&family=Instrument+Sans:wght@400;500;600&display=swap');
:root {
--bg: #f7f5f2;
--surface: #ffffff;
--border: #e8e4de;
--border2: #f0ece6;
--text: #1a1714;
--muted: #9c9489;
--red: #e8442a;
--red-bg: #fdf1ef;
--red-bdr: #f4c4bc;
--amber: #d97706;
--amber-bg: #fffbeb;
--amber-bdr: #fde68a;
--teal: #0d7a6b;
--teal-bg: #f0faf8;
--teal-bdr: #a7f3e4;
--ink: #2d2a26;
--shadow: 0 1px 3px rgba(0,0,0,0.06), 0 4px 12px rgba(0,0,0,0.04);
--shadow-lg: 0 2px 8px rgba(0,0,0,0.08), 0 12px 32px rgba(0,0,0,0.06);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
.inv-root {
font-family: 'Instrument Sans', sans-serif;
background: var(--bg);
min-height: 100vh;
color: var(--text);
padding: 40px 36px;
}
.inv-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 36px;
flex-wrap: wrap;
gap: 20px;
}
.inv-eyebrow {
font-size: 0.68rem;
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 6px;
}
.inv-title {
font-family: 'Fraunces', serif;
font-size: 2.2rem;
font-weight: 700;
color: var(--ink);
line-height: 1;
letter-spacing: -0.02em;
}
.inv-summary {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.inv-pill {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 16px;
border-radius: 12px;
border: 1px solid var(--border);
background: var(--surface);
box-shadow: var(--shadow);
}
.inv-pill-icon {
width: 32px;
height: 32px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.9rem;
flex-shrink: 0;
}
.inv-pill-val {
font-family: 'Fraunces', serif;
font-size: 1.3rem;
font-weight: 700;
line-height: 1;
color: var(--ink);
}
.inv-pill-label {
font-size: 0.65rem;
font-weight: 500;
color: var(--muted);
letter-spacing: 0.06em;
text-transform: uppercase;
margin-top: 2px;
}
.pill-total .inv-pill-icon { background: #f0ece6; }
.pill-ok .inv-pill-icon { background: var(--teal-bg); }
.pill-low .inv-pill-icon { background: var(--amber-bg); }
.pill-out .inv-pill-icon { background: var(--red-bg); }
.inv-tabs {
display: flex;
gap: 2px;
border-bottom: 1.5px solid var(--border);
margin-bottom: 28px;
}
.inv-tab {
padding: 10px 18px;
border: none;
background: transparent;
color: var(--muted);
font-family: 'Instrument Sans', sans-serif;
font-size: 0.82rem;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 7px;
transition: color 0.18s;
letter-spacing: 0.01em;
border-bottom: 2px solid transparent;
margin-bottom: -1.5px;
}
.inv-tab:hover { color: var(--ink); }
.inv-tab.active { color: var(--ink); border-bottom-color: var(--ink); }
.inv-tab-chip {
font-size: 0.6rem;
font-weight: 600;
padding: 2px 6px;
border-radius: 99px;
letter-spacing: 0.04em;
}
.chip-out { background: var(--red-bg); color: var(--red); }
.chip-low { background: var(--amber-bg); color: var(--amber); }
.chip-fast { background: var(--teal-bg); color: var(--teal); }
.inv-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
gap: 14px;
}
.inv-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
padding: 20px;
box-shadow: var(--shadow);
transition: box-shadow 0.2s, transform 0.2s, border-color 0.2s;
position: relative;
overflow: hidden;
animation: riseIn 0.3s both;
}
.inv-card:hover {
box-shadow: var(--shadow-lg);
transform: translateY(-2px);
border-color: #d8d2ca;
}
@keyframes riseIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.inv-card-stripe {
position: absolute;
left: 0; top: 16px; bottom: 16px;
width: 3px;
border-radius: 0 3px 3px 0;
}
.stripe-red { background: var(--red); }
.stripe-amber { background: var(--amber); }
.stripe-teal { background: var(--teal); }
.inv-card-inner { padding-left: 14px; }
.inv-card-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 8px;
margin-bottom: 4px;
}
.inv-card-name {
font-family: 'Fraunces', serif;
font-size: 0.97rem;
font-weight: 600;
color: var(--ink);
line-height: 1.3;
}
.inv-badge {
font-size: 0.62rem;
font-weight: 600;
padding: 3px 8px;
border-radius: 20px;
white-space: nowrap;
flex-shrink: 0;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.badge-out { background: var(--red-bg); color: var(--red); border: 1px solid var(--red-bdr); }
.badge-low { background: var(--amber-bg); color: var(--amber); border: 1px solid var(--amber-bdr); }
.badge-ok { background: var(--teal-bg); color: var(--teal); border: 1px solid var(--teal-bdr); }
.inv-card-meta {
font-size: 0.68rem;
font-weight: 500;
color: var(--muted);
margin-bottom: 14px;
letter-spacing: 0.03em;
}
.inv-bar-row {
display: flex;
align-items: center;
gap: 10px;
}
.inv-bar-bg {
flex: 1;
height: 4px;
background: var(--border2);
border-radius: 99px;
overflow: hidden;
}
.inv-bar-fill {
height: 100%;
border-radius: 99px;
transition: width 0.55s cubic-bezier(0.34,1.4,0.64,1);
}
.inv-bar-label {
font-size: 0.7rem;
font-weight: 600;
color: var(--muted);
min-width: 24px;
text-align: right;
}
.inv-rank-row {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 4px;
}
.inv-rank-num {
font-family: 'Fraunces', serif;
font-size: 2rem;
font-weight: 700;
color: var(--border);
line-height: 1;
min-width: 44px;
flex-shrink: 0;
}
.inv-purchase-tag {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 0.65rem;
font-weight: 600;
color: var(--teal);
background: var(--teal-bg);
border: 1px solid var(--teal-bdr);
padding: 2px 7px;
border-radius: 99px;
margin-top: 3px;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.inv-empty {
grid-column: 1/-1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 72px 20px;
gap: 10px;
}
.inv-empty-icon { font-size: 2.5rem; opacity: 0.25; }
.inv-empty-text {
font-size: 0.75rem;
font-weight: 600;
color: var(--muted);
letter-spacing: 0.08em;
text-transform: uppercase;
}
.inv-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 60vh;
gap: 14px;
}
.inv-spinner {
width: 32px; height: 32px;
border: 2px solid var(--border);
border-top-color: var(--ink);
border-radius: 50%;
animation: spin 0.75s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.inv-state-text {
font-size: 0.72rem;
font-weight: 600;
color: var(--muted);
letter-spacing: 0.1em;
text-transform: uppercase;
}
.inv-state-text.err { color: var(--red); }
`;
const shortId = (id = "") =>
typeof id === "string" && id.length > 8
? id.slice(-6).toUpperCase()
: String(id).toUpperCase();
const getStockPct = (stock, max = 50) =>
stock === 0 ? 0 : Math.min(100, Math.round((stock / max) * 100));
const StockBadge = ({ stock }) => {
if (stock === 0) return <span className="inv-badge badge-out">No stock</span>;
if (stock <= 5)
return <span className="inv-badge badge-low">{stock} left</span>;
return <span className="inv-badge badge-ok">In stock</span>;
};
const StockBar = ({ stock, fillColor }) => (
<div className="inv-bar-row">
<div className="inv-bar-bg">
<div
className="inv-bar-fill"
style={{ width: `${getStockPct(stock)}%`, background: fillColor }}
/>
</div>
<span className="inv-bar-label">{stock}</span>
</div>
);
const OutCard = ({ product, idx }) => (
<div className="inv-card" style={{ animationDelay: `${idx * 35}ms` }}>
<div className="inv-card-stripe stripe-red" />
<div className="inv-card-inner">
<div className="inv-card-top">
<span className="inv-card-name">{product.name}</span>
<StockBadge stock={0} />
</div>
<div className="inv-card-meta">
Category · {shortId(product.category)}
</div>
<StockBar stock={0} fillColor="var(--red)" />
</div>
</div>
);
const LowCard = ({ product, idx }) => (
<div className="inv-card" style={{ animationDelay: `${idx * 35}ms` }}>
<div className="inv-card-stripe stripe-amber" />
<div className="inv-card-inner">
<div className="inv-card-top">
<span className="inv-card-name">{product.name}</span>
<StockBadge stock={product.stock} />
</div>
<div className="inv-card-meta">
Category · {shortId(product.category)}
</div>
<StockBar stock={product.stock} fillColor="var(--amber)" />
</div>
</div>
);
const FastCard = ({ product, idx }) => (
<div className="inv-card" style={{ animationDelay: `${idx * 35}ms` }}>
<div className="inv-card-stripe stripe-teal" />
<div className="inv-card-inner">
<div className="inv-rank-row">
<span className="inv-rank-num">{String(idx + 1).padStart(2, "0")}</span>
<div style={{ flex: 1 }}>
<div className="inv-card-top" style={{ marginBottom: 2 }}>
<span className="inv-card-name">{product.name}</span>
<StockBadge stock={product.stock} />
</div>
<span className="inv-purchase-tag">
{product.purchaseCount ?? 0} sold
</span>
</div>
</div>
<div className="inv-card-meta" style={{ marginTop: 8 }}>
Category · {shortId(product.category)}
</div>
<StockBar stock={product.stock} fillColor="var(--teal)" />
</div>
</div>
);
const EmptyState = ({ label }) => (
<div className="inv-empty">
<div className="inv-empty-icon"></div>
<div className="inv-empty-text">No {label} products</div>
</div>
);
const TABS = [
{ key: "out", label: "Out of Stock", chipClass: "chip-out" },
{ key: "low", label: "Low Stock", chipClass: "chip-low" },
{ key: "fast", label: "Fast Moving", chipClass: "chip-fast" },
];
const InventoryReport = () => {
const [activeTab, setActiveTab] = useState("out");
const { data, isLoading, isError } = useGetInventoryStatsQuery();
if (isLoading)
return (
<div className="inv-root">
<style>{styles}</style>
<div className="inv-state">
<div className="inv-spinner" />
<span className="inv-state-text">Loading inventory</span>
</div>
</div>
);
if (isError)
return (
<div className="inv-root">
<style>{styles}</style>
<div className="inv-state">
<span style={{ fontSize: "1.8rem" }}></span>
<span className="inv-state-text err">Failed to load data</span>
</div>
</div>
);
const {
summary,
lowStock = [],
outOfStock = [],
fastMoving = [],
} = data.data;
const tabContent = {
out: { products: outOfStock, Card: OutCard, emptyLabel: "out-of-stock" },
low: { products: lowStock, Card: LowCard, emptyLabel: "low-stock" },
fast: { products: fastMoving, Card: FastCard, emptyLabel: "fast-moving" },
};
const { products, Card, emptyLabel } = tabContent[activeTab];
return (
<div className="inv-root">
<style>{styles}</style>
<div className="inv-header">
<div>
<div className="inv-eyebrow">Admin · Reports</div>
<div className="inv-title">Inventory</div>
</div>
{summary && (
<div className="inv-summary">
<div className="inv-pill pill-total">
<div className="inv-pill-icon">📦</div>
<div>
<div className="inv-pill-val">{summary.totalProducts}</div>
<div className="inv-pill-label">Total</div>
</div>
</div>
<div className="inv-pill pill-ok">
<div className="inv-pill-icon"></div>
<div>
<div className="inv-pill-val">{summary.healthyStockCount}</div>
<div className="inv-pill-label">Healthy</div>
</div>
</div>
<div className="inv-pill pill-low">
<div className="inv-pill-icon"></div>
<div>
<div className="inv-pill-val">{summary.lowStockCount}</div>
<div className="inv-pill-label">Low Stock</div>
</div>
</div>
<div className="inv-pill pill-out">
<div className="inv-pill-icon">🚫</div>
<div>
<div className="inv-pill-val">{summary.outOfStockCount}</div>
<div className="inv-pill-label">Out of Stock</div>
</div>
</div>
</div>
)}
</div>
<div className="inv-tabs">
{TABS.map((tab) => (
<button
key={tab.key}
className={`inv-tab${activeTab === tab.key ? " active" : ""}`}
onClick={() => setActiveTab(tab.key)}
>
{tab.label}
<span className={`inv-tab-chip ${tab.chipClass}`}>
{tabContent[tab.key].products.length}
</span>
</button>
))}
</div>
<div className="inv-grid">
{products.length === 0 ? (
<EmptyState label={emptyLabel} />
) : (
products.map((p, i) => <Card key={p._id} product={p} idx={i} />)
)}
</div>
</div>
);
};
export default InventoryReport;