first commit

This commit is contained in:
2026-03-10 14:55:59 +05:30
commit 39d42663c0
134 changed files with 16311 additions and 0 deletions

View File

@@ -0,0 +1,577 @@
// 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;